2026-10-27 01:21:04 +01:00
|
|
|
#!/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()
|
2026-10-27 17:13:09 +01:00
|
|
|
assert archive.with_name(archive.name + '.sha256').read_text().split()[0] == digest(archive)
|
2026-10-27 01:21:04 +01:00
|
|
|
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:
|
2026-10-27 04:00:27 +01:00
|
|
|
subprocess.run(make + ['BUILD_DIR=build', 'release'], cwd=project, env=env, stdout=output,
|
2026-10-27 01:21:04 +01:00
|
|
|
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()
|