#!/usr/bin/env python3 """ version.json CLI updater. Usage examples: # Update a GitHub source to its latest release tag, then recompute hash scripts/update.py --file packages/edk2/version.json --github-latest-release --prefetch # Update a specific component to the latest commit scripts/update.py --file packages/edk2/version.json --component edk2 --github-latest-commit --prefetch # Update all URL-based sources in a file (recompute hash only) scripts/update.py --file packages/uboot/version.json --url-prefetch # Update a variant's variables scripts/update.py --file packages/proton-cachyos/version.json --variant cachyos-v4 \\ --set variables.base=10.0 --set variables.release=20260301 # Filter tags with a regex (e.g. only stable_* tags) scripts/update.py --file packages/raspberrypi/linux-rpi/version.json \\ --component stable --github-latest-tag --tag-regex '^stable_\\d{8}$' --prefetch # Update a fetchgit source to HEAD scripts/update.py --file packages/linux-cachyos/version.json --component zfs --git-latest --prefetch # Dry run (show what would change, don't write) scripts/update.py --file packages/edk2/version.json --github-latest-release --prefetch --dry-run """ from __future__ import annotations import argparse import os import sys from pathlib import Path from typing import List, Optional # Ensure scripts/ is on the path so we can import lib and hooks sys.path.insert(0, str(Path(__file__).resolve().parent)) import lib import hooks # noqa: F401 — registers hooks as a side effect def _apply_set_pairs(target: lib.Json, pairs: List[str]) -> bool: changed = False for pair in pairs: if "=" not in pair: lib.eprint(f"--set: expected KEY=VALUE, got: {pair!r}") continue key, val = pair.split("=", 1) path = [p for p in key.strip().split(".") if p] lib.deep_set(target, path, val) lib.eprint(f" set {'.'.join(path)} = {val!r}") changed = True return changed def update_components( spec: lib.Json, variant: Optional[str], components: Optional[List[str]], args: argparse.Namespace, ) -> bool: changed = False merged_vars, merged_srcs, target_dict = lib.merged_view(spec, variant) target_sources: lib.Json = target_dict.setdefault("sources", {}) names = ( list(merged_srcs.keys()) if not components else [c for c in components if c in merged_srcs] ) if components: missing = [c for c in components if c not in merged_srcs] for m in missing: lib.eprint(f" [warn] component '{m}' not found in merged sources") for name in names: view_comp = merged_srcs[name] fetcher = view_comp.get("fetcher", "none") comp = target_sources.setdefault(name, {}) if fetcher == "github": owner = view_comp.get("owner") or "" repo = view_comp.get("repo") or "" if not (owner and repo): lib.eprint(f" [{name}] missing owner/repo, skipping") continue # --set-branch: update branch field and fetch HEAD of that branch if args.set_branch is not None: new_branch = args.set_branch or None # empty string → clear branch if new_branch: comp["branch"] = new_branch lib.eprint(f" [{name}] branch -> {new_branch!r}") else: comp.pop("branch", None) lib.eprint(f" [{name}] branch cleared") changed = True rev = lib.gh_head_commit(owner, repo, new_branch) if rev: comp["rev"] = rev comp.pop("tag", None) lib.eprint(f" [{name}] rev -> {rev}") changed = True if args.prefetch: sri = lib.prefetch_github( owner, repo, rev, submodules=bool(view_comp.get("submodules", False)), ) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True else: lib.eprint( f" [{name}] could not resolve HEAD for branch {new_branch!r}" ) continue # skip the normal ref-update logic for this component new_ref: Optional[str] = None ref_kind = "" if args.github_latest_release: tag = lib.gh_latest_release(owner, repo) if tag: new_ref, ref_kind = tag, "tag" elif args.github_latest_tag: tag = lib.gh_latest_tag(owner, repo, tag_regex=args.tag_regex) if tag: new_ref, ref_kind = tag, "tag" elif args.github_latest_commit: rev = lib.gh_head_commit(owner, repo) if rev: new_ref, ref_kind = rev, "rev" if new_ref: if ref_kind == "tag": comp["tag"] = new_ref comp.pop("rev", None) else: comp["rev"] = new_ref comp.pop("tag", None) lib.eprint(f" [{name}] {ref_kind} -> {new_ref}") changed = True if args.prefetch and (new_ref or args.url_prefetch): # Use merged view with the updated ref for prefetching merged_vars2, merged_srcs2, _ = lib.merged_view(spec, variant) view2 = lib.render(merged_srcs2.get(name, view_comp), merged_vars2) sri = lib.prefetch_github( owner, repo, view2.get("tag") or view2.get("rev") or new_ref or "", submodules=bool(view_comp.get("submodules", False)), ) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True elif fetcher == "git": url = view_comp.get("url") or "" if not url: lib.eprint(f" [{name}] missing url for git fetcher, skipping") continue # --set-branch: update branch field and fetch HEAD of that branch if args.set_branch is not None: new_branch = args.set_branch or None if new_branch: comp["branch"] = new_branch lib.eprint(f" [{name}] branch -> {new_branch!r}") else: comp.pop("branch", None) lib.eprint(f" [{name}] branch cleared") changed = True rev = lib.git_branch_commit(url, new_branch) if rev: comp["rev"] = rev lib.eprint(f" [{name}] rev -> {rev}") changed = True if args.prefetch: sri = lib.prefetch_git(url, rev) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True else: lib.eprint( f" [{name}] could not resolve HEAD for branch {new_branch!r}" ) continue if args.git_latest: rev = lib.git_branch_commit(url, view_comp.get("branch")) if rev: comp["rev"] = rev lib.eprint(f" [{name}] rev -> {rev}") changed = True if args.prefetch: sri = lib.prefetch_git(url, rev) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True elif fetcher == "url": if args.latest_version: url_info = lib._url_source_info(view_comp, merged_vars) kind = url_info.get("kind", "plain") version_var = url_info.get("version_var") or "version" new_ver: Optional[str] = None if kind == "github": owner = url_info.get("owner", "") repo = url_info.get("repo", "") tags = lib.gh_release_tags(owner, repo) if owner and repo else [] prefix = str(merged_vars.get("releasePrefix") or "") suffix = str(merged_vars.get("releaseSuffix") or "") if prefix or suffix: tag = next( ( t for t in tags if t.startswith(prefix) and t.endswith(suffix) ), None, ) else: tag = tags[0] if tags else None if tag: # Proton-cachyos style: extract base+release from tag mid = tag if prefix and mid.startswith(prefix): mid = mid[len(prefix) :] if suffix and mid.endswith(suffix): mid = mid[: -len(suffix)] parts = mid.split("-") if ( len(parts) >= 2 and "base" in merged_vars and "release" in merged_vars ): lib.eprint( f" [{name}] latest tag: {tag} (base={parts[0]}, release={parts[-1]})" ) vs = target_dict.setdefault("variables", {}) vs["base"] = parts[0] vs["release"] = parts[-1] changed = True merged_vars2, merged_srcs2, _ = lib.merged_view( spec, variant ) view2 = merged_srcs2.get(name, view_comp) sri = lib.prefetch_source(view2, merged_vars2) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True else: new_ver = tag tag = None # avoid fall-through elif kind == "openvsx": publisher = url_info.get("publisher", "") ext_name = url_info.get("ext_name", "") new_ver = lib.openvsx_latest_version(publisher, ext_name) elif kind == "plain": lib.eprint( f" [{name}] url (plain): cannot auto-detect version; use --set" ) if new_ver: lib.eprint(f" [{name}] latest version: {new_ver}") vs = target_dict.setdefault("variables", {}) vs[version_var] = new_ver changed = True if args.prefetch: # Re-render with updated variable merged_vars2, merged_srcs2, _ = lib.merged_view(spec, variant) view2 = merged_srcs2.get(name, view_comp) sri = lib.prefetch_source(view2, merged_vars2) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True elif args.url_prefetch or args.prefetch: rendered = lib.render(view_comp, merged_vars) url = rendered.get("url") or rendered.get("urlTemplate") or "" if not url: lib.eprint(f" [{name}] no url/urlTemplate for url fetcher") else: sri = lib.prefetch_source(view_comp, merged_vars) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True elif fetcher == "pypi": if args.latest_version: pkg_name = view_comp.get("name") or str(merged_vars.get("name") or name) new_ver = lib.pypi_latest_version(pkg_name) if new_ver: version_var = ( lib._url_source_info(view_comp, merged_vars).get("version_var") or "version" ) cur_ver = str(merged_vars.get(version_var) or "") if new_ver == cur_ver: lib.eprint(f" [{name}] pypi: already at {new_ver}") else: lib.eprint(f" [{name}] pypi: {cur_ver} -> {new_ver}") vs = target_dict.setdefault("variables", {}) vs[version_var] = new_ver changed = True if args.prefetch: sri = lib.pypi_hash(pkg_name, new_ver) if sri: comp["hash"] = sri lib.eprint(f" [{name}] hash -> {sri}") changed = True else: lib.eprint(f" [{name}] pypi hash prefetch failed") else: lib.eprint( f" [{name}] pypi: could not fetch latest version for {pkg_name!r}" ) elif args.url_prefetch or args.prefetch: lib.eprint( f" [{name}] pypi: use --latest-version --prefetch to update hash" ) return changed def main() -> int: ap = argparse.ArgumentParser( description="Update version.json files", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split("\n", 2)[2], # show the usage block as epilog ) ap.add_argument( "--file", required=True, metavar="PATH", help="Path to version.json" ) ap.add_argument( "--variant", metavar="NAME", help="Variant to target (default: base)" ) ap.add_argument( "--component", dest="components", action="append", metavar="NAME", help="Limit to specific component(s); can be repeated", ) ap.add_argument( "--github-latest-release", action="store_true", help="Update GitHub sources to latest release tag", ) ap.add_argument( "--github-latest-tag", action="store_true", help="Update GitHub sources to latest tag", ) ap.add_argument( "--github-latest-commit", action="store_true", help="Update GitHub sources to HEAD commit", ) ap.add_argument( "--tag-regex", metavar="REGEX", help="Filter tags (used with --github-latest-tag)", ) ap.add_argument( "--set-branch", metavar="BRANCH", default=None, help=( "Set the branch field on github/git sources, resolve its HEAD commit, " "and (with --prefetch) recompute the hash. " "Pass an empty string ('') to clear the branch and switch back to tag/release tracking." ), ) ap.add_argument( "--git-latest", action="store_true", help="Update fetchgit sources to latest HEAD commit", ) ap.add_argument( "--latest-version", action="store_true", help=( "Fetch the latest version from upstream (PyPI, Open VSX, GitHub releases) " "and update the version variable. Use with --prefetch to also recompute the hash." ), ) ap.add_argument( "--url-prefetch", action="store_true", help="Recompute hash for url/urlTemplate sources", ) ap.add_argument( "--prefetch", action="store_true", help="After updating ref, also recompute hash", ) ap.add_argument( "--set", dest="sets", action="append", default=[], metavar="KEY=VALUE", help="Set a field (dot-path relative to base or --variant). Can be repeated.", ) ap.add_argument( "--dry-run", action="store_true", help="Show changes without writing" ) ap.add_argument( "--print", dest="do_print", action="store_true", help="Print resulting JSON to stdout", ) args = ap.parse_args() path = Path(args.file) if not path.exists(): lib.eprint(f"File not found: {path}") return 1 spec = lib.load_json(path) lib.eprint(f"Loaded: {path}") # Apply --set mutations target = spec if args.variant: target = spec.setdefault("variants", {}).setdefault(args.variant, {}) changed = _apply_set_pairs(target, args.sets) # Update refs/hashes if update_components(spec, args.variant, args.components, args): changed = True if changed: if args.dry_run: lib.eprint("Dry run: no changes written.") else: lib.save_json(path, spec) lib.eprint(f"Saved: {path}") else: lib.eprint("No changes.") if args.do_print: import json print(json.dumps(spec, indent=2, ensure_ascii=False)) return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: sys.exit(130)