71 lines
3.5 KiB
Python
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()
|