#!/usr/bin/env python3 """Validate and create a deterministic DevRunbook build-pack ZIP.""" from __future__ import annotations import argparse import hashlib import os import subprocess import sys import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_OUTPUT = ROOT.parent / "DevRunbook_Autonomous_Build_Pack_v1_2.zip" FIXED_TIMESTAMP = (2026, 7, 27, 0, 0, 0) GENERATED = {"FILE_INDEX.txt", "PACK_MANIFEST.sha256"} def regular_files() -> list[Path]: files: list[Path] = [] for path in ROOT.rglob("*"): if path.is_symlink(): raise RuntimeError(f"Symlinks are not allowed in the build pack: {path.relative_to(ROOT)}") if path.is_file(): files.append(path) return sorted(files, key=lambda p: p.relative_to(ROOT).as_posix()) def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def run_validator() -> None: subprocess.run([sys.executable, str(ROOT / "scripts/validate_pack.py")], cwd=ROOT, check=True) def generate_integrity_files() -> None: for name in GENERATED: (ROOT / name).unlink(missing_ok=True) planned = [p.relative_to(ROOT).as_posix() for p in regular_files()] planned.extend(sorted(GENERATED)) planned = sorted(set(planned)) (ROOT / "FILE_INDEX.txt").write_text("\n".join(planned) + "\n", encoding="utf-8", newline="\n") manifest_paths = [p for p in regular_files() if p.name != "PACK_MANIFEST.sha256"] lines = [f"{sha256(path)} {path.relative_to(ROOT).as_posix()}" for path in manifest_paths] (ROOT / "PACK_MANIFEST.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") def write_zip(output: Path) -> None: output.parent.mkdir(parents=True, exist_ok=True) temp = output.with_suffix(output.suffix + ".tmp") temp.unlink(missing_ok=True) prefix = ROOT.name with zipfile.ZipFile(temp, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=True) as archive: for path in regular_files(): relative = path.relative_to(ROOT).as_posix() info = zipfile.ZipInfo(f"{prefix}/{relative}", FIXED_TIMESTAMP) info.create_system = 3 info.flag_bits |= 0x800 mode = 0o755 if path.parent.name == "scripts" and path.suffix == ".py" else 0o644 info.external_attr = (mode & 0xFFFF) << 16 info.compress_type = zipfile.ZIP_DEFLATED archive.writestr(info, path.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) os.replace(temp, output) def file_digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() output = args.output.resolve() if ROOT in output.parents: raise SystemExit("Output ZIP must be outside the build-pack directory") run_validator() generate_integrity_files() run_validator() write_zip(output) subprocess.run([sys.executable, str(ROOT / "scripts/verify_archive.py"), str(output)], check=True) print(f"Archive: {output}") print(f"SHA-256: {file_digest(output)}") return 0 if __name__ == "__main__": raise SystemExit(main())