#!/usr/bin/env python3 """Create and verify the reproducible standalone application manifest.""" import argparse import hashlib import json from pathlib import Path import subprocess import tomllib ROOT = Path(__file__).resolve().parents[1] COMPONENTS = {'gui': 'intake-gui', 'scanner': 'pc-intake'} def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def source_id(): paths = [] for component in COMPONENTS: base = ROOT / component paths.extend((base / 'src').rglob('*.rs')) paths.extend(p for p in [base / 'Cargo.toml', base / 'Cargo.lock', base / 'build.rs'] if p.exists()) paths.extend(p for p in (ROOT / 'scripts').iterdir() if p.is_file()) h = hashlib.sha256() for path in sorted(paths): h.update(str(path.relative_to(ROOT)).encode() + b'\0') h.update(path.read_bytes()) return 'csr-' + h.hexdigest()[:20] def create(path): binaries = {} for component, name in COMPONENTS.items(): package = tomllib.loads((ROOT / component / 'Cargo.toml').read_text())['package'] binary = ROOT / component / 'target/release' / name binaries[name] = {'version': package['version'], 'sha256': digest(binary)} build_id = source_id() version = subprocess.check_output([ROOT / 'gui/target/release/intake-gui', '--version'], text=True).strip() if build_id not in version: raise ValueError('GUI source fingerprint does not match current sources; rebuild release') manifest = {'schema_version': 1, 'build_id': build_id, 'rustc': subprocess.check_output(['rustc', '--version'], text=True).strip(), 'binaries': binaries} path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(manifest, indent=2) + '\n') def verify(release_dir): manifest = json.loads((release_dir / 'release.json').read_text()) expected = set(COMPONENTS.values()) if manifest.get('schema_version') != 1: raise ValueError('Unsupported release manifest schema') if set(manifest.get('binaries', {})) != expected: raise ValueError('Release manifest has missing or unexpected binaries') for name, spec in manifest['binaries'].items(): binary = release_dir / name if digest(binary) != spec['sha256']: raise ValueError(f'Binary hash mismatch: {name}') if not binary.stat().st_mode & 0o111: raise ValueError(f'Binary is not executable: {name}') version = subprocess.check_output([release_dir / 'intake-gui', '--version'], text=True) if manifest['build_id'] not in version: raise ValueError('GUI build ID differs from release manifest') print(f"Release verified: {manifest['build_id']}") def main(): parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--source-id', action='store_true') group.add_argument('--create', type=Path) group.add_argument('--verify-release', type=Path) args = parser.parse_args() if args.source_id: print(source_id()) elif args.create: create(args.create) else: verify(args.verify_release) if __name__ == '__main__': main()