portable.py (4752B)
1 #!/usr/bin/env python3 2 """Relocate trusted, Nix-built ELF dependencies; system glibc remains external.""" 3 import argparse 4 import hashlib 5 import json 6 import re 7 import shutil 8 import subprocess 9 from pathlib import Path 10 11 12 def output(*command): 13 return subprocess.check_output(command, text=True).strip() 14 15 16 def dependencies(path): 17 text = output('ldd', str(path)) 18 if 'not found' in text: 19 raise RuntimeError(text) 20 found = {} 21 for line in text.splitlines(): 22 fields = line.split() 23 if len(fields) >= 3 and fields[1] == '=>' and fields[2].startswith('/'): 24 name = Path(fields[0]).name 25 path = Path(fields[2]).resolve(strict=True) 26 if name in found and found[name] != path: 27 raise RuntimeError(f'ambiguous native dependency: {name}') 28 found[name] = path 29 return found 30 31 32 def main(): 33 parser = argparse.ArgumentParser(description=__doc__) 34 parser.add_argument('--host', type=Path, required=True) 35 parser.add_argument('--glibc', type=Path, required=True) 36 parser.add_argument('--output', type=Path, required=True) 37 parser.add_argument('--materials', type=Path, required=True) 38 args = parser.parse_args() 39 root = args.output 40 root.mkdir(parents=True, exist_ok=False) 41 (root/'lib').mkdir() 42 (root/'include').mkdir() 43 (root/'share/tricu').mkdir(parents=True) 44 library = args.host/'lib/libtricu-haskell.so' 45 materials = json.loads((args.materials/'components.json').read_text()) 46 if hashlib.sha256(library.read_bytes()).hexdigest() != materials['host_sha256']: 47 raise RuntimeError('source materials identify a different embedding library') 48 loaded = dependencies(library) 49 if any(name.startswith('libHS') for name in loaded): 50 raise RuntimeError('portable packaging requires archive-linked Haskell') 51 system = {name: path for name, path in loaded.items() if path.is_relative_to(args.glibc)} 52 native = {name: path for name, path in loaded.items() if name not in system} 53 if set(native) != set(materials['shared_owners']): 54 raise RuntimeError('native source/notice inventory differs from bundled libraries') 55 for name, path in native.items(): 56 row = next(row for row in materials['inputs'] if row.get('soname') == name) 57 if hashlib.sha256(path.read_bytes()).hexdigest() != row['sha256']: 58 raise RuntimeError(f'native source materials identify different bytes: {name}') 59 native[library.name] = library.resolve(strict=True) 60 versions = set() 61 inventory = {} 62 for name, source in sorted(native.items()): 63 if Path(name).name != name: 64 raise RuntimeError(f'non-relative library identity: {name}') 65 target = root/'lib'/name 66 shutil.copyfile(source, target) 67 target.chmod(0o755) 68 subprocess.run(['patchelf', '--set-rpath', '$ORIGIN', str(target)], check=True) 69 needed = output('patchelf', '--print-needed', str(target)).splitlines() 70 if any('/' in item or item not in native and item not in system for item in needed): 71 raise RuntimeError(f'incomplete dependency closure: {name}: {needed}') 72 assert output('patchelf', '--print-rpath', str(target)) == '$ORIGIN' 73 versions.update(re.findall(r'\bGLIBC_(\d+\.\d+(?:\.\d+)?)\b', 74 output('readelf', '--version-info', str(target)))) 75 inventory[name] = dict(sha256=hashlib.sha256(target.read_bytes()).hexdigest(), 76 bytes=target.stat().st_size, needed=needed) 77 shutil.copyfile(args.host/'include/tricu.h', root/'include/tricu.h') 78 shutil.copyfile(args.host/'share/tricu/tricu.py', root/'share/tricu/tricu.py') 79 shutil.copyfile(args.host/'share/tricu/build.json', root/'share/tricu/build.json') 80 for name in ('THIRD-PARTY-NOTICES.md', 'REDISTRIBUTION.md', 'license-components.json'): 81 shutil.copyfile(args.materials/name, root/'share/tricu'/name) 82 shutil.copytree(args.materials/'notices', root/'share/tricu/notices') 83 report = dict(source_manifest_sha256=hashlib.sha256((args.materials/'SHA256.json').read_bytes()).hexdigest(), 84 source_symlinks_sha256=hashlib.sha256((args.materials/'SYMLINKS.json').read_bytes()).hexdigest(), 85 schema='tricu.portable-elf.v1', architecture='x86_64-linux', 86 glibc_symbol_floor=max(versions, key=lambda s: tuple(map(int, s.split('.')))), 87 system_sonames=sorted(system), libraries=inventory, 88 scope='Relocatable native embedding; system glibc; same-platform acceptance required') 89 (root/'share/tricu/portable.json').write_text(json.dumps(report, indent=2, sort_keys=True)+'\n') 90 91 92 if __name__ == '__main__': 93 main()