Spaces:
Sleeping
Sleeping
File size: 2,962 Bytes
2e658e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | #!/usr/bin/env python3
"""P1-T06: Generate a normalized source/build manifest for clean-build equivalence.
Produces a deterministic SHA-256 inventory of source files (excluding caches,
secrets, and generated artifacts). Two clean checkouts of the same revision
should produce identical manifests for all tracked source paths.
Usage:
python scripts/clean_build_manifest.py [project_root] > build_manifest.sha256
python scripts/clean_build_manifest.py --compare a.sha256 b.sha256
"""
from __future__ import annotations
import argparse
import hashlib
import os
import sys
from pathlib import Path
SKIP_DIR_NAMES = {
".git", "__pycache__", ".pytest_cache", "node_modules", ".venv", "venv",
"Temp", ".runtime_audit", "dist", "build",
}
SKIP_SUFFIXES = {".pyc", ".pyo", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".zip", ".tar", ".gz", ".log"}
SKIP_FILES = {".env", "paper_positions.json"}
def should_skip(path: Path, root: Path) -> bool:
rel = path.relative_to(root)
parts = set(rel.parts)
if parts & SKIP_DIR_NAMES:
return True
if path.suffix.lower() in SKIP_SUFFIXES:
return True
if path.name in SKIP_FILES or path.name.startswith(".env."):
return True
return False
def file_sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def generate(root: Path) -> list[str]:
lines = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIR_NAMES)
for fn in sorted(filenames):
p = Path(dirpath) / fn
if should_skip(p, root):
continue
rel = p.relative_to(root).as_posix()
try:
digest = file_sha256(p)
except OSError:
continue
lines.append(f"{digest} {rel}")
return lines
def compare(a: Path, b: Path) -> int:
la = a.read_text(encoding="utf-8").splitlines()
lb = b.read_text(encoding="utf-8").splitlines()
sa, sb = set(la), set(lb)
only_a = sorted(sa - sb)
only_b = sorted(sb - sa)
if not only_a and not only_b and len(la) == len(lb):
print(f"EQUIVALENT: {len(la)} entries match")
return 0
print(f"DIVERGED: only_in_a={len(only_a)} only_in_b={len(only_b)}")
for x in only_a[:20]:
print(f" - {x}")
for x in only_b[:20]:
print(f" + {x}")
return 1
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("root", nargs="?", default=".")
ap.add_argument("--compare", nargs=2, metavar=("A", "B"))
args = ap.parse_args()
if args.compare:
return compare(Path(args.compare[0]), Path(args.compare[1]))
root = Path(args.root).resolve()
for line in generate(root):
print(line)
return 0
if __name__ == "__main__":
sys.exit(main())
|