#!/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()