Stage npm, ffmpeg and its bionic runtime libraries, and static ARM ripgrep. Bind caches to actual build inputs. Generate entrypoints from the project manifest and verify real CLI/TUI startup and media conversion offline. Keep versions unchanged. Windows service work remains out of scope.
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
"""The wheelhouse manifest is the proof used to admit restored build output."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
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",
|
|
"scripts/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/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"))
|
|
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)
|
|
parser.add_argument("--tag", default="")
|
|
args = parser.parse_args()
|
|
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, tag=args.tag,
|
|
platformTag=args.platform_tag, pythonAbi=args.python_abi,
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|