| """Generates Starlark source lists for Gnulib license tiers using pygnulib.""" | |
| import argparse | |
| import pathlib | |
| import sys | |
| from pygnulib import constants | |
| from pygnulib.GLConfig import GLConfig | |
| from pygnulib.GLModuleSystem import GLModule, GLModuleSystem | |
| sys.dont_write_bytecode = True | |
| ALL_COMPATIBLE_LICENSES = frozenset({ | |
| "GPLv2+ build tool", | |
| "GPLed build tool", | |
| "public domain", | |
| "unlimited", | |
| "unmodifiable license text", | |
| }) | |
| LGPL2_COMPATIBLE_LICENSES = ALL_COMPATIBLE_LICENSES | frozenset({ | |
| "LGPLv2+", | |
| }) | |
| LGPL3_COMPATIBLE_LICENSES = ALL_COMPATIBLE_LICENSES | frozenset({ | |
| "LGPLv2+", | |
| "LGPLv3+ or GPLv2+", | |
| "LGPLv3+", | |
| "LGPL", | |
| }) | |
| INFRASTRUCTURE_PREFIXES = ( | |
| "pygnulib/", | |
| "build-aux/", | |
| "top/", | |
| "config/", | |
| ) | |
| INFRASTRUCTURE_EXACT_FILES = frozenset({ | |
| "gnulib-tool", | |
| "gnulib-tool.py", | |
| ".gnulib-tool.py", | |
| "gnulib-tool.sh", | |
| "m4/gnulib-tool.m4", | |
| "doc/INSTALL", | |
| "doc/COPYING.LESSERv2", | |
| "doc/COPYING.LESSERv3", | |
| "doc/COPYINGv2", | |
| "doc/COPYINGv3", | |
| }) | |
| def _find_gnulib_dir(inputs: list[pathlib.Path]) -> pathlib.Path: | |
| matches = [input_path for input_path in inputs if input_path.name == "gnulib-tool.py"] | |
| if len(matches) != 1: | |
| raise ValueError( | |
| f"Expected exactly 1 'gnulib-tool.py' among inputs, found {len(matches)}: {matches}" | |
| ) | |
| return matches[0].parent | |
| def _format_starlark_tuple(name: str, items: list[str]) -> str: | |
| lines = [f"{name} = ("] | |
| for item in items: | |
| lines.append(f' "{item}",') | |
| lines.append(")") | |
| return "\n".join(lines) | |
| def generate_gnulib_srcs(inputs: list[pathlib.Path], output: pathlib.Path) -> None: | |
| gnulib_dir = _find_gnulib_dir(inputs) | |
| constants.init_DIRS(str(gnulib_dir)) | |
| existing_files: set[str] = set() | |
| for input_path in inputs: | |
| relative_path = input_path.relative_to(gnulib_dir).as_posix() | |
| existing_files.add(relative_path) | |
| config = GLConfig() | |
| module_system = GLModuleSystem(config) | |
| non_test_modules: list[GLModule] = [] | |
| for relative_path in sorted(existing_files): | |
| if not relative_path.startswith("modules/"): | |
| continue | |
| module_name = relative_path.removeprefix("modules/") | |
| if not module_system.file_is_module(module_name): | |
| continue | |
| module = module_system.find(module_name) | |
| if module is not None and module.isNonTests(): | |
| non_test_modules.append(module) | |
| all_module_files: set[str] = set() | |
| for module in non_test_modules: | |
| all_module_files.update(module.getFiles()) | |
| infrastructure_files = { | |
| relative_path | |
| for relative_path in existing_files - all_module_files | |
| if relative_path.startswith(INFRASTRUCTURE_PREFIXES) | |
| or relative_path in INFRASTRUCTURE_EXACT_FILES | |
| } | |
| def collect_tier_files(tier_name: str, allowed_licenses: frozenset[str]) -> list[str]: | |
| allowed_modules = [ | |
| module for module in non_test_modules if module.getLicense() in allowed_licenses | |
| ] | |
| allowed_module_set = set(allowed_modules) | |
| for module in allowed_modules: | |
| for dependency in module.getDependenciesWithoutConditions(): | |
| if dependency not in allowed_module_set: | |
| raise ValueError( | |
| f"Module '{module.name}' (license: '{module.getLicense()}') in {tier_name} " | |
| f"depends on '{dependency.name}' (license: '{dependency.getLicense()}'), " | |
| f"which is outside the allowed module set for {tier_name}." | |
| ) | |
| tier_files = set(infrastructure_files) | |
| for module in allowed_modules: | |
| module_files = {f"modules/{module.name}", *module.getFiles()} | |
| tier_files |= module_files & existing_files | |
| return sorted(tier_files) | |
| lgpl2_srcs = collect_tier_files("LGPL2_SRCS", LGPL2_COMPATIBLE_LICENSES) | |
| lgpl3_srcs = collect_tier_files("LGPL3_SRCS", LGPL3_COMPATIBLE_LICENSES) | |
| header = "\n".join([ | |
| "# Generated by //third_party/gnulib:write_gnulib_srcs. DO NOT EDIT.", | |
| "# Regenerate with: bazel run //third_party/gnulib:write_gnulib_srcs", | |
| '"""Generated Gnulib license-scoped source file lists."""', | |
| ]) | |
| content = "\n\n".join([ | |
| header, | |
| _format_starlark_tuple("LGPL2_SRCS", lgpl2_srcs), | |
| _format_starlark_tuple("LGPL3_SRCS", lgpl3_srcs), | |
| ]) + "\n" | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| output.write_text(content, encoding="utf-8") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Generate Starlark source lists for Gnulib license tiers.", | |
| fromfile_prefix_chars="@", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| "-o", | |
| required=True, | |
| type=pathlib.Path, | |
| help="Output .bzl file path", | |
| ) | |
| parser.add_argument( | |
| "inputs", | |
| nargs="*", | |
| type=pathlib.Path, | |
| help="Input files from the Gnulib repository", | |
| ) | |
| args = parser.parse_args() | |
| generate_gnulib_srcs(args.inputs, args.output) | |
| if __name__ == "__main__": | |
| main() |