#!/usr/bin/env python3 """Stage a Linux binary/development archive without modifying a live prefix.""" import argparse import gzip import hashlib import io import json import os from pathlib import Path import platform import re import subprocess import tarfile import tempfile from dist import version def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--build-dir', type=Path, required=True) p.add_argument('--output', type=Path, required=True) p.add_argument('--gui', action='store_true') a = p.parse_args() epoch = int(os.environ.get('SOURCE_DATE_EPOCH', '0')) if not 0 <= epoch <= 4294967295: p.error('SOURCE_DATE_EPOCH must fit uint32') name = f'xtc-{version(Path.cwd())}-linux-{platform.machine()}' a.output.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='xtc-binary-stage-') as temporary: stage = Path(temporary) env = dict(os.environ) driver = stage / 'cmake-driver' cache = (a.build_dir / 'CMakeCache.txt').read_text() generator = next(line.partition('=')[2] for line in cache.splitlines() if line.startswith('CMAKE_GENERATOR:INTERNAL=')) jack = 'OFF' if 'XTC_WITH_JACK:BOOL=OFF' in cache else 'ON' subprocess.run(['cmake', '-G', generator, '-S', str(Path.cwd()), '-B', str(driver), '-DBUILD_TESTING=OFF', '-DCMAKE_INSTALL_PREFIX=/usr', '-DCMAKE_INSTALL_LIBDIR=lib', f'-DXTC_BUILD_GUI={"ON" if a.gui else "OFF"}', '-DXTC_INSTALL_PKGCONFIGDIR=lib/pkgconfig', f'-DXTC_INSTALL_BUILD_DIR={a.build_dir.resolve()}', f'-DXTC_WITH_JACK={jack}', f'-DXTC_INSTALL_GUI={"ON" if a.gui else "OFF"}'], env=env, check=True) subprocess.run(['cmake', '--install', str(driver)], env=dict(env, DESTDIR=str(stage)), check=True) # Only the installed /usr tree belongs to the archive. import shutil shutil.rmtree(driver) pc = stage / 'usr/lib/pkgconfig/xtc.pc' pc.write_text(pc.read_text().replace('prefix=/usr\n', 'prefix=${pcfiledir}/../..\n')) entries = [] needed = {} for path in sorted(stage.rglob('*')): if not path.is_file(): continue assert not path.is_symlink() relative = path.relative_to(stage).as_posix() entries.append(dict(path=relative, size=path.stat().st_size, sha256=hashlib.sha256(path.read_bytes()).hexdigest())) if relative in ('usr/bin/xtc', 'usr/bin/xtc-check'): dynamic = subprocess.check_output(['readelf', '-d', str(path)], text=True) needed[relative] = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic) manifest = dict(schema=1, version=version(Path.cwd()), architecture=platform.machine(), libc=list(platform.libc_ver()), needed=needed, files=entries, scope='System-linked Linux build; no libraries, plugins or media bundled.') (stage / 'usr/share/doc/xtc/BINARY_MANIFEST.json').write_text(json.dumps(manifest, indent=2) + '\n') fd, pending = tempfile.mkstemp(prefix='.xtc-package-', dir=a.output.parent) try: with os.fdopen(fd, 'wb') as raw, gzip.GzipFile(fileobj=raw, filename='', mode='wb', mtime=epoch) as gz: with tarfile.open(fileobj=gz, mode='w', format=tarfile.PAX_FORMAT) as tar: for path in sorted(stage.rglob('*')): if not path.is_file(): continue data = path.read_bytes() info = tarfile.TarInfo(name + '/' + path.relative_to(stage).as_posix()) info.size = len(data) info.mode = 0o755 if os.access(path, os.X_OK) else 0o644 info.mtime = epoch tar.addfile(info, io.BytesIO(data)) os.replace(pending, a.output) finally: if os.path.exists(pending): os.unlink(pending) sha = hashlib.sha256(a.output.read_bytes()).hexdigest() a.output.with_name(a.output.name + '.sha256').write_text(f'{sha} {a.output.name}\n') print(f'{a.output}: {sha}') if __name__ == '__main__': main()