87 lines
4.1 KiB
Python
87 lines
4.1 KiB
Python
"""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()
|