This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user