tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

container_probe.py (4785B)


      1 #!/usr/bin/env python3
      2 """Run Nix-free acceptance inside a container using distro Python."""
      3 import argparse
      4 import hashlib
      5 import json
      6 import os
      7 from pathlib import Path, PurePosixPath
      8 import platform
      9 import re
     10 import subprocess
     11 import sys
     12 import tarfile
     13 
     14 
     15 def incompatible_loader_error(message, actual_glibc):
     16     """Recognize loader rejection of an unavailable, newer GLIBC symbol version."""
     17     actual = tuple(map(int, actual_glibc.split('.')))
     18     missing = re.findall(r"version [`']GLIBC_(\d+\.\d+(?:\.\d+)?)[`'] not found", message)
     19     return bool(missing) and all(tuple(map(int, v.split('.'))) > actual for v in missing)
     20 
     21 
     22 def extract(archive, destination):
     23     # Transfer members are directories and regular files only. This explicit
     24     # reader works with the distro Python 3.10 incompatibility control as well.
     25     with tarfile.open(archive, 'r:gz') as bundle:
     26         for member in bundle:
     27             path = PurePosixPath(member.name)
     28             if path.is_absolute() or '..' in path.parts or not path.parts or path.parts[0] != 'tricu':
     29                 raise ValueError('invalid transfer member')
     30             target = destination.joinpath(*path.parts)
     31             if member.isdir():
     32                 target.mkdir(parents=True, exist_ok=True)
     33             elif member.isfile():
     34                 target.parent.mkdir(parents=True, exist_ok=True)
     35                 with bundle.extractfile(member) as source, target.open('wb') as output:
     36                     import shutil
     37                     shutil.copyfileobj(source, output)
     38                 target.chmod(0o755 if member.mode & 0o111 else 0o644)
     39             else:
     40                 raise ValueError('transfer must contain only regular files/directories')
     41     return destination/'tricu'
     42 
     43 
     44 def main():
     45     parser = argparse.ArgumentParser(description=__doc__)
     46     parser.add_argument('--expect', choices=['pass', 'glibc-rejection'], required=True)
     47     parser.add_argument('--bindings', action='store_true')
     48     parser.add_argument('--node-version')
     49     args = parser.parse_args()
     50     if args.bindings != bool(args.node_version):
     51         raise ValueError('bindings profile requires an exact Node version')
     52     assert not Path('/nix/store').exists()
     53     assert not os.environ.get('LD_LIBRARY_PATH') and not os.environ.get('TRICU_LIB')
     54     assert platform.machine() == 'x86_64'
     55     glibc = os.confstr('CS_GNU_LIBC_VERSION').split()[-1]
     56     report = dict(schema='tricu.distro-probe.v1', expectation=args.expect,
     57                   os_release=Path('/etc/os-release').read_text(), glibc=glibc,
     58                   python=sys.version, executable=sys.executable,
     59                   kernel=platform.release(), machine=platform.machine(), runs=[])
     60     for location in ('first', 'relocated path with spaces'):
     61         release = extract('/kit/tricu-linux-x86_64.tar.gz', Path('/work')/location)
     62         inventory = json.loads((release/'share/tricu/portable.json').read_text())
     63         for name, row in inventory['libraries'].items():
     64             assert Path(name).name == name
     65             assert hashlib.sha256((release/'lib'/name).read_bytes()).hexdigest() == row['sha256']
     66         library = str(release/'lib/libtricu-haskell.so')
     67         if args.expect == 'glibc-rejection':
     68             # Require a successful ctypes import and a version-specific loader error.
     69             child = subprocess.run([sys.executable, '-c', '''import ctypes, json, sys
     70 try:
     71     ctypes.CDLL(sys.argv[1])
     72 except OSError as error:
     73     print(json.dumps({'loader_error': str(error)}))
     74     sys.exit(42)
     75 sys.exit(0)
     76 ''', library], text=True, capture_output=True, timeout=30)
     77             assert child.returncode == 42 and not child.stderr, child
     78             error = json.loads(child.stdout)['loader_error']
     79             assert incompatible_loader_error(error, glibc), error
     80             report['runs'].append(dict(location=location, loader_error=error))
     81         else:
     82             env = dict(os.environ, PYTHONPATH=str(release/'share/tricu')+':/tests',
     83                        PYTHONDONTWRITEBYTECODE='1')
     84             # Each relocation gets a fresh process/runtime and fresh workload files.
     85             for name in ('checks.json', 'report.json'):
     86                 (Path('/work')/name).unlink(missing_ok=True)
     87             subprocess.run([sys.executable, '/tests/portable_workload.py', str(release)],
     88                            env=env, check=True, timeout=240)
     89             run = dict(location=location, **json.loads(Path('/work/report.json').read_text()))
     90             if args.bindings:
     91                 from container_bindings import run as bindings_run
     92                 run['bindings'] = bindings_run(release, args.node_version)
     93             report['runs'].append(run)
     94     print('TRICU_REPORT='+json.dumps(report, sort_keys=True), flush=True)
     95 
     96 
     97 if __name__ == '__main__':
     98     main()