tricu

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

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