tricu

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

prepare-index.py (6295B)


      1 #!/usr/bin/env python3
      2 """Publish the site's exact releases and prebaked demo environment via Haskell.
      3 
      4 Run through a Nix-built tricu. Does not change existing demo/compiler bundles.
      5 Rerun after prepare.py or an explicit compiler update; inspect resulting pins.
      6 """
      7 import argparse
      8 import json
      9 from pathlib import Path
     10 import re
     11 import subprocess
     12 import tempfile
     13 from prepare import HERE, ROOT, digest, reference_compiler, pair, string, of_string
     14 
     15 
     16 def main():
     17     parser = argparse.ArgumentParser(description=__doc__)
     18     parser.add_argument('tricu', type=Path)
     19     parser.add_argument('--version', action='append', default=[], metavar='NAME=VERSION', help='Explicit new release label for a changed demo')
     20     args = parser.parse_args()
     21     versions = dict(value.split('=', 1) for value in args.version)
     22     host = args.tricu.resolve()
     23     catalogue = json.loads((HERE / 'catalogue.json').read_text())
     24     compiler = json.loads((HERE / 'compiler.json').read_text())
     25     with tempfile.TemporaryDirectory(prefix='site-index-') as tmp:
     26         tmp = Path(tmp)
     27         store = tmp / 'store'
     28 
     29         def run(*words):
     30             reply = subprocess.run([str(host), *map(str, words), '+RTS', '-N1', '-M8192m', '-RTS'],
     31                                    cwd=ROOT, capture_output=True, timeout=180)
     32             if reply.returncode:
     33                 raise RuntimeError((reply.stdout + reply.stderr).decode())
     34             return reply.stdout.decode().strip()
     35 
     36         output = run('arboricx', 'import', '--store', store, '--file', reference_compiler())
     37         manifest = re.search(r'Created module alias .* -> ([0-9a-f]{64})', output)[1]
     38         names = ['isNat?', 'lte_?', 'add', 'and?', 'not?', 'equal?', 'or?', 'contains?']
     39         base = {n: run('arboricx', 'run', '--store', store, '--module', manifest,
     40                        '--name', 'base.' + n, '--format', 'ternary') for n in names}
     41         environment = tmp / 'environment.tree'
     42         environment.write_bytes(bytes(map(int, ''.join('22' + of_string(n) + tree for n, tree in base.items()) + '0')))
     43         source = (HERE / 'demos/practical-support.tri').read_bytes()
     44         reply = run('arboricx', 'run', '--store', store, '--module', manifest,
     45                     '--name', compiler['export'], '--arg-tree-file', environment, '--arg', source.decode(), '--format', 'ternary')
     46         success, payload = pair(reply)
     47         assert success == '10', reply[:200]
     48         definitions, _ = pair(payload)
     49         bindings = dict(base)
     50         while definitions != '0':
     51             binding, definitions = pair(definitions)
     52             name, tree = pair(binding)
     53             bindings[string(name)] = tree
     54 
     55         def put(domain, data):
     56             pin = digest(domain.encode() + b'\0' + data)
     57             path = store / 'objects' / pin[:3] / pin
     58             path.parent.mkdir(parents=True, exist_ok=True)
     59             path.write_bytes(data)
     60             return pin
     61 
     62         rows = []
     63         for name, tree in bindings.items():
     64             pin = put('arboricx.tree-term.v1', bytes(map(int, tree)))
     65             rows.append(f'export\t{name}\tarboricx.tree-term.v1\t{pin}\tarboricx.abi.tree.v1\n')
     66         pin = put('arboricx.module-manifest.v1', ('arboricx.module-manifest.v1\n' + ''.join(rows)).encode())
     67         file = tmp / 'environment.arboricx'
     68         run('arboricx', 'export', '--store', store, '--module', pin, '--output', file)
     69         data = file.read_bytes()
     70         assert len(data) <= 16_777_216
     71         support_hash = digest(data)
     72         profiles = dict(schema=1, compiler=compiler['hash'], profiles={
     73             'empty': None,
     74             'practical': dict(hash=support_hash, names=list(bindings), sourceSha256=digest(source)),
     75         })
     76         old_index = HERE / 'packages.json'
     77         previous = json.loads(old_index.read_text())['releases'] if old_index.exists() else []
     78         releases = list(previous)
     79         for entry in catalogue:
     80             if entry.get('preparation') == 'native':
     81                 if entry['id'] in versions:
     82                     raise ValueError('Native demos are released through prepare-showcase.py, not prepare-index.py')
     83                 if not any(r.get('demo') == entry['id'] and r['hash'] == entry['hash'] for r in previous):
     84                     raise ValueError('Native demo has no accepted release: ' + entry['id'])
     85                 continue  # Preserve its accepted native source and explicit editability metadata.
     86             # Operator-owned release labels. Refuse changed contents at a label;
     87             # add/change a version explicitly rather than silently repointing it.
     88             matches = [r for r in previous if r.get('demo') == entry['id'] and r['hash'] == entry['hash']]
     89             version = versions.get(entry['id'], matches[-1]['version'] if matches else '0.1.0')
     90             name = entry['id']
     91             output = run('package', 'publish', '--root', tmp / 'registry', '--name', name,
     92                          '--version', version, '--file', HERE / 'bundles' / (entry['hash'] + '.arboricx'))
     93             _, row = output.splitlines()
     94             _, _, module, bundle = row.split('\t')
     95             release = dict(name=name, version=version, hash=bundle, manifest=module, title=entry['title'], demo=entry['id'])
     96             existing = next((r for r in previous if (r['name'], r['version']) == (name, version)), None)
     97             if existing and (existing['hash'], existing['manifest']) != (bundle, module):
     98                 raise ValueError('Immutable release changed; choose a new version explicitly: ' + name)
     99             if existing:
    100                 releases[releases.index(existing)] = release
    101             else:
    102                 releases.append(release)
    103             entry['environment'] = 'empty'  # Current demos declare imports, never implicit profiles.
    104         (HERE / 'bundles' / (support_hash + '.arboricx')).write_bytes(data)
    105         (HERE / 'environments.json').write_text(json.dumps(profiles, indent=2) + '\n')
    106         (HERE / 'packages.json').write_text(json.dumps(dict(schema=1, releases=releases), indent=2) + '\n')
    107         (HERE / 'catalogue.json').write_text(json.dumps(catalogue, ensure_ascii=False, indent=2) + '\n')
    108         print(f'Prepared {len(releases)} exact releases and pinned practical environment {support_hash}')
    109 
    110 
    111 if __name__ == '__main__':
    112     main()