76 lines
3.3 KiB
Python
76 lines
3.3 KiB
Python
#!/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())
|
|
launcher = ROOT / 'launcher'
|
|
paths.extend((launcher / 'src').rglob('*.rs'))
|
|
paths.extend(p for p in [launcher / 'Cargo.toml', launcher / 'Cargo.lock'] 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()
|