tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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()