bootstrap_generation_cli.py (7745B)
1 #!/usr/bin/env python3 2 """Opt-in bounded generation checks using retained compiler and adapter artifacts. 3 No bootstrap compilation and no full compiler generation. 4 """ 5 import argparse 6 import json 7 from pathlib import Path 8 import subprocess 9 import sys 10 import tempfile 11 from generation_backend import BACKENDS, DEFAULT_BACKEND 12 13 REPO = Path(__file__).resolve().parent.parent 14 15 16 def main(): 17 parser = argparse.ArgumentParser(description=__doc__) 18 parser.add_argument('previous', type=Path) 19 parser.add_argument('--tricu', type=Path, default=REPO / 'result/bin/tricu') 20 parser.add_argument('--directory', type=Path) 21 parser.add_argument('--backend', choices=BACKENDS, default=DEFAULT_BACKEND, 22 help='generation lowering backend (default: %(default)s)') 23 parser.add_argument('--worker-bundle', type=Path) 24 parser.add_argument('--adapter', type=Path, help='explicit standard adapter for transition probes') 25 args = parser.parse_args() 26 directory = args.directory or Path(tempfile.mkdtemp(prefix='tricu-generation-check-')) 27 directory = directory.resolve() 28 source = directory / 'source' 29 source.mkdir(parents=True, exist_ok=True) 30 map_file = directory / 'map.v1' 31 map_file.write_text('arboricx.source-map.v1\n' 32 'unit\tdep\tprobe.dep\tdep.tri\t-\n' 33 'unit\tapp\tprobe.app\tapp.tri\tdep\n') 34 (source / 'dep.tri').write_text('answer = 41\n') 35 (source / 'app.tri').write_text('!import "probe.dep" !Local\nmain = answer\n') 36 output = directory / 'run' 37 # Transport allowlist proves setup never calls the recovery frontend. 38 transport_log = directory / 'transport.jsonl' 39 transport_log.write_text('') 40 transport = directory / 'artifact-only-tricu' 41 transport.write_text('#!' + sys.executable + '\nimport json, os, sys\n' 42 'assert sys.argv[1] == "arboricx" and sys.argv[2] in ("export", "import", "run"), sys.argv\n' 43 f'with open({str(transport_log)!r}, "a") as log: log.write(json.dumps(sys.argv[1:]) + "\\n")\n' 44 f'os.execv({str(args.tricu.resolve())!r}, [{str(args.tricu.resolve())!r}, *sys.argv[1:]])\n') 45 transport.chmod(0o755) 46 command = [sys.executable, str(REPO / 'test/bootstrap_generations.py'), 47 str(args.previous.resolve()), str(output), '--tricu', str(transport), 48 '--map', str(map_file), '--source-root', str(source), '--root', 'app', '--backend', args.backend] 49 if args.worker_bundle: 50 command += ['--worker-bundle', str(args.worker_bundle.resolve())] 51 if args.adapter: 52 command += ['--adapter', str(args.adapter.resolve())] 53 def run(extra=(), success=True): 54 reply = subprocess.run([*command, *extra], cwd=REPO, capture_output=True, text=True, timeout=120) 55 assert (reply.returncode == 0) == success, (reply.stdout, reply.stderr) 56 return reply 57 def checkpoints(): 58 return {name: (output / 'cas/cache/source-build' / name).read_bytes() for name in ('dep', 'app')} 59 def manifest(name): 60 pin = (output / 'cas/cache/source-build' / name).read_text().split('\t')[2] 61 return pin, (output / 'cas/objects' / pin[:3] / pin).read_text() 62 def result(number): 63 pin, _ = manifest('app') 64 reply = subprocess.run([str(args.tricu.resolve()), 'arboricx', 'run', '--store', str(output / 'cas'), 65 '--module', pin, '--name', 'main', '--format', 'number'], capture_output=True, text=True, check=True) 66 assert reply.stdout.strip() == str(number), reply.stdout 67 run() 68 assert json.loads((output / 'status.json').read_text())['complete'] 69 result(41) 70 cold = checkpoints() 71 run() 72 assert checkpoints() == cold 73 print('cold publication and verified warm reuse passed', flush=True) 74 # Unrelated stale checkpoints must not establish closure completeness. 75 cache = output / 'cas/cache/source-build' 76 (cache / 'unrelated').write_text('unrelated stale metadata') 77 (source / 'dep.tri').write_text('answer = 42\n') 78 run() 79 changed = checkpoints() 80 assert all(changed[name] != cold[name] for name in cold) 81 result(42) 82 print('changed dependency invalidates both modules; old map receipts tolerated', flush=True) 83 # Remove the root checkpoint, retaining the completed dependency/object CAS. 84 (cache / 'app').unlink() 85 run(('--memory-mib', '1'), success=False) 86 assert not json.loads((output / 'status.json').read_text())['complete'] 87 assert (cache / 'dep').read_bytes() == changed['dep'] 88 run() 89 assert checkpoints() == changed 90 result(42) 91 print('guarded interruption and restart passed', flush=True) 92 # Matching local cache corruption must fail closed, not silently compile. 93 original = (cache / 'app').read_bytes() 94 (cache / 'app').write_text('corrupt') 95 run(success=False) 96 assert not json.loads((output / 'status.json').read_text())['complete'] 97 (cache / 'app').write_bytes(original) 98 run() 99 print('matching checkpoint corruption rejected; restored restart passed', flush=True) 100 # No implicit source compilation when adapter is unavailable. 101 reply = run(('--adapter', str(directory / 'missing.arboricx')), success=False) 102 assert 'precompiled adapter required' in reply.stderr 103 # Header/map mismatch is caught before any subprocess/artifact preparation. 104 (source / 'app.tri').write_text('main = 42\n') 105 reply = run(('--validate-only',), success=False) 106 assert 'header mismatch' in reply.stderr 107 print('missing adapter and stale header rejected without bootstrap fallback', flush=True) 108 # Both profiles reuse the same source/CAS directory, but never each other's 109 # receipt key. Literal executable trees may coincide; keys must not. 110 (source / 'app.tri').write_text('!import "probe.dep" !Local\nmain = answer\n') 111 run(('--backend', 'optimized')) 112 optimized = checkpoints() 113 run(('--backend', 'stable')) 114 stable = checkpoints() 115 assert all(stable[name] != optimized[name] for name in stable) 116 status = json.loads((output / 'status.json').read_text()) 117 assert status['backend'] == 'stable' and status['worker_export'] == 'sourceMapBuildStable' 118 result(42) 119 run(('--backend', 'stable')) 120 assert checkpoints() == stable 121 # Lie about the selected profile in both provenance records: independently 122 # checked module options still expose this (not just metadata consistency). 123 saved = {} 124 for filename in ('status.json', 'inputs.json'): 125 path = output / filename 126 saved[filename] = path.read_bytes() 127 data = json.loads(saved[filename]) 128 data.update(backend='optimized', worker_export=BACKENDS['optimized'][0], 129 backend_options=BACKENDS['optimized'][1]) 130 path.write_text(json.dumps(data)) 131 audit = subprocess.run([sys.executable, str(REPO / 'test/bootstrap_store_check.py'), str(output)], 132 capture_output=True, text=True) 133 assert audit.returncode != 0 134 for filename, payload in saved.items(): 135 (output / filename).write_bytes(payload) 136 (source / 'app.tri').write_text('!import "probe.dep" !Local\n' 137 'walk t = answer\nwalk (t x) = walk x\nwalk input = 9\nmain = walk (t (t t))\n') 138 run(('--backend', 'stable')) 139 result(42) 140 reply = run(('--backend', 'optimized'), success=False) 141 assert not json.loads((output / 'status.json').read_text())['complete'] 142 run(('--backend', 'stable')) 143 result(42) 144 print('backend switch, stable warm reuse, false-profile audit, direct recursion and no fallback passed', flush=True) 145 assert transport_log.read_text() 146 print('artifact-only transport allowlist passed (export/import/run only)', flush=True) 147 print('Artifacts: ' + str(directory), flush=True) 148 149 150 if __name__ == '__main__': 151 main()