97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
#!/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())
|