refresh_support_fixture.py (6319B)
1 #!/usr/bin/env python3 2 """Pack completed native library candidates after their exact sources are installed. 3 No compilation, source mutation, header equivalence claim or recovery fallback. 4 """ 5 import argparse 6 import hashlib 7 import json 8 import os 9 from pathlib import Path 10 import signal 11 import subprocess 12 import tempfile 13 import time 14 from process_rss import ChildRSS 15 from native_library_migration import digest 16 17 REPO = Path(__file__).resolve().parent.parent 18 19 20 def main(): 21 parser = argparse.ArgumentParser(description=__doc__) 22 parser.add_argument('tricu', type=Path) 23 parser.add_argument('--completed-only', action='store_true', help='explicitly publish only completed records; retain failures in candidate evidence') 24 parser.add_argument('--replace', action='append', default=[], help='explicit candidate alias whose later completed record supersedes an earlier one') 25 parser.add_argument('directory', type=Path, nargs='+', help='completed candidate directories to combine') 26 args = parser.parse_args() 27 records = [] 28 for directory in args.directory: 29 root = directory.resolve() 30 for record in json.loads((root / 'migration.json').read_text()): 31 if args.completed_only and not record['complete']: 32 continue 33 records.append((root, record)) 34 selected = {} 35 for root, record in records: 36 alias = record['alias'] 37 if alias in selected and alias not in args.replace: 38 raise ValueError('duplicate candidate alias; explicit --replace required') 39 selected[alias] = (root, record) 40 records = list(selected.values()) 41 staging = tempfile.TemporaryDirectory(prefix='.support-cas-', dir=REPO / 'test/fixtures') 42 combined_store = Path(staging.name) 43 command = [str(args.tricu.resolve()), 'arboricx', 'export', '--store', str(combined_store)] 44 metadata = ['tricu.support-library-fixture.v1'] 45 sources, provenance = {}, [] 46 seen = set() 47 for root, record in records: 48 store = root / 'application' 49 if record['alias'] in seen: 50 raise ValueError('duplicate candidate alias') 51 seen.add(record['alias']) 52 if not record['complete']: 53 raise ValueError('incomplete candidate: ' + record['alias']) 54 source = (REPO / record['path']).read_bytes() 55 candidate = (root / 'workspace' / (record['alias'] + '.tri')).read_bytes() 56 if source != candidate or hashlib.sha256(source).hexdigest() != record['after_sha256']: 57 raise ValueError('authored source differs from compiled candidate') 58 sources[record['path']] = source 59 def object_bytes(domain, pin): 60 payload = (store / 'objects' / pin[:3] / pin).read_bytes() 61 if digest(domain, payload) != pin: 62 raise ValueError('corrupt candidate object') 63 return payload 64 manifest = object_bytes('arboricx.module-manifest.v1', record['manifest']) 65 receipt = object_bytes('arboricx.build-receipt.v1', record['receipt']) 66 receipt_rows = receipt.decode().splitlines() 67 if ('source\t' + digest('arboricx.source.v1', source) not in receipt_rows or 68 'output\t' + record['manifest'] not in receipt_rows): 69 raise ValueError('candidate receipt mismatch') 70 for entry in manifest.decode().splitlines()[1:]: 71 _, name, _, tree, _ = entry.split('\t') 72 target = combined_store / 'objects' / tree[:3] / tree 73 target.parent.mkdir(exist_ok=True, parents=True) 74 if not target.exists(): 75 os.link(store / 'objects' / tree[:3] / tree, target) 76 command += ['--target', tree, '--name', record['alias'] + '.' + name] 77 metadata.append('\t'.join(['module', record['legacy'], record['alias'], 78 record['path'], digest('arboricx.source.v1', source)])) 79 provenance.append({key: record[key] for key in 80 ['legacy', 'alias', 'manifest', 'receipt', 'after_sha256']}) 81 compiler_path = REPO / 'test/fixtures/compiler-library.arboricx' 82 compiler_bytes = compiler_path.read_bytes() 83 metadata.append('compiler-fixture\t' + digest('tricu.compiler-library-fixture.v1', compiler_bytes)) 84 metadata.append('provenance\t' + json.dumps(provenance, sort_keys=True, separators=(',', ':'))) 85 output = REPO / 'test/fixtures/support-library.arboricx' 86 with tempfile.TemporaryDirectory(prefix='.support-refresh-', dir=output.parent) as temporary: 87 candidate = Path(temporary) / output.name 88 invocation = [*command, '--output', str(candidate), '+RTS', '-N1', '-M512m', '-RTS'] 89 started, peak = time.monotonic(), 0 90 proc = subprocess.Popen(invocation, start_new_session=True) 91 try: 92 monitor = ChildRSS(proc) 93 while proc.poll() is None: 94 rss = monitor.sample() 95 if rss is not None: 96 peak = max(peak, rss) 97 if peak > 1024 * 1024 or time.monotonic() - started > 600: 98 raise RuntimeError('support fixture packing guard') 99 time.sleep(0.05) 100 if proc.returncode: 101 raise RuntimeError('support fixture exporter failed') 102 finally: 103 if proc.poll() is None: 104 os.killpg(proc.pid, signal.SIGTERM) 105 try: 106 proc.wait(timeout=5) 107 except subprocess.TimeoutExpired: 108 os.killpg(proc.pid, signal.SIGKILL); proc.wait() 109 for path, payload in sources.items(): 110 if (REPO / path).read_bytes() != payload: 111 raise ValueError('source changed during packing') 112 if compiler_path.read_bytes() != compiler_bytes: 113 raise ValueError('compiler fixture changed during packing') 114 metadata.append('bundle\t' + digest('tricu.support-library-fixture.v1', candidate.read_bytes())) 115 candidate_metadata = Path(temporary) / 'support-library.v1' 116 candidate_metadata.write_text('\n'.join(metadata) + '\n') 117 os.replace(candidate, output) 118 os.replace(candidate_metadata, output.with_suffix('.v1')) 119 print(f'Packing elapsed={time.monotonic()-started:.2f}s peak-RSS={peak} KiB') 120 staging.cleanup() 121 122 123 if __name__ == '__main__': 124 main()