Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e6f0-a6f6-7b93-9124-55ef584930e2
118 lines
6 KiB
Python
118 lines
6 KiB
Python
"""Build twice, smoke-test an isolated wheel, and emit release evidence and SBOM."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import tomllib
|
|
import zipfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EPOCH = '315532800' # Fixed ZIP-compatible build timestamp, not trusted time.
|
|
|
|
|
|
def run(*args, cwd=ROOT, env=None):
|
|
try:
|
|
return subprocess.run([str(a) for a in args], cwd=cwd, env=env,
|
|
check=True, capture_output=True, text=True).stdout
|
|
except subprocess.CalledProcessError as exc:
|
|
print(exc.stderr, file=sys.stderr)
|
|
raise
|
|
|
|
|
|
def sha(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def write(path, value):
|
|
path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--output', type=Path, required=True, help='New evidence directory')
|
|
args = parser.parse_args()
|
|
output = args.output.resolve()
|
|
output.mkdir(parents=True, exist_ok=False)
|
|
env = dict(os.environ, SOURCE_DATE_EPOCH=EPOCH, PYTHONHASHSEED='0')
|
|
env.pop('PYTHONPATH', None)
|
|
inputs = [ROOT / 'pyproject.toml', ROOT / 'README.md', ROOT / '.gitignore', *sorted((ROOT / 'src').rglob('*.py')),
|
|
*sorted((ROOT / 'src').rglob('*.json'))]
|
|
manifest = {str(p.relative_to(ROOT)): sha(p) for p in inputs}
|
|
# Verify the separately reviewed spec and packaged schema copies agree.
|
|
for p in (ROOT / 'src/railiance_clock/schemas').glob('*.json'):
|
|
assert p.read_bytes() == (ROOT / 'specs' / p.name).read_bytes(), p.name
|
|
with tempfile.TemporaryDirectory(prefix='rclk-release-') as tmp:
|
|
temp = Path(tmp)
|
|
build_env = temp / 'build-env'
|
|
run('uv', 'venv', '--python', sys.executable, build_env)
|
|
build_python = build_env / 'bin/python'
|
|
run('uv', 'pip', 'install', '--python', build_python, '--require-hashes',
|
|
'-r', ROOT / 'tools/build-requirements.txt')
|
|
artifacts = []
|
|
for number in (1, 2):
|
|
source = temp / f'source-{number}'
|
|
source.mkdir()
|
|
for original in inputs:
|
|
target = source / original.relative_to(ROOT)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(original, target)
|
|
# Deliberately vary source mtimes between the independent roots.
|
|
os.utime(target, (int(EPOCH) + number * 100, int(EPOCH) + number * 100))
|
|
dest = temp / f'dist-{number}'
|
|
run('uv', 'build', source, '--python', build_python, '--no-build-isolation',
|
|
'--out-dir', dest, env=env)
|
|
artifacts.append({p.name: sha(p) for p in dest.iterdir()
|
|
if p.name.endswith(('.whl', '.tar.gz'))})
|
|
assert artifacts[0] == artifacts[1], 'build outputs differ'
|
|
checkout_dist = temp / 'checkout-dist'
|
|
run('uv', 'build', ROOT, '--python', build_python, '--no-build-isolation',
|
|
'--out-dir', checkout_dist, env=env)
|
|
checkout_artifacts = {p.name: sha(p) for p in checkout_dist.iterdir()
|
|
if p.name.endswith(('.whl', '.tar.gz'))}
|
|
assert artifacts[0] == checkout_artifacts, 'checkout build differs from copied inputs'
|
|
wheel = next((temp / 'dist-1').glob('*.whl'))
|
|
with zipfile.ZipFile(wheel) as archive:
|
|
members = archive.namelist()
|
|
assert len([m for m in members if m.endswith('.schema.json')]) == 3
|
|
requirements = temp / 'runtime.txt'
|
|
run('uv', 'export', '--locked', '--no-dev', '--no-emit-project', '-o', requirements)
|
|
install_env = temp / 'installed-env'
|
|
run('uv', 'venv', '--python', sys.executable, install_env)
|
|
python = install_env / 'bin/python'
|
|
run('uv', 'pip', 'install', '--python', python, '--require-hashes', '-r', requirements)
|
|
run('uv', 'pip', 'install', '--python', python, '--no-deps', wheel)
|
|
run('uv', 'pip', 'check', '--python', python)
|
|
smoke = json.loads(run(python, '-I', ROOT / 'tools/wheel_smoke.py', cwd=temp, env=env))
|
|
locked = {p['name']: p['version'] for p in tomllib.loads((ROOT / 'uv.lock').read_text())['package']}
|
|
for name, version in smoke['distributions']:
|
|
assert locked[name.lower().replace('_', '-')] == version, (name, version)
|
|
components = [{'type': 'library', 'name': name, 'version': version,
|
|
'purl': f'pkg:pypi/{name.lower().replace("_", "-")}@{version}'}
|
|
for name, version in smoke['distributions']]
|
|
write(output / 'runtime.cdx.json', {'bomFormat': 'CycloneDX', 'specVersion': '1.5',
|
|
'version': 1, 'components': components})
|
|
build_tools = json.loads(run(build_python, '-I', '-c',
|
|
'import importlib.metadata as m,json; print(json.dumps(sorted((d.metadata["Name"],d.version) for d in m.distributions())))'))
|
|
write(output / 'verification.json', {
|
|
'source_base_commit': run('git', 'rev-parse', 'HEAD').strip(),
|
|
'source_inputs_sha256': manifest, 'runtime_lock_sha256': sha(ROOT / 'uv.lock'),
|
|
'build_requirements_sha256': sha(ROOT / 'tools/build-requirements.txt'),
|
|
'verifier_sha256': sha(Path(__file__)), 'smoke_script_sha256': sha(ROOT / 'tools/wheel_smoke.py'),
|
|
'python': platform.python_version(), 'platform': platform.system() + '/' + platform.machine(),
|
|
'uv': run('uv', '--version').strip(), 'build_tools': build_tools,
|
|
'source_date_epoch': EPOCH, 'independent_builds': artifacts,
|
|
'checkout_build': checkout_artifacts,
|
|
'byte_identical': True, 'smoke': smoke,
|
|
'sbom_sha256': sha(output / 'runtime.cdx.json'),
|
|
'limits': 'Local Linux build/install evidence; no native UTC, security admission, vulnerability audit or cross-platform reproducibility claim.'})
|
|
print(f'Verified two identical builds and isolated wheel smoke test: {output}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|