Harden first-release validation and dependency hygiene
This commit is contained in:
86
tools/dependency_notices.py
Normal file
86
tools/dependency_notices.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user