#!/usr/bin/env python3 """Rebuild source exports in different paths and compare release artifacts.""" import argparse import hashlib import os from pathlib import Path import shlex import subprocess import tarfile import tempfile def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('archive', type=Path) parser.add_argument('--gui', action='store_true') args = parser.parse_args() archive = args.archive.resolve() assert archive.with_name(archive.name + '.sha256').read_text().split()[0] == digest(archive) with tempfile.TemporaryDirectory(prefix='xtc-release-') as directory: root = Path(directory) with tarfile.open(archive) as source: names = [m.name for m in source.getmembers()] assert len(set(names)) == len(names) assert all(m.isfile() for m in source.getmembers()) top = names[0].split('/')[0] epoch = source.getmembers()[0].mtime assert all(n.startswith(top + '/') and '..' not in Path(n).parts for n in names) assert all('__pycache__' not in Path(n).parts and '.git' not in Path(n).parts for n in names) for name in ('first', 'different-path'): source.extractall(root / name, filter='data') env = dict(os.environ, SOURCE_DATE_EPOCH=str(epoch), MAKEFLAGS='', MFLAGS='') builds = [] for name in ('first', 'different-path'): project = root / name / top log = root / (name + '.log') make = shlex.split(env.get('MAKE', 'make')) with log.open('w') as output: try: subprocess.run(make + ['BUILD_DIR=build', 'release'], cwd=project, env=env, stdout=output, stderr=subprocess.STDOUT, check=True) except subprocess.CalledProcessError: print(log.read_text()[-12000:]) raise # Exported source can package itself without a .git directory. repacked = root / (name + '.tar.gz') subprocess.run([env.get('PYTHON', 'python3'), 'tools/dist.py', '--output', str(repacked)], cwd=project, env=env, check=True) assert digest(repacked) == digest(archive), 'Source archive is not reproducible' release = project / 'build/release' files = ['libxtc.a', 'xtc-check'] + (['xtc'] if args.gui else []) builds.append({file: digest(release / file) for file in files}) assert builds[0] == builds[1], (builds[0], builds[1]) for file, sha in builds[0].items(): print(f'Reproducible {file}: {sha}') print('Two clean source-export release builds and archives are byte-identical.') if __name__ == '__main__': main()