tricu

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

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