"""Reproduce notices from locked Cargo archives and pinned upstream supplements.""" import argparse import hashlib import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] HEADER = """# Third-party notices Panda RPKI is distributed under BSD-3-Clause; dependencies retain their own licenses. The table below records the locked Cargo dependency graph, including build-time and platform-specific packages that may not be linked on Linux. Upstream license expressions are reproduced as declared by each package. The accompanying [license texts](docs/third-party-licenses.txt) retain notices from package archives, including bundled native code. These files must accompany binary distributions. Container OS package notices remain under `/usr/share/doc/` and `/usr/share/common-licenses/` in the Debian image. Python cryptography and OpenSSL are external test tools, not bundled source. Consult their installed license notices when distributing a test environment. No production trust-anchor or repository data is included in this source tree. | Package | Locked version | Declared license | | --- | --- | --- | """ def generate(): metadata = json.loads(subprocess.check_output( ["cargo", "metadata", "--locked", "--format-version", "1"], cwd=ROOT)) supplements = json.loads((ROOT / "tools/license_supplements.json").read_text()) rows, texts = [], [] for package in sorted(metadata["packages"], key=lambda p: (p["name"], p["version"])): if package["id"] in metadata["workspace_members"]: continue name, version = package["name"], package["version"] source = Path(package["manifest_path"]).parent candidates = sorted(p for p in source.rglob("*") if p.is_file() and (p.name.upper().startswith(("LICENSE", "LICENCE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS")) or "LICENSES" in p.relative_to(source).parts) and p.stat().st_size < 500_000) contents = [] for path in candidates: try: contents.append((str(path.relative_to(source)), path.read_text(encoding="utf-8"))) except UnicodeDecodeError: continue if not contents: entry = supplements.get(f"{name}@{version}") if entry is None: raise RuntimeError(f"Missing license texts: {name}@{version}; review upstream sources") data = subprocess.check_output([ "curl", "--fail", "--silent", "--show-error", "--location", "--proto", "=https", "--proto-redir", "=https", "--connect-timeout", "15", "--max-time", "120", entry["url"]]) if hashlib.sha256(data).hexdigest() != entry["sha256"]: raise RuntimeError(f"Supplement checksum mismatch: {name}@{version}") contents.append(("LICENSE (upstream package revision)", data.decode("utf-8"))) rows.append(f"| {name} | {version} | {package['license'] or 'See upstream license'} |") for label, content in contents: texts.append(f"\n{'=' * 72}\n{name} {version} — {label}\n{'=' * 72}\n{content}\n") return { ROOT / "THIRD_PARTY_NOTICES.md": HEADER + "\n".join(rows) + "\n", ROOT / "docs/third-party-licenses.txt": "Third-party license texts from locked package archives.\n" + "".join(texts), } def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="Fail if tracked notices differ; do not write files") args = parser.parse_args() outputs = generate() # Resolve everything before writing any output. for path, content in outputs.items(): if args.check: if not path.exists() or path.read_bytes() != content.encode("utf-8"): raise SystemExit(f"Outdated notices: {path.name}; run tools/dependency_notices.py") else: path.write_bytes(content.encode("utf-8")) print("Dependency notices are current." if args.check else "Dependency notices generated.") if __name__ == "__main__": main()