bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

gnulib-tool.py: Allow verifying license compatibility with GPLv2+


From: Bruno Haible
Subject: gnulib-tool.py: Allow verifying license compatibility with GPLv2+
Date: Thu, 29 Aug 2024 18:34:47 +0200

Some packages that use Gnulib are under GPLv2+. It is useful for these packages
— just for packages under LGPL — to be able to verify that the modules they
import are compatible with this license choice.

This patch does it, by adding an option --gpl=2 to gnulib-tool.

The option --gpl=3 is also accepted, but is currently a no-op.

In the implementation of the serialization format (gnulib-cache.m4)
this adds a gl_GPL pseudo-macro. I considered merging gl_LGPL and gl_GPL
into a single pseudo-macro, but that would have become more complicated
(due to the need to handle new and old gnulib-cache.m4 files).


2024-08-29  Bruno Haible  <bruno@clisp.org>

        gnulib-tool.py: Allow verifying license compatibility with GPLv2+.
        * pygnulib/GLInfo.py (GLInfo.usage): Document the --gpl option.
        * pygnulib/main.py (main): Accept a --gpl option. Pass it to the
        GLConfig.
        * pygnulib/GLConfig.py (GLConfig): Add 'gpl' field and constructor
        argument. Add getGPL, setGPL, resetGPL methods.
        * m4/gnulib-tool.m4 (gl_GPL): New macro.
        * doc/gnulib-tool.texi (Modified imports): Document the gl_GPL macro.
        * pygnulib/GLImport.py (GLImport.__init__): Look for gl_GPL invocations
        in gnulib-cache.m4.
        (GLImport.actioncmd): Output --gpl option when option --gpl was given.
        (GLImport.gnulib_cache): Emit a gl_GPL invocation when option --gpl was
        given.
        (GLImport.prepare): Do license compatibility checking when option --gpl
        was given.
        * pygnulib/GLModuleSystem.py: Update a comment.

diff --git a/doc/gnulib-tool.texi b/doc/gnulib-tool.texi
index 41eccb3019..470342722d 100644
--- a/doc/gnulib-tool.texi
+++ b/doc/gnulib-tool.texi
@@ -454,6 +454,10 @@
 value must be 2 or 3) corresponds to the @samp{--lgpl=@var{arg}} command line
 argument.
 
+@item gl_GPL
+The presence of this macro with an argument (whose value must be 2 or 3)
+corresponds to the @samp{--gpl=@var{arg}} command line argument.
+
 @item gl_MAKEFILE_NAME
 The argument is the name of the makefile in the source-base and tests-base
 directories.  Corresponds to the @samp{--makefile-name} command line argument.
diff --git a/m4/gnulib-tool.m4 b/m4/gnulib-tool.m4
index ef45f51fc8..2f517f1bbc 100644
--- a/m4/gnulib-tool.m4
+++ b/m4/gnulib-tool.m4
@@ -1,5 +1,5 @@
 # gnulib-tool.m4
