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 <br...@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,