tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

bootstrap_acceptance.py (6234B)


      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 from generation_io import version, versions, produced_adapter, check_pair, execution_flags, check_invocation
     14 
     15 REPO = Path(__file__).resolve().parent.parent
     16 ARTIFACTS = ('rebuilt-compiler.arboricx', 'build.arboricx', 'build-stable.arboricx')
     17 
     18 
     19 def sha(path):
     20     return hashlib.sha256(path.read_bytes()).hexdigest()
     21 
     22 
     23 def verify_generation(run, runtime, source_root=None, execution_host='haskell'):
     24     """Authenticate and independently audit one full fresh compiler generation.
     25 
     26     Execution host is caller-selected, never inferred from untrusted provenance.
     27     Pair acceptance below remains explicitly Haskell-only.
     28     """
     29     status = json.loads((run / 'status.json').read_text())
     30     inputs = json.loads((run / 'inputs.json').read_text())
     31     if not status.get('complete') or status.get('backend') != 'stable':
     32         raise ValueError('completed stable generations required')
     33     if not inputs.get('fresh_store') or not status.get('fresh_store'):
     34         raise ValueError('fresh-store execution evidence required; seeded checkpoints are not reproduction')
     35     if any(status.get(key) != value for key, value in inputs.items()):
     36         raise ValueError('status/input provenance mismatch')
     37     if inputs.get('execution_host', 'haskell') != execution_host:
     38         raise ValueError('execution host mismatch')
     39     execution_io, _ = versions(inputs)
     40     execution_flags(execution_io, execution_host)
     41     if 'produced_io_version' not in inputs and (run / 'produced-io-adapter.arboricx').exists():
     42         raise ValueError('missing generation IO metadata')
     43     if 'produced_io_version' in inputs:
     44         produced_adapter(run, inputs)
     45         invocation = json.loads((run / 'invocation.json').read_text())
     46         check_invocation(inputs, invocation, execution_host)
     47     if execution_host == 'haskell-ffi':
     48         request = json.loads((run / 'execution-request.json').read_text())
     49         if version(request.get('execution_io_version', 1)) != execution_io:
     50             raise ValueError('execution request IO version mismatch')
     51         for key in ('worker_export','compiler_bundle_sha256','adapter_sha256'):
     52             if request.get(key) != inputs[key]:
     53                 raise ValueError('execution request identity mismatch: '+key)
     54     root = source_root or Path(inputs['source_root'])
     55     text, entries, _ = source_map(run / 'source-map.v1', root, 'source_map')
     56     actual = snapshot(text, entries, root)
     57     if inputs.get('root') != 'source_map' or any(inputs.get(k) != v for k, v in actual.items()):
     58         raise ValueError('frozen source/map fingerprint mismatch')
     59     if sha(runtime) != inputs['runtime_sha256']:
     60         raise ValueError('runtime identity mismatch')
     61     if sha(run / 'compiler.arboricx') != inputs['compiler_bundle_sha256']:
     62         raise ValueError('compiler bundle identity mismatch')
     63     if sha(run / 'io-adapter.arboricx') != inputs['adapter_sha256']:
     64         raise ValueError('adapter identity mismatch')
     65     for name in ARTIFACTS:
     66         if sha(run / name) != status['artifacts'][name]['sha256']:
     67             raise ValueError('packaged artifact identity mismatch: ' + name)
     68     command = [sys.executable, str(REPO / 'test/bootstrap_store_check.py'), str(run)]
     69     if source_root:
     70         command += ['--source-root', str(source_root)]
     71     audit, _ = run_guarded(command, memory_mib=8192, seconds=600)
     72     return inputs, status, audit
     73 
     74 
     75 def verify(first, second, tricu, source_root=None):
     76     first, second = first.resolve(), second.resolve()
     77     if first == second:
     78         raise ValueError('two independent generation directories required')
     79     records = []
     80     audits = []
     81     for run in (first, second):
     82         inputs, _, audit = verify_generation(run, tricu, source_root)
     83         audits.append(audit)
     84         records.append(inputs)
     85     for key in ('sources', 'map_sha256', 'backend', 'root', 'runtime_sha256'):
     86         if records[0][key] != records[1][key]:
     87             raise ValueError('generation inputs differ: ' + key)
     88     check_pair(records[0], records[1])
     89     if sha(first / 'rebuilt-compiler.arboricx') != records[1]['compiler_bundle_sha256']:
     90         raise ValueError('second generation did not execute the first rebuilt compiler')
     91     for name in ARTIFACTS:
     92         if sha(first / name) != sha(second / name):
     93             raise ValueError('artifact reproduction failure: ' + name)
     94     # Check actual manifests, not the runner's recorded comparison booleans.
     95     for alias in records[0]['sources']:
     96         a = (first / 'cas/cache/source-build' / alias).read_text().split('\t')[2]
     97         b = (second / 'cas/cache/source-build' / alias).read_text().split('\t')[2]
     98         if a != b:
     99             raise ValueError('manifest reproduction failure: ' + alias)
    100     return dict(accepted=True, backend='stable', generations=[str(first), str(second)],
    101                 inputs=records, audits=audits,
    102                 artifacts={name: sha(second / name) for name in ARTIFACTS})
    103 
    104 
    105 def main():
    106     parser = argparse.ArgumentParser(description=__doc__)
    107     parser.add_argument('first', type=Path)
    108     parser.add_argument('second', type=Path)
    109     parser.add_argument('--tricu', type=Path, required=True)
    110     parser.add_argument('--source-root', type=Path, help='explicit relocated frozen source for restore validation')
    111     parser.add_argument('--output', type=Path, required=True, help='new acceptance evidence file; never overwritten')
    112     args = parser.parse_args()
    113     if args.output.exists():
    114         parser.error('acceptance evidence already exists; do not overwrite')
    115     evidence = verify(args.first, args.second, args.tricu.resolve(), args.source_root)
    116     args.output.write_text(json.dumps(evidence, indent=2) + '\n')
    117     print('Accepted independently audited fresh-store stable reproduction: ' + str(args.output))
    118 
    119 
    120 if __name__ == '__main__':
    121     main()