Spaces:
Sleeping
Sleeping
File size: 5,277 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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | #!/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())
|