"""The wheelhouse manifest is the proof used to admit restored build output.""" from __future__ import annotations import argparse import hashlib import json import re from pathlib import Path def build_identity(repo: Path, builder: str, platform_tag: str, python_abi: str) -> dict[str, str]: files = ( "uv.lock", "pyproject.toml", "pm/lock.json", "pm/termux_runtime_libs.json", "scripts/termux/build_config.sh", "scripts/termux/termux_build.sh", "scripts/termux/build_wheels.py", "scripts/termux/retag_wheel.py", "scripts/termux/python_linkage.py", "scripts/termux/wheelhouse_cache.py", "scripts/termux/build_environment.py", "pm/environment.py", "pm/build_operations.py", "pm/operations.py", "pm/pyproject.toml", "pm/uv.lock", "scripts/termux/termux-builder.Dockerfile", ) return { "builder": builder, "platformTag": platform_tag, "pythonAbi": python_abi, **{name: _sha256(repo / name) for name in files}, } def _sha256(path: Path) -> str: with path.open("rb") as stream: return hashlib.file_digest(stream, "sha256").hexdigest() def write_manifest(payload: Path, identity: dict[str, str], **metadata: str) -> None: wheels = sorted((payload / "wheelhouse").glob("*.whl")) if not wheels: raise ValueError("cannot cache an empty wheelhouse") index = { **metadata, "schemaVersion": 2, "inputs": identity, "resolvedSha256": _sha256(payload / ".work/resolved.txt"), "buildSetSha256": _sha256(payload / ".work/build_set.txt"), "wheels": [{"name": w.name, "sha256": _sha256(w)} for w in wheels], } (payload / "index.json").write_text(json.dumps(index, indent=2) + "\n", encoding="utf-8") (payload / "SHA256SUMS").write_text( "".join(f"{w['sha256']} {w['name']}\n" for w in index["wheels"]), encoding="utf-8", ) def is_usable(payload: Path, identity: dict[str, str]) -> bool: try: index = json.loads((payload / "index.json").read_text(encoding="utf-8-sig")) if index["schemaVersion"] != 2 or index["inputs"] != identity: return False if index["resolvedSha256"] != _sha256(payload / ".work/resolved.txt"): return False if index["buildSetSha256"] != _sha256(payload / ".work/build_set.txt"): return False wheels = index["wheels"] names = [w["name"] for w in wheels] actual = {w.name for w in (payload / "wheelhouse").glob("*.whl")} if not names or len(names) != len(set(names)) or set(names) != actual: return False for wheel in wheels: name = wheel["name"] if "/" in name or "\\" in name or name in (".", ".."): return False path = payload / "wheelhouse" / name if path.is_symlink() or _sha256(path) != wheel["sha256"]: return False return True except (OSError, ValueError, KeyError, TypeError): return False def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("action", choices=("check", "write")) parser.add_argument("--payload", type=Path, required=True) parser.add_argument("--repo", type=Path, required=True) parser.add_argument("--builder", required=True) parser.add_argument("--platform-tag", required=True) parser.add_argument("--python-abi", required=True) provenance = parser.add_mutually_exclusive_group() provenance.add_argument("--tag", default="") provenance.add_argument("--commit") args = parser.parse_args() if args.commit is not None and not re.fullmatch(r"[a-f0-9]{40}", args.commit): parser.error("--commit requires an exact full SHA") identity = build_identity(args.repo, args.builder, args.platform_tag, args.python_abi) if args.action == "check": return 0 if is_usable(args.payload, identity) else 1 write_manifest( args.payload, identity, **({"commit": args.commit} if args.commit else {"tag": args.tag}), platformTag=args.platform_tag, pythonAbi=args.python_abi, ) return 0 if __name__ == "__main__": raise SystemExit(main())