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>
This commit is contained in:
124
ci/generate-manifest.py
Executable file
124
ci/generate-manifest.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user