project_support_fixture.py (7701B)
1 #!/usr/bin/env python3 2 """Project an authenticated mixed support cohort; never compile or change pins. 3 4 Explicit retained aliases must form a complete authored dependency closure with 5 compiler-library.v1. Output is a NEW directory. Original evidence is read-only. 6 Exact constructor/child-ID comparisons check every selected export independently 7 of the packer's interning table; unreachable nodes are excluded. The ordinary 8 Haskell gate still authenticates receipts and proves retained-source reuse. 9 """ 10 import argparse 11 import hashlib 12 import json 13 from pathlib import Path 14 import shutil 15 16 from bundle_dag import read, merge_bundles, Intern 17 from native_header import native_header 18 from native_library_migration import digest 19 20 21 def sha(path): 22 return hashlib.sha256(path.read_bytes()).hexdigest() 23 24 25 def rows(path): 26 return [line.split('\t') for line in path.read_text().splitlines()] 27 28 29 def verify_projection(original, projected, aliases): 30 """Full selected export sets and exact tree equality, not tree-hash equality.""" 31 old, new = read(original), read(projected) 32 selected = {k: v for k, v in old[3].items() if k.split('.', 1)[0] in aliases} 33 if not selected or set(selected) != set(new[3]): 34 raise ValueError('projection export set mismatch') 35 intern = Intern() 36 before, after = intern.graph(old[2]), intern.graph(new[2]) 37 if any(before[root] != after[new[3][name]] for name, root in selected.items()): 38 raise ValueError('projection changed a kernel') 39 reachable, todo = set(), list(new[3].values()) 40 while todo: 41 root = todo.pop() 42 if root not in reachable: 43 reachable.add(root) 44 todo.extend(new[2][root][1:]) 45 if len(reachable) != len(new[2]): 46 raise ValueError('projection retained unreachable nodes') 47 return len(selected) 48 49 50 def project(original, compiler, source_root, aliases, output): 51 metadata = rows(original / 'support-library.v1') 52 origin = rows(original / 'support-origin.v1') 53 if metadata[0] != ['tricu.support-library-fixture.v2']: 54 raise ValueError('expected authenticated mixed-origin fixture') 55 domains = { 56 'bundle': ('tricu.support-library-fixture.v1', 'support-library.arboricx'), 57 'origin-support': ('tricu.support-library-fixture.v1', 'support-origin.arboricx'), 58 'origin-compiler': ('tricu.compiler-library-fixture.v1', 'support-origin-compiler.arboricx'), 59 'origin-metadata': ('tricu.support-origin-metadata.v1', 'support-origin.v1'), 60 'rebuilt-support': ('tricu.support-library-fixture.v1', 'support-rebuilt.arboricx'), 61 } 62 for label, (domain, name) in domains.items(): 63 if [label, digest(domain, (original / name).read_bytes())] not in metadata: 64 raise ValueError('unauthenticated original: ' + name) 65 if ['compiler-fixture', digest('tricu.compiler-library-fixture.v1', 66 (compiler / 'compiler-library.arboricx').read_bytes())] not in metadata: 67 raise ValueError('current compiler pin mismatch') 68 all_modules = [row for row in metadata if row[0] == 'module'] 69 if all_modules != [row for row in origin if row[0] == 'module']: 70 raise ValueError('original source cohort mismatch') 71 modules = [row for row in all_modules if row[2] in aliases] 72 if len(modules) != len(aliases): 73 raise ValueError('unknown or duplicate retained alias') 74 providers = aliases | {row[2] for row in rows(compiler / 'compiler-library.v1') if row[0] == 'module'} 75 for _, _, alias, path, pin in modules: 76 source = (source_root / path).read_bytes() 77 if digest('arboricx.source.v1', source) != pin: 78 raise ValueError('changed source: ' + path) 79 dependencies, _ = native_header(source.decode()) 80 if not set(dependencies) <= providers: 81 raise ValueError('projection is not dependency-closed: ' + alias) 82 output.mkdir(parents=True, exist_ok=False) 83 evidence = {'format': 'tricu.support-projection.v1', 'aliases': sorted(aliases), 84 'inputs_sha256': {}, 'outputs_sha256': {}, 'exact_exports': {}} 85 for name in ['support-library.v1', *[value[1] for value in domains.values()]]: 86 evidence['inputs_sha256'][name] = sha(original / name) 87 for name in ['support-library.arboricx', 'support-origin.arboricx', 'support-rebuilt.arboricx']: 88 names = [(k, k) for k in read(original / name)[3] if k.split('.', 1)[0] in aliases] 89 merge_bundles(output / name, [(original / name, names)], prune_unreachable=True) 90 evidence['exact_exports'][name] = verify_projection(original / name, output / name, aliases) 91 shutil.copyfile(original / 'support-origin-compiler.arboricx', output / 'support-origin-compiler.arboricx') 92 # Project source/provenance rows, retaining the original per-module records 93 # verbatim in meaning. The full immutable snapshots stay in the archive. 94 projected_origin = [origin[0]] + modules 95 for row in origin[1:]: 96 if row[0] == 'compiler-fixture': 97 projected_origin.append(row) 98 elif row[0] == 'provenance': 99 records = [r for r in json.loads(row[1]) if r['alias'] in aliases] 100 projected_origin.append(['provenance', json.dumps(records, sort_keys=True, separators=(',', ':'))]) 101 projected_origin.append(['bundle', digest('tricu.support-library-fixture.v1', 102 (output / 'support-origin.arboricx').read_bytes())]) 103 (output / 'support-origin.v1').write_text(''.join('\t'.join(r) + '\n' for r in projected_origin)) 104 selected = [metadata[0]] + modules + [r for r in metadata if r[0] == 'compiler-fixture'] 105 for label, (domain, name) in domains.items(): 106 selected.append([label, digest(domain, (output / name).read_bytes())]) 107 for row in metadata: 108 if row[0] in ('reuse', 'rebuilt') and row[1] in aliases: 109 selected.append(row) 110 if row[0] == 'rebuilt': 111 for suffix, domain, pin in [('receipt', 'arboricx.build-receipt.v1', row[2]), 112 ('manifest', 'arboricx.module-manifest.v1', row[3])]: 113 relative = Path('support-receipts') / (row[1] + '.' + suffix) 114 if digest(domain, (original / relative).read_bytes()) != pin: 115 raise ValueError('unauthenticated receipt/manifest') 116 (output / relative).parent.mkdir(exist_ok=True) 117 shutil.copyfile(original / relative, output / relative) 118 evidence['inputs_sha256'][str(relative)] = sha(original / relative) 119 (output / 'support-library.v1').write_text(''.join('\t'.join(r) + '\n' for r in selected)) 120 for path in sorted(output.rglob('*')): 121 if path.is_file(): 122 evidence['outputs_sha256'][str(path.relative_to(output))] = sha(path) 123 (output / 'support-projection.json').write_text(json.dumps(evidence, indent=2, sort_keys=True) + '\n') 124 return evidence 125 126 127 def main(): 128 parser = argparse.ArgumentParser(description=__doc__) 129 parser.add_argument('--original', type=Path, required=True) 130 parser.add_argument('--compiler', type=Path, required=True) 131 parser.add_argument('--source-root', type=Path, required=True) 132 parser.add_argument('--aliases', type=Path, required=True, help='one retained alias per line') 133 parser.add_argument('--output', type=Path, required=True) 134 args = parser.parse_args() 135 evidence = project(args.original, args.compiler, args.source_root, 136 set(args.aliases.read_text().splitlines()), args.output) 137 print(json.dumps(evidence['exact_exports'], sort_keys=True)) 138 139 140 if __name__ == '__main__': 141 main()