Files
pi-pm3/ci/generate-manifest.py
michael a9acdb85ce Build optimization: pre-built PM3 binaries, ARM64 CI, base image caching
Replace PM3 compile-from-source in pi-gen with pre-built tarball extraction
(saves 43-58 min). Merge stagePM3 into stageDangerousPi as 02-pm3-install
substage, renumber all subsequent substages. Switch CI PM3 build to native
ARM64 runner (ubuntu-24.04-arm64) eliminating QEMU overhead. Add weekly
base-image workflow for pre-baking stages 0-2. Support PM3_TARBALL,
BASE_IMAGE, and APT_PROXY env vars in build-image.sh.

Also includes prior Phase 5 work: theme system, design system integration,
component update system, OS updates, CI build pipeline, and test results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:01:01 -08:00

125 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate release-manifest.json from built component tarballs.
Scans the dist/ directory for component tarballs, reads their
COMPONENT_VERSION metadata, computes SHA256 checksums, and writes
a release-manifest.json suitable for upload as a GitHub Release asset.
Usage:
python3 ci/generate-manifest.py [dist_dir] [--version VERSION]
"""
import hashlib
import json
import os
import sys
import tarfile
from pathlib import Path
def sha256_file(path: Path) -> str:
"""Compute SHA256 checksum of a file."""
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
def read_component_version(tarball_path: Path) -> dict:
"""Read COMPONENT_VERSION JSON from inside a tarball."""
with tarfile.open(tarball_path, "r:gz") as tf:
for member in tf.getmembers():
if member.name.endswith("COMPONENT_VERSION"):
f = tf.extractfile(member)
if f:
return json.loads(f.read().decode())
return {}
def main():
dist_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("dist")
version = None
if "--version" in sys.argv:
idx = sys.argv.index("--version")
version = sys.argv[idx + 1]
if not dist_dir.exists():
print(f"Error: {dist_dir} does not exist")
sys.exit(1)
# Read VERSION file if version not specified
if not version:
version_file = Path("VERSION")
version = version_file.read_text().strip() if version_file.exists() else "0.1.0"
manifest = {
"schema_version": 1,
"release_version": version,
"components": {},
}
tarballs = sorted(dist_dir.glob("*.tar.gz"))
if not tarballs:
print(f"Warning: no tarballs found in {dist_dir}")
for tarball in tarballs:
filename = tarball.name
checksum = sha256_file(tarball)
size = tarball.stat().st_size
comp_version = read_component_version(tarball)
comp_id = comp_version.get("component_id", "")
if not comp_id:
# Try to infer from filename
for known in ("pm3", "frontend", "backend", "theme"):
if filename.startswith(known):
comp_id = known
break
if not comp_id:
print(f" Skipping {filename} (unknown component)")
continue
comp_ver = comp_version.get("version", version)
# Build asset key (platform-specific for pm3)
platform_str = comp_version.get("platform")
python_abi = comp_version.get("python_abi")
if platform_str and python_abi:
asset_key = f"{platform_str}-{python_abi}"
else:
asset_key = "default"
asset_info = {
"filename": filename,
"checksum_sha256": checksum,
"size": size,
}
if comp_version.get("python_version"):
asset_info["python_version"] = comp_version["python_version"]
if platform_str:
asset_info["platform"] = platform_str
# Initialize component entry if needed
if comp_id not in manifest["components"]:
manifest["components"][comp_id] = {
"version": comp_ver,
"assets": {},
"changelog": "",
}
manifest["components"][comp_id]["assets"][asset_key] = asset_info
print(f" {comp_id}/{asset_key}: {filename} ({size} bytes)")
# Write manifest
manifest_path = dist_dir / "release-manifest.json"
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
print(f"\n✅ Wrote {manifest_path}")
print(f" Components: {list(manifest['components'].keys())}")
if __name__ == "__main__":
main()