473 lines
18 KiB
Python
Executable File
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)
|