xtc/tests/integration/test_release.py
2026-11-06 19:05:14 +01:00

71 lines
3.5 KiB
Python

#!/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()