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