#!/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 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') parser.add_argument('--generator', default=os.environ.get('CMAKE_GENERATOR')) 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)) if args.generator: env['CMAKE_GENERATOR'] = args.generator builds = [] for name in ('first', 'different-path'): project = root / name / top log = root / (name + '.log') with log.open('w') as output: try: subprocess.run(['cmake', '-S', '.', '-B', 'build/release', '-DCMAKE_BUILD_TYPE=Release', '-DXTC_REPRODUCIBLE=ON', f'-DXTC_BUILD_GUI={"ON" if args.gui else "OFF"}'], cwd=project, env=env, stdout=output, stderr=subprocess.STDOUT, check=True) subprocess.run(['cmake', '--build', 'build/release', '--parallel', '3'], cwd=project, env=env, stdout=output, stderr=subprocess.STDOUT, check=True) subprocess.run(['ctest', '--test-dir', 'build/release', '--output-on-failure'], 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()