63 lines
2.7 KiB
Python
63 lines
2.7 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 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()
|
||
|
|
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 + ['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()
|