#!/usr/bin/env python3 """Create a deterministic SHA-256 manifest for release files.""" from __future__ import annotations import argparse import hashlib import os from pathlib import Path EXCLUDED_PARTS = {".git", "__pycache__", ".pytest_cache"} def included(path: Path, root: Path) -> bool: relative = path.relative_to(root) return ( path.is_file() and path.name != "MANIFEST.sha256" and not path.name.endswith(".partial") and not any(part in EXCLUDED_PARTS for part in relative.parts) ) def digest(path: Path, chunk_size: int = 64 * 1024 * 1024) -> str: value = hashlib.sha256() with path.open("rb") as stream: while block := stream.read(chunk_size): value.update(block) return value.hexdigest() def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", required=True, type=Path) args = parser.parse_args() root = args.root.resolve() files = sorted(path for path in root.rglob("*") if included(path, root)) temporary = root / ".MANIFEST.sha256.partial" with temporary.open("w", encoding="utf-8") as stream: for index, path in enumerate(files, start=1): relative = path.relative_to(root).as_posix() stream.write(f"{digest(path)} {relative}\n") print(f"[manifest] {index:,}/{len(files):,} {relative}", flush=True) stream.flush() os.fsync(stream.fileno()) os.replace(temporary, root / "MANIFEST.sha256") if __name__ == "__main__": main()