secrets-engine/scripts/build_release_bundle.py
tegwick ebca4f5adb build: package CLI with pinned producer verifier
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 09:20:33 +02:00

86 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Build wheel + pinned producer verifier; install is a separate reviewed step."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tomllib
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def run(command, **kwargs):
return subprocess.check_output(command,text=True,**kwargs).strip()
def build(root, output, dependency_wheel, uv, allow_build_downloads=False):
if platform.system() != 'Linux':
raise ValueError('this bundle supports Linux CLI only')
# No discovered keys, source rewriting, or runtime compilation.
subprocess.run([sys.executable,str(root/'scripts/check_pdp_producer_source.py')],check=True,stdout=sys.stderr)
locked=next(p for p in tomllib.loads((root/'uv.lock').read_text())['package'] if p['name']=='pyyaml')
admitted={Path(item['url']).name:item['hash'].removeprefix('sha256:') for item in locked['wheels']}
if dependency_wheel.name not in admitted or digest(dependency_wheel)!=admitted[dependency_wheel.name]:
raise ValueError('supplied PyYAML wheel does not match uv.lock')
output.mkdir(mode=0o700,parents=False,exist_ok=False)
epoch=run(['git','log','-1','--format=%ct'],cwd=root)
commit=run(['git','rev-parse','HEAD'],cwd=root)
architecture={'x86_64':'amd64','aarch64':'arm64'}.get(platform.machine())
if architecture is None:
raise ValueError('unsupported Linux architecture')
env={**os.environ,'CGO_ENABLED':'0','GOOS':'linux','GOARCH':architecture,
'GOFLAGS':'','GOWORK':'off','SOURCE_DATE_EPOCH':epoch}
(output/'bin').mkdir();(output/'wheels').mkdir()
subprocess.run(['go','build','-trimpath','-buildvcs=false','-ldflags=-buildid=',
'-o',str(output/'bin/secrets-engine-pdp-verify'),'.'],cwd=root/'verifier',env=env,check=True)
command=[uv,'--no-config','build','--wheel','--out-dir',str(output/'wheels'),str(root)]
if not allow_build_downloads:command.append('--offline')
subprocess.run(command,env=env,check=True)
wheel,=list((output/'wheels').glob('secrets_engine-*.whl'))
shutil.copyfile(dependency_wheel,output/'wheels'/dependency_wheel.name)
helper_hash=digest(output/'bin/secrets-engine-pdp-verify')
launcher=(root/'scripts/release_cli.py').read_text().replace('@HELPER_SHA256@',helper_hash)
(output/'release_cli.py').write_text(launcher)
shutil.copyfile(root/'scripts/install_release_bundle.py',output/'install.py')
shutil.copyfile(root/'verifier/LICENSE.access-engine',output/'LICENSE.access-engine')
sources=[root/'pyproject.toml',root/'uv.lock',root/'verifier/producer-source.json']
sources+=list((root/'src/secrets_engine').glob('*.py'))
sources+=list((root/'verifier/pkg/api').glob('*.go'))+list((root/'verifier/internal/sign').glob('*.go'))
sources+=[root/'verifier/main.go',root/'scripts/check_pdp_producer_source.py',
root/'scripts/build_release_bundle.py',root/'scripts/install_release_bundle.py',root/'scripts/release_cli.py']
source_hashes={str(path.relative_to(root)):digest(path) for path in sorted(sources)}
manifest={"schema":"secrets-engine.cli-bundle.v1","source_commit":commit,"source_files":source_hashes,
"source_date_epoch":int(epoch),"system":platform.system(),"machine":platform.machine(),
"python":platform.python_version(),"go":run(['go','version']),"uv":run([uv,'--version']),
"build_backend":"hatchling==1.27.0","wheel":str(wheel.relative_to(output)),
"producer":json.loads((root/'verifier/producer-source.json').read_text()),
"artifacts":{str(path.relative_to(output)):digest(path) for path in sorted(output.rglob('*')) if path.is_file()},
"admission":"checksums are integrity evidence; owner must admit the bundle/prefix independently"}
(output/'manifest.json').write_text(json.dumps(manifest,sort_keys=True,indent=2)+'\n')
return {'bundle':str(output),'manifest_sha256':digest(output/'manifest.json'),'helper_sha256':helper_hash}
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--dependency-wheel',type=Path,required=True)
parser.add_argument('--uv',default='uv')
parser.add_argument('--allow-build-downloads',action='store_true',help='permit uv to fetch existing build-system dependencies')
args=parser.parse_args()
try:
print(json.dumps(build(Path(__file__).resolve().parents[1],args.output.resolve(),args.dependency_wheel.resolve(),
args.uv,args.allow_build_downloads),sort_keys=True))
except (OSError,ValueError,KeyError,subprocess.SubprocessError) as exc:
print('bundle build refused: '+str(exc),file=sys.stderr);return 1
return 0
if __name__=='__main__':
raise SystemExit(main())