Files
nix-config/scripts/update.py
mjallen18 70002a19e2 hmm
2026-04-07 18:39:42 -05:00

473 lines
18 KiB
Python
Executable File

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