bootstrap_acceptance.py (5183B)
1 #!/usr/bin/env python3 2 """Strict opt-in stable bootstrap acceptance, independent of cached fixture checks. 3 Run only after two successful fresh-store generations. This audits existing 4 outputs; it does not compile source. Failed verification never accepts a baseline. 5 """ 6 import argparse 7 import hashlib 8 import json 9 from pathlib import Path 10 from process_rss import run_guarded 11 import sys 12 from bootstrap_generations import source_map, snapshot 13 14 REPO = Path(__file__).resolve().parent.parent 15 ARTIFACTS = ('rebuilt-compiler.arboricx', 'build.arboricx', 'build-stable.arboricx') 16 17 18 def sha(path): 19 return hashlib.sha256(path.read_bytes()).hexdigest() 20 21 22 def verify_generation(run, runtime, source_root=None, execution_host='haskell'): 23 """Authenticate and independently audit one full fresh compiler generation. 24 25 Execution host is caller-selected, never inferred from untrusted provenance. 26 Pair acceptance below remains explicitly Haskell-only. 27 """ 28 status = json.loads((run / 'status.json').read_text()) 29 inputs = json.loads((run / 'inputs.json').read_text()) 30 if not status.get('complete') or status.get('backend') != 'stable': 31 raise ValueError('completed stable generations required') 32 if not inputs.get('fresh_store') or not status.get('fresh_store'): 33 raise ValueError('fresh-store execution evidence required; seeded checkpoints are not reproduction') 34 if any(status.get(key) != value for key, value in inputs.items()): 35 raise ValueError('status/input provenance mismatch') 36 if inputs.get('execution_host', 'haskell') != execution_host: 37 raise ValueError('execution host mismatch') 38 root = source_root or Path(inputs['source_root']) 39 text, entries, _ = source_map(run / 'source-map.v1', root, 'source_map') 40 actual = snapshot(text, entries, root) 41 if inputs.get('root') != 'source_map' or any(inputs.get(k) != v for k, v in actual.items()): 42 raise ValueError('frozen source/map fingerprint mismatch') 43 if sha(runtime) != inputs['runtime_sha256']: 44 raise ValueError('runtime identity mismatch') 45 if sha(run / 'compiler.arboricx') != inputs['compiler_bundle_sha256']: 46 raise ValueError('compiler bundle identity mismatch') 47 if sha(run / 'io-adapter.arboricx') != inputs['adapter_sha256']: 48 raise ValueError('adapter identity mismatch') 49 for name in ARTIFACTS: 50 if sha(run / name) != status['artifacts'][name]['sha256']: 51 raise ValueError('packaged artifact identity mismatch: ' + name) 52 command = [sys.executable, str(REPO / 'test/bootstrap_store_check.py'), str(run)] 53 if source_root: 54 command += ['--source-root', str(source_root)] 55 audit, _ = run_guarded(command, memory_mib=8192, seconds=600) 56 return inputs, status, audit 57 58 59 def verify(first, second, tricu, source_root=None): 60 first, second = first.resolve(), second.resolve() 61 if first == second: 62 raise ValueError('two independent generation directories required') 63 records = [] 64 audits = [] 65 for run in (first, second): 66 inputs, _, audit = verify_generation(run, tricu, source_root) 67 audits.append(audit) 68 records.append(inputs) 69 for key in ('sources', 'map_sha256', 'backend', 'root', 'runtime_sha256', 'adapter_sha256'): 70 if records[0][key] != records[1][key]: 71 raise ValueError('generation inputs differ: ' + key) 72 if sha(first / 'rebuilt-compiler.arboricx') != records[1]['compiler_bundle_sha256']: 73 raise ValueError('second generation did not execute the first rebuilt compiler') 74 for name in ARTIFACTS: 75 if sha(first / name) != sha(second / name): 76 raise ValueError('artifact reproduction failure: ' + name) 77 # Check actual manifests, not the runner's recorded comparison booleans. 78 for alias in records[0]['sources']: 79 a = (first / 'cas/cache/source-build' / alias).read_text().split('\t')[2] 80 b = (second / 'cas/cache/source-build' / alias).read_text().split('\t')[2] 81 if a != b: 82 raise ValueError('manifest reproduction failure: ' + alias) 83 return dict(accepted=True, backend='stable', generations=[str(first), str(second)], 84 inputs=records, audits=audits, 85 artifacts={name: sha(second / name) for name in ARTIFACTS}) 86 87 88 def main(): 89 parser = argparse.ArgumentParser(description=__doc__) 90 parser.add_argument('first', type=Path) 91 parser.add_argument('second', type=Path) 92 parser.add_argument('--tricu', type=Path, required=True) 93 parser.add_argument('--source-root', type=Path, help='explicit relocated frozen source for restore validation') 94 parser.add_argument('--output', type=Path, required=True, help='new acceptance evidence file; never overwritten') 95 args = parser.parse_args() 96 if args.output.exists(): 97 parser.error('acceptance evidence already exists; do not overwrite') 98 evidence = verify(args.first, args.second, args.tricu.resolve(), args.source_root) 99 args.output.write_text(json.dumps(evidence, indent=2) + '\n') 100 print('Accepted independently audited fresh-store stable reproduction: ' + str(args.output)) 101 102 103 if __name__ == '__main__': 104 main()