archive_probe.py (4901B)
1 #!/usr/bin/env python3 2 """Probe the pinned archives' readiness for a C-loadable core shared library. 3 4 Invoked by archive-probe.nix with the same reducer/API sources and ordinary -O2 5 settings as build.nix. A recognized non-PIC link failure is diagnostic data; 6 unrelated build errors fail the derivation. A successful link is not installed 7 or presented as an accepted runtime: packaging/lifecycle/performance acceptance 8 remains separate. 9 """ 10 import argparse 11 from collections import Counter 12 import gzip 13 import json 14 from pathlib import Path 15 import re 16 import subprocess 17 import sys 18 19 20 def classify(returncode, log): 21 if returncode == 0: 22 return 'link-succeeded' 23 errors = [line for line in log.splitlines() if 'error:' in line] 24 def known_error(line): 25 return bool(re.search(r'error: .*\.a\(.*(?:recompile with -fPIC|unsupported reloc \d+ against global symbol)', line) 26 or re.search(r'collect2: error: ld returned \d+ exit status', line)) 27 if (re.search(r'error: .*\.a\([^\n]+recompile with -fPIC', log) 28 and all(known_error(line) for line in errors)): 29 return 'blocked-non-pic-archives' 30 raise RuntimeError('Archive probe failed for an unclassified reason; inspect its build log') 31 32 33 def main(): 34 # Reject ordinary compile failures and mixed-cause link failures rather than 35 # misreporting every unsuccessful build as a known toolchain limitation. 36 assert classify(0, '') == 'link-succeeded' 37 example = 'ld.gold: error: /tmp/libHSx.a(X.o): recompile with -fPIC' 38 assert classify(1, example) == 'blocked-non-pic-archives' 39 for bad in ('Main.hs: error: missing name', example + '\nld: error: undefined symbol'): 40 try: 41 classify(1, bad) 42 except RuntimeError: 43 pass 44 else: 45 raise AssertionError('unrelated failure accepted') 46 parser = argparse.ArgumentParser(description=__doc__) 47 parser.add_argument('--output', required=True, type=Path) 48 args = parser.parse_args() 49 args.output.mkdir(parents=True, exist_ok=True) 50 command = [ 51 'ghc', '-O2', '-static', '-shared', '-fPIC', '-flink-rts', '-threaded', 52 '-optl-Wl,-z,nodelete', 53 '-XOverloadedStrings', '-XScopedTypeVariables', '-XMultiWayIf', '-XLambdaCase', 54 '-Wall', '-isrc', '-iext/haskell/src', '-outputdir', 'objects', 55 '-optc-Iext/haskell/include', 56 'ext/haskell/src/EmbeddingCore.hs', 'ext/haskell/src/init.c', 57 '-o', 'libtricu-core.so', 58 ] 59 ghc_version = subprocess.check_output(['ghc', '--numeric-version'], text=True).strip() 60 if tuple(map(int, ghc_version.split('.')[:2])) >= (9, 14): 61 command.insert(1, '-optl-Wl,-u,init_ghc_hs_iface') 62 result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 63 log = result.stdout.decode('utf-8', errors='replace') 64 # Keep full diagnostics compressed rather than filling the console with tens 65 # of thousands of repeated relocation errors. They are not runtime data. 66 with gzip.GzipFile(args.output / 'link.log.gz', 'wb', mtime=1) as stream: 67 stream.write(result.stdout) 68 try: 69 status = classify(result.returncode, log) 70 except RuntimeError: 71 print(log[-16000:], file=sys.stderr) 72 raise 73 errors = [line for line in log.splitlines() if 'error:' in line and 'recompile with -fPIC' in line] 74 archives = Counter(name for line in errors for name in re.findall(r'/([^/ ]+\.a)\(', line)) 75 report = { 76 'schema': 'tricu.core-archive-probe.v1', 77 'ghc_version': ghc_version, 78 'link_status': status, 79 'link_exit_code': result.returncode, 80 'command': command, 81 'non_pic_archives': dict(sorted(archives.items())), 82 'relocation_examples': [re.sub(r'/nix/store/[^ ]+?/(libHS[^ ]+\.a|bin/ld.gold)', r'\1', line) 83 for line in errors[:3]], 84 'runtime_artifact_accepted': False, 85 'size_and_performance_measured': False, 86 } 87 if status == 'link-succeeded': 88 # Immediate ABI sanity checks; full installed-runtime and timing checks 89 # are deliberately not inferred from a successful linker invocation. 90 subprocess.run([ 91 'cc', '-Wall', '-Wextra', '-Werror', 'ext/haskell/tests.c', 92 '-Iext/haskell/include', '-L.', '-ltricu-core', 93 '-Wl,-rpath,' + str(Path.cwd()), '-o', 'core-client', 94 ], check=True) 95 subprocess.run(['./core-client'], check=True) 96 subprocess.run([sys.executable, 'ext/haskell/test_exports.py', 97 './libtricu-core.so', 'core'], check=True) 98 report['c_client_and_exports'] = 'passed' 99 else: 100 report['c_client_and_exports'] = 'not-run-link-blocked' 101 (args.output / 'report.json').write_text(json.dumps(report, indent=2) + '\n') 102 print(json.dumps(report, indent=2)) 103 104 105 if __name__ == '__main__': 106 main()