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