-# serial 4
+# serial 5
 dnl Copyright (C) 2004-2005, 2009-2024 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -42,6 +42,9 @@ AC_DEFUN([gl_LIB]
 dnl Usage: gl_LGPL or gl_LGPL([VERSION])
 AC_DEFUN([gl_LGPL], [])
 
+dnl Usage: gl_GPL([VERSION])
+AC_DEFUN([gl_GPL], [])
+
 dnl Usage: gl_MAKEFILE_NAME([FILENAME])
 AC_DEFUN([gl_MAKEFILE_NAME], [])
 
diff --git a/pygnulib/GLConfig.py b/pygnulib/GLConfig.py
index b8a7fc5b0b..353701cd3b 100644
--- a/pygnulib/GLConfig.py
+++ b/pygnulib/GLConfig.py
@@ -60,6 +60,7 @@ def __init__(self,
                  excl_test_categories: list[int] | tuple[int] | None = None,
                  libname: str | None = None,
                  lgpl: str | bool | None = None,
+                 gpl: str | None = None,
                  gnu_make: bool | None = None,
                  makefile_name: str | None = None,
                  tests_makefile_name: str | None = None,
@@ -151,6 +152,10 @@ def __init__(self,
         self.resetLGPL()
         if lgpl != None:
             self.setLGPL(lgpl)
+        # gpl
+        self.resetGPL()
+        if gpl != None:
+            self.setGPL(gpl)
         # gnu-make
         self.resetGnuMake()
         if gnu_make != None:
@@ -318,7 +323,7 @@ def default(self, key: str) -> Any:
                 return False
             elif key in ['copymode', 'lcopymode']:
                 return CopyAction.Copy
-            elif key in ['lgpl', 'libtool', 'conddeps', 'vc_files']:
+            elif key in ['lgpl', 'gpl', 'libtool', 'conddeps', 'vc_files']:
                 return None
             elif key == 'errors':
                 return True
@@ -834,6 +839,25 @@ def resetLGPL(self) -> None:
         Default value is None, which means that lgpl is disabled.'''
         self.table['lgpl'] = None
 
+    # Define gpl methods.
+    def getGPL(self) -> str | None:
+        '''Check for abort if modules aren't available under the GPL.
+        Default value is None, which means that gpl is disabled.'''
+        return self.table['gpl']
+
+    def setGPL(self, gpl: str | bool | None) -> None:
+        '''Abort if modules aren't available under the GPL.
+        Default value is None, which means that gpl is disabled.'''
+        if gpl in [None, '2', '3']:
+            self.table['gpl'] = gpl
+        else:
+            raise TypeError('invalid GPL version: %s' % repr(gpl))
+
+    def resetGPL(self) -> None:
+        '''Disable abort if modules aren't available under the GPL.
+        Default value is None, which means that gpl is disabled.'''
+        self.table['gpl'] = None
+
     # Define gnu-make methods.
     def getGnuMake(self) -> bool:
         '''Return a boolean value describing whether the --gnu-make argument
diff --git a/pygnulib/GLImport.py b/pygnulib/GLImport.py
index 5f090d4ea1..32ab54b29a 100644
--- a/pygnulib/GLImport.py
+++ b/pygnulib/GLImport.py
@@ -175,7 +175,8 @@ def __init__(self, config: GLConfig, mode: int, m4dirs: 
list[str]) -> None:
                 [
                     'gl_LOCAL_DIR', 'gl_MODULES', 'gl_AVOID', 'gl_SOURCE_BASE',
                     'gl_M4_BASE', 'gl_PO_BASE', 'gl_DOC_BASE', 'gl_TESTS_BASE',
-                    'gl_LIB', 'gl_LGPL', 'gl_MAKEFILE_NAME', 
'gl_TESTS_MAKEFILE_NAME',
+                    'gl_LIB', 'gl_LGPL', 'gl_GPL',
+                    'gl_MAKEFILE_NAME', 'gl_TESTS_MAKEFILE_NAME',
                     'gl_MACRO_PREFIX', 'gl_PO_DOMAIN', 'gl_WITNESS_C_MACRO',
                     'gl_VC_FILES',
                 ]
@@ -191,6 +192,8 @@ def __init__(self, config: GLConfig, mode: int, m4dirs: 
list[str]) -> None:
                 self.cache.setLGPL(None)
             if tempdict['gl_LIB']:
                 self.cache.setLibName(cleaner(tempdict['gl_LIB']))
+            if tempdict['gl_GPL']:
+                self.cache.setLibName(cleaner(tempdict['gl_GPL']))
             if tempdict['gl_LOCAL_DIR']:
                 
self.cache.setLocalPath(cleaner(tempdict['gl_LOCAL_DIR']).split(':'))
             if tempdict['gl_MODULES']:
@@ -331,6 +334,7 @@ def actioncmd(self) -> str:
         conddeps = self.config.checkCondDeps()
         libname = self.config.getLibName()
         lgpl = self.config.getLGPL()
+        gpl = self.config.getGPL()
         gnu_make = self.config.getGnuMake()
         makefile_name = self.config.getMakefileName()
         tests_makefile_name = self.config.getTestsMakefileName()
@@ -380,6 +384,8 @@ def actioncmd(self) -> str:
                 actioncmd += ' \\\n#  --lgpl'
             else:  # if lgpl != True
                 actioncmd += ' \\\n#  --lgpl=%s' % lgpl
+        if gpl:
+            actioncmd += ' \\\n#  --gpl=%s' % gpl
         if gnu_make:
             actioncmd += ' \\\n#  --gnu-make'
         if makefile_name:
@@ -455,6 +461,7 @@ def gnulib_cache(self) -> str:
         docbase = self.config['docbase']
         testsbase = self.config['testsbase']
         lgpl = self.config['lgpl']
+        gpl = self.config['gpl']
         libname = self.config['libname']
         makefile_name = self.config['makefile_name']
         tests_makefile_name = self.config['tests_makefile_name']
@@ -512,6 +519,8 @@ def gnulib_cache(self) -> str:
                 emit += 'gl_LGPL\n'
             else:  # if lgpl != True
                 emit += 'gl_LGPL([%s])\n' % lgpl
+        if gpl != None:
+            emit += 'gl_GPL([%s])\n' % gpl
         emit += 'gl_MAKEFILE_NAME([%s])\n' % makefile_name
         if tests_makefile_name:
             emit += 'gl_TESTS_MAKEFILE_NAME([%s])\n' % tests_makefile_name
@@ -751,6 +760,7 @@ def prepare(self) -> tuple[GLFileTable, dict[str, 
tuple[re.Pattern, str] | None]
         modules = list(self.config['modules'])
         m4base = self.config['m4base']
         lgpl = self.config['lgpl']
+        gpl = self.config['gpl']
         verbose = self.config['verbosity']
         base_modules = set()
         for name in modules:
@@ -815,18 +825,23 @@ def prepare(self) -> tuple[GLFileTable, dict[str, 
tuple[re.Pattern, str] | None]
         compatibilities['all'] = ['GPLv2+ build tool', 'GPLed build tool',
                                   'public domain', 'unlimited',
                                   'unmodifiable license text']
-        compatibilities['3']        = ['LGPLv2+', 'LGPLv3+ or GPLv2+', 
'LGPLv3+', 'LGPL']
-        compatibilities['3orGPLv2'] = ['LGPLv2+', 'LGPLv3+ or GPLv2+']
-        compatibilities['2']        = ['LGPLv2+']
-        if lgpl:
+        compatibilities['GPLv3']         = ['LGPLv2+', 'LGPLv3+ or GPLv2+', 
'LGPLv3+', 'LGPL', 'GPLv2+', 'GPLv3+', 'GPL']
+        compatibilities['GPLv2']         = ['LGPLv2+', 'LGPLv3+ or GPLv2+', 
'GPLv2+']
+        compatibilities['LGPLv3']        = ['LGPLv2+', 'LGPLv3+ or GPLv2+', 
'LGPLv3+', 'LGPL']
+        compatibilities['LGPLv3orGPLv2'] = ['LGPLv2+', 'LGPLv3+ or GPLv2+']
+        compatibilities['LGPLv2']        = ['LGPLv2+']
+        if lgpl or gpl:
             for module in main_modules:
                 license = module.getLicense()
                 if license not in compatibilities['all']:
                     if lgpl == True:
-                        if license not in compatibilities['3']:
+                        if license not in compatibilities['LGPLv3']:
+                            listing.append(tuple([module.name, license]))
+                    elif lgpl:
+                        if license not in compatibilities['LGPLv'+lgpl]:
                             listing.append(tuple([module.name, license]))
-                    else:
-                        if license not in compatibilities[lgpl]:
+                    elif gpl:
+                        if license not in compatibilities['GPLv'+gpl]:
                             listing.append(tuple([module.name, license]))
             if listing:
                 raise GLError(11, listing)
diff --git a/pygnulib/GLInfo.py b/pygnulib/GLInfo.py
index a8277f2471..1ade11a3e1 100644
--- a/pygnulib/GLInfo.py
+++ b/pygnulib/GLInfo.py
@@ -276,6 +276,8 @@ def usage(self) -> str:
                             Abort if modules aren't available under the LGPL.
                             The version number of the LGPL can be specified;
                             the default is currently LGPLv3.
+      --gpl=[2|3]           Abort if modules aren't available under the
+                            specified GPL version.
       --makefile-name=NAME  Name of makefile in the source-base and tests-base
                             directories (default \"Makefile.am\", or
                             \"Makefile.in\" if --gnu-make).
diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py
index e9855f775c..4185765ab2 100644
--- a/pygnulib/GLModuleSystem.py
+++ b/pygnulib/GLModuleSystem.py
@@ -1000,7 +1000,9 @@ def transitive_closure_separately(self, basemodules: 
list[GLModule],
         '''Determine main module list and tests-related module list separately.
         The main module list is the transitive closure of the specified 
modules,
         ignoring tests modules. Its lib/* sources go into $sourcebase/. If 
lgpl is
-        specified, it will consist only of LGPLed source.
+        specified, it will consist only of LGPLed source; if gpl is specified, 
it
+        will consist of only source than can be relicensed under the specified 
GPL
+        version.
         The tests-related module list is the transitive closure of the 
specified
         modules, including tests modules, minus the main module list excluding
         modules of applicability 'all'. Its lib/* sources (brought in through
diff --git a/pygnulib/main.py b/pygnulib/main.py
index f39a05bfd3..f6aa9d47ea 100644
--- a/pygnulib/main.py
+++ b/pygnulib/main.py
@@ -427,6 +427,12 @@ def main(temp_directory: str) -> None:
                         action='append',
                         choices=['2', '3orGPLv2', '3'],
                         nargs='?')
+    # gpl
+    parser.add_argument('--gpl',
+                        dest='gpl',
+                        default=None,
+                        choices=['2', '3'],
+                        nargs=1)
     # gnu-make
     parser.add_argument('--gnu-make',
                         dest='gnu_make',
@@ -714,7 +720,7 @@ def main(temp_directory: str) -> None:
                  or cmdargs.excl_privileged_tests != None
                  or cmdargs.excl_unportable_tests != None
                  or cmdargs.avoids != None or cmdargs.lgpl != None
-                 or cmdargs.makefile_name != None
+                 or cmdargs.gpl != None or cmdargs.makefile_name != None
                  or cmdargs.tests_makefile_name != None
                  or cmdargs.automake_subdir != None
                  or cmdargs.automake_subdir_tests != None
@@ -733,6 +739,13 @@ def main(temp_directory: str) -> None:
         message += '%s: *** Stop.\n' % APP['name']
         sys.stderr.write(message)
         sys.exit(1)
+    if cmdargs.lgpl != None and cmdargs.gpl != None:
+        message = '%s: *** ' % APP['name']
+        message += 'invalid options: cannot specify both --lgpl and --gpl\n'
+        message += 'Try \'gnulib-tool --help\' for more information.\n'
+        message += '%s: *** Stop.\n' % APP['name']
+        sys.stderr.write(message)
+        sys.exit(1)
     if cmdargs.pobase == None and cmdargs.podomain != None:
         message = 'gnulib-tool: warning: --po-domain has no effect without a 
--po-base option\n'
         sys.stderr.write(message)
@@ -808,6 +821,9 @@ def main(temp_directory: str) -> None:
         lgpl = lgpl[-1]
         if lgpl == None:
             lgpl = True
+    gpl = cmdargs.gpl
+    if gpl != None:
+        gpl = gpl[0]
     cond_dependencies = cmdargs.cond_dependencies
     libtool = cmdargs.libtool
     gnu_make = cmdargs.gnu_make == True
@@ -856,6 +872,7 @@ def main(temp_directory: str) -> None:
         excl_test_categories=excl_test_categories,
         libname=libname,
         lgpl=lgpl,
+        gpl=gpl,
         gnu_make=gnu_make,
         makefile_name=makefile_name,
         tests_makefile_name=tests_makefile_name,






reply via email to

[Prev in Thread] Current Thread [Next in Thread]