Harden first-release validation and dependency hygiene
This commit is contained in:
9
tools/check_format.py
Normal file
9
tools/check_format.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Check every Rust source file, including include! fragments."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
files = sorted(p for folder in ("src", "tests") for p in (root / folder).rglob("*.rs"))
|
||||
subprocess.run(["cargo", "fmt", "--all", "--check"], cwd=root, check=True)
|
||||
subprocess.run(["rustfmt", "--check", "--edition", "2024", *map(str, files)],
|
||||
cwd=root, check=True)
|
||||
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()
|
||||
14
tools/license_supplements.json
Normal file
14
tools/license_supplements.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"alloc-stdlib@0.2.4": {
|
||||
"url": "https://raw.githubusercontent.com/dropbox/rust-alloc-no-stdlib/ae42d22078b98549e987d2f03d12df7b984fde47/LICENSE",
|
||||
"sha256": "c0c56f26d9c051cac4d200c34c84e7ae9aaa853e01a982a1df08b09931e518ae"
|
||||
},
|
||||
"asn1-rs-impl@0.2.0": {
|
||||
"url": "https://raw.githubusercontent.com/rusticata/asn1-rs/a20e5f7319c896737ad0f2557037817b91ad854f/LICENSE-MIT",
|
||||
"sha256": "a5c61b93b6ee1d104af9920cf020ff3c7efe818e31fe562c72261847a728f513"
|
||||
},
|
||||
"cookie-factory@0.3.3": {
|
||||
"url": "https://raw.githubusercontent.com/rust-bakery/cookie-factory/d36b805dbd7dd65f2df947235c5bcc573afe2c76/LICENSES/MIT.txt",
|
||||
"sha256": "d09216dc1ea5f273997667935a8d6514d80f00b89fb6954ecc3a43e0a6e360de"
|
||||
}
|
||||
}
|
||||
59
tools/test_dependency_notices.py
Normal file
59
tools/test_dependency_notices.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Offline regression checks for release-maintenance failure handling."""
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import dependency_notices as notices
|
||||
|
||||
|
||||
class NoticeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
(self.root / "tools").mkdir()
|
||||
(self.root / "package").mkdir()
|
||||
(self.root / "tools/license_supplements.json").write_text("{}")
|
||||
self.metadata = json.dumps({"workspace_members": [], "packages": [{
|
||||
"id": "example", "name": "example", "version": "1.0.0", "license": "MIT",
|
||||
"manifest_path": str(self.root / "package/Cargo.toml"),
|
||||
}]}).encode()
|
||||
self.patch = patch.object(notices, "ROOT", self.root)
|
||||
self.patch.start()
|
||||
self.addCleanup(self.patch.stop)
|
||||
|
||||
def test_missing_license_fails(self):
|
||||
with patch.object(notices.subprocess, "check_output", return_value=self.metadata):
|
||||
with self.assertRaisesRegex(RuntimeError, "Missing license"):
|
||||
notices.generate()
|
||||
|
||||
def test_license_text_is_preserved(self):
|
||||
original = "Copyright Example \n\nMIT license text\n"
|
||||
(self.root / "package/LICENSE").write_text(original)
|
||||
with patch.object(notices.subprocess, "check_output", return_value=self.metadata):
|
||||
outputs = notices.generate()
|
||||
self.assertIn(original, outputs[self.root / "docs/third-party-licenses.txt"])
|
||||
self.assertFalse((self.root / "THIRD_PARTY_NOTICES.md").exists())
|
||||
|
||||
def test_supplement_checksum_mismatch_fails(self):
|
||||
(self.root / "tools/license_supplements.json").write_text(json.dumps({
|
||||
"example@1.0.0": {"url": "https://example.invalid/LICENSE", "sha256": "0" * 64}
|
||||
}))
|
||||
with patch.object(notices.subprocess, "check_output", side_effect=[self.metadata, b"bad"]):
|
||||
with self.assertRaisesRegex(RuntimeError, "checksum mismatch"):
|
||||
notices.generate()
|
||||
|
||||
def test_check_does_not_overwrite_outdated_file(self):
|
||||
path = self.root / "THIRD_PARTY_NOTICES.md"
|
||||
path.write_text("old")
|
||||
with patch.object(notices, "generate", return_value={path: "new"}):
|
||||
with patch("sys.argv", ["dependency_notices.py", "--check"]):
|
||||
with self.assertRaisesRegex(SystemExit, "Outdated notices"):
|
||||
notices.main()
|
||||
self.assertEqual(path.read_text(), "old")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user