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())