tricu

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

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