#!/usr/bin/env python3 """Verify deterministic source-build equivalence without requiring Docker. Two isolated copies are created from the same source tree, compiled, subjected to a focused startup/build smoke suite, and compared using normalized manifests. Container-image equivalence remains a separate Desktop/CI check. """ from __future__ import annotations import argparse import hashlib import json import os import shutil import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKIP_NAMES = { ".git", ".pytest_cache", "__pycache__", ".coverage", "coverage.json", "coverage.xml", "node_modules", ".venv", "venv", "dist", "build", "evidence_package", } SMOKE_TESTS = ( "hermes_overlay/tests/test_build_reproducibility.py", "hermes_overlay/tests/test_config_validation.py", "hermes_overlay/tests/test_upstream_integration_preflight.py", "hermes_overlay/tests/test_repository_build_metadata.py", ) def _ignore(_directory: str, names: list[str]) -> set[str]: return {name for name in names if name in SKIP_NAMES or name.endswith((".pyc", ".pyo"))} def _sha256_lines(lines: list[str]) -> str: return hashlib.sha256(("\n".join(lines) + "\n").encode("utf-8")).hexdigest() def _manifest(root: Path) -> list[str]: lines: list[str] = [] for path in sorted(root.rglob("*")): if not path.is_file() or any(part in SKIP_NAMES for part in path.relative_to(root).parts): continue if path.suffix in {".pyc", ".pyo"}: continue digest = hashlib.sha256(path.read_bytes()).hexdigest() lines.append(f"{digest} {path.relative_to(root).as_posix()}") return lines def _run(command: list[str], cwd: Path) -> dict[str, object]: env = os.environ.copy() env["PYTHONPATH"] = str(cwd / "hermes_overlay") env["TRADING_MODE"] = "paper" env["HERMES_EXECUTION_ENABLED"] = "false" env["HERMES_NONPAPER_EXECUTION_ENABLED"] = "false" result = subprocess.run(command, cwd=cwd, env=env, text=True, capture_output=True) return { "command": command, "returncode": result.returncode, "stdout": result.stdout[-4000:], "stderr": result.stderr[-4000:], } def verify(root: Path = ROOT, *, run_smoke: bool = True) -> dict[str, object]: root = root.resolve() with tempfile.TemporaryDirectory(prefix="hermes-clean-build-") as tmp: base = Path(tmp) copies = [base / "build-a", base / "build-b"] results: list[dict[str, object]] = [] manifests: list[list[str]] = [] for copy in copies: shutil.copytree(root, copy, ignore=_ignore) commands = [ [sys.executable, "-m", "compileall", "-q", "app.py", "hermes_overlay", "scripts"], ] if run_smoke: commands.append([sys.executable, "-m", "pytest", "-q", *SMOKE_TESTS]) command_results = [_run(command, copy) for command in commands] failed = [item for item in command_results if item["returncode"] != 0] if failed: raise RuntimeError(f"clean build smoke failed in {copy.name}: {failed[0]}") manifests.append(_manifest(copy)) results.append({"copy": copy.name, "commands": command_results}) equivalent = manifests[0] == manifests[1] if not equivalent: only_a = sorted(set(manifests[0]) - set(manifests[1]))[:20] only_b = sorted(set(manifests[1]) - set(manifests[0]))[:20] raise RuntimeError(f"clean build manifests diverged: only_a={only_a}, only_b={only_b}") return { "schemaVersion": 1, "equivalent": True, "manifestEntries": len(manifests[0]), "manifestSha256": _sha256_lines(manifests[0]), "python": sys.version.split()[0], "smokeTests": list(SMOKE_TESTS) if run_smoke else [], "builds": results, "allowedNondeterminism": [ "temporary directory paths", "pytest timing output", "Python bytecode/cache files excluded from normalized manifest", "container layer timestamps verified separately in Docker-capable CI", ], } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", default=str(ROOT)) parser.add_argument("--output", default="evidence_package/clean_build_equivalence.json") parser.add_argument("--no-smoke", action="store_true") args = parser.parse_args(argv) try: result = verify(Path(args.root), run_smoke=not args.no_smoke) except (OSError, RuntimeError, subprocess.SubprocessError) as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 output = Path(args.output) if not output.is_absolute(): output = Path(args.root) / output output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"clean_build_equivalence=PASS entries={result['manifestEntries']} sha256={result['manifestSha256']}") return 0 if __name__ == "__main__": raise SystemExit(main())