packages
This commit is contained in:
290
scripts/version_tui.py
Normal file → Executable file
290
scripts/version_tui.py
Normal file → Executable file
@@ -184,6 +184,16 @@ def http_get_json(url: str, token: Optional[str] = None) -> Any:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
def http_get_text(url: str) -> Optional[str]:
|
||||
try:
|
||||
# Provide a basic User-Agent to avoid some hosts rejecting the request
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "version-tui/1.0"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.read().decode("utf-8")
|
||||
except Exception as e:
|
||||
eprintln(f"http_get_text failed for {url}: {e}")
|
||||
return None
|
||||
|
||||
def gh_latest_release(owner: str, repo: str, token: Optional[str]) -> Optional[str]:
|
||||
try:
|
||||
data = http_get_json(f"https://api.github.com/repos/{owner}/{repo}/releases/latest", token)
|
||||
@@ -201,6 +211,14 @@ def gh_latest_tag(owner: str, repo: str, token: Optional[str]) -> Optional[str]:
|
||||
eprintln(f"latest_tag failed for {owner}/{repo}: {e}")
|
||||
return None
|
||||
|
||||
def gh_list_tags(owner: str, repo: str, token: Optional[str]) -> List[str]:
|
||||
try:
|
||||
data = http_get_json(f"https://api.github.com/repos/{owner}/{repo}/tags?per_page=100", token)
|
||||
return [t.get("name") for t in data if isinstance(t, dict) and "name" in t]
|
||||
except Exception as e:
|
||||
eprintln(f"list_tags failed for {owner}/{repo}: {e}")
|
||||
return []
|
||||
|
||||
def gh_head_commit(owner: str, repo: str) -> Optional[str]:
|
||||
out = run_get_stdout(["git", "ls-remote", f"https://github.com/{owner}/{repo}.git", "HEAD"])
|
||||
if not out:
|
||||
@@ -358,11 +376,46 @@ class PackageDetailScreen(ScreenBase):
|
||||
repo = comp.get("repo")
|
||||
if owner and repo:
|
||||
r = gh_latest_release(owner, repo, self.gh_token)
|
||||
if r: c["release"] = r
|
||||
if r:
|
||||
c["release"] = r
|
||||
t = gh_latest_tag(owner, repo, self.gh_token)
|
||||
if t: c["tag"] = t
|
||||
if t:
|
||||
c["tag"] = t
|
||||
m = gh_head_commit(owner, repo)
|
||||
if m: c["commit"] = m
|
||||
if m:
|
||||
c["commit"] = m
|
||||
|
||||
# Special-case raspberrypi/linux: prefer latest stable_* tag or series-specific tags
|
||||
try:
|
||||
if owner == "raspberrypi" and repo == "linux":
|
||||
tags_all = gh_list_tags(owner, repo, self.gh_token)
|
||||
rendered = render_templates(comp, self.merged_vars)
|
||||
cur_tag = str(rendered.get("tag") or "")
|
||||
# If current tag uses stable_YYYYMMDD scheme, pick latest stable_* tag
|
||||
if cur_tag.startswith("stable_"):
|
||||
stable_tags = sorted(
|
||||
[x for x in tags_all if re.match(r"^stable_\d{8}$", x)],
|
||||
reverse=True,
|
||||
)
|
||||
if stable_tags:
|
||||
c["tag"] = stable_tags[0]
|
||||
else:
|
||||
# Try to pick a tag matching the current major.minor series if available
|
||||
mm = str(self.merged_vars.get("modDirVersion") or "")
|
||||
m2 = re.match(r"^(\d+)\.(\d+)", mm)
|
||||
if m2:
|
||||
base = f"rpi-{m2.group(1)}.{m2.group(2)}"
|
||||
series_tags = [x for x in tags_all if (
|
||||
x == f"{base}.y"
|
||||
or x.startswith(f"{base}.y")
|
||||
or x.startswith(f"{base}.")
|
||||
)]
|
||||
series_tags.sort(reverse=True)
|
||||
if series_tags:
|
||||
c["tag"] = series_tags[0]
|
||||
except Exception as _e:
|
||||
# Fallback to previously computed values
|
||||
pass
|
||||
elif fetcher == "git":
|
||||
url = comp.get("url")
|
||||
if url:
|
||||
@@ -414,6 +467,126 @@ class PackageDetailScreen(ScreenBase):
|
||||
return nix_prefetch_url(url)
|
||||
return None
|
||||
|
||||
def cachyos_suffix(self) -> str:
|
||||
if self.vidx == 0:
|
||||
return ""
|
||||
v = self.variants[self.vidx]
|
||||
mapping = {"rc": "-rc", "hardened": "-hardened", "lts": "-lts"}
|
||||
return mapping.get(v, "")
|
||||
|
||||
def fetch_cachyos_linux_latest(self, suffix: str) -> Optional[str]:
|
||||
"""
|
||||
Try to determine latest linux version from upstream:
|
||||
- Prefer .SRCINFO (preprocessed)
|
||||
- Fallback to PKGBUILD (parse pkgver= line)
|
||||
Tries both 'CachyOS' and 'cachyos' org casing just in case.
|
||||
"""
|
||||
bases = [
|
||||
"https://raw.githubusercontent.com/CachyOS/linux-cachyos/master",
|
||||
"https://raw.githubusercontent.com/cachyos/linux-cachyos/master",
|
||||
]
|
||||
paths = [
|
||||
f"linux-cachyos{suffix}/.SRCINFO",
|
||||
f"linux-cachyos{suffix}/PKGBUILD",
|
||||
]
|
||||
|
||||
def parse_srcinfo(text: str) -> Optional[str]:
|
||||
m = re.search(r"^\s*pkgver\s*=\s*([^\s#]+)\s*$", text, re.MULTILINE)
|
||||
if not m:
|
||||
return None
|
||||
v = m.group(1).strip()
|
||||
return v
|
||||
|
||||
def parse_pkgbuild(text: str) -> Optional[str]:
|
||||
# Parse assignments and expand variables in pkgver
|
||||
# Build a simple env map from VAR=value lines
|
||||
env: Dict[str, str] = {}
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
m_assign = re.match(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$', line)
|
||||
if m_assign:
|
||||
key = m_assign.group(1)
|
||||
val = m_assign.group(2).strip()
|
||||
# Remove trailing comments
|
||||
val = re.sub(r'\s+#.*$', '', val).strip()
|
||||
# Strip surrounding quotes
|
||||
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
||||
val = val[1:-1]
|
||||
env[key] = val
|
||||
|
||||
m = re.search(r"^\s*pkgver\s*=\s*(.+)$", text, re.MULTILINE)
|
||||
if not m:
|
||||
return None
|
||||
raw = m.group(1).strip()
|
||||
# Strip quotes
|
||||
if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")):
|
||||
raw = raw[1:-1]
|
||||
|
||||
def expand_vars(s: str) -> str:
|
||||
def repl_braced(mb):
|
||||
key = mb.group(1)
|
||||
return env.get(key, mb.group(0))
|
||||
def repl_unbraced(mu):
|
||||
key = mu.group(1)
|
||||
return env.get(key, mu.group(0))
|
||||
# Expand ${var} then $var
|
||||
s = re.sub(r"\$\{([^}]+)\}", repl_braced, s)
|
||||
s = re.sub(r"\$([A-Za-z_][A-Za-z0-9_]*)", repl_unbraced, s)
|
||||
return s
|
||||
|
||||
v = expand_vars(raw).strip()
|
||||
# normalize rc form like 6.19.rc6 -> 6.19-rc6
|
||||
v = v.replace(".rc", "-rc")
|
||||
return v
|
||||
|
||||
# Try .SRCINFO first, then PKGBUILD
|
||||
for base in bases:
|
||||
# .SRCINFO
|
||||
url = f"{base}/{paths[0]}"
|
||||
text = http_get_text(url)
|
||||
if text:
|
||||
ver = parse_srcinfo(text)
|
||||
if ver:
|
||||
return ver.replace(".rc", "-rc")
|
||||
# PKGBUILD fallback
|
||||
url = f"{base}/{paths[1]}"
|
||||
text = http_get_text(url)
|
||||
if text:
|
||||
ver = parse_pkgbuild(text)
|
||||
if ver:
|
||||
return ver.replace(".rc", "-rc")
|
||||
|
||||
return None
|
||||
|
||||
def linux_tarball_url_for_version(self, version: str) -> str:
|
||||
# Use torvalds snapshot for -rc, stable releases from CDN
|
||||
if "-rc" in version:
|
||||
return f"https://git.kernel.org/torvalds/t/linux-{version}.tar.gz"
|
||||
parts = version.split(".")
|
||||
major = parts[0] if parts else "6"
|
||||
major_minor = ".".join(parts[:2]) if len(parts) >= 2 else version
|
||||
ver_for_tar = major_minor if version.endswith(".0") else version
|
||||
return f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{ver_for_tar}.tar.xz"
|
||||
|
||||
def update_linux_from_pkgbuild(self, name: str):
|
||||
suffix = self.cachyos_suffix()
|
||||
latest = self.fetch_cachyos_linux_latest(suffix)
|
||||
if not latest:
|
||||
self.set_status("linux: failed to get version from PKGBUILD")
|
||||
return
|
||||
url = self.linux_tarball_url_for_version(latest)
|
||||
sri = nix_prefetch_url(url)
|
||||
if not sri:
|
||||
self.set_status("linux: prefetch failed")
|
||||
return
|
||||
ts = self.target_dict.setdefault("sources", {})
|
||||
compw = ts.setdefault(name, {})
|
||||
compw["version"] = latest
|
||||
compw["hash"] = sri
|
||||
self.set_status(f"{name}: updated version to {latest} and refreshed hash")
|
||||
|
||||
def set_ref(self, name: str, kind: str, value: str):
|
||||
# Write to selected target dict (base or variant override)
|
||||
ts = self.target_dict.setdefault("sources", {})
|
||||
@@ -511,15 +684,32 @@ class PackageDetailScreen(ScreenBase):
|
||||
elif ch in (ord('r'),):
|
||||
if self.snames:
|
||||
name = self.snames[self.sidx]
|
||||
self.fetch_candidates_for(name)
|
||||
cand = self.candidates.get(name, {})
|
||||
lines = [
|
||||
f"Candidates for {name}:",
|
||||
f" latest release: {cand.get('release') or '-'}",
|
||||
f" latest tag : {cand.get('tag') or '-'}",
|
||||
f" latest commit : {cand.get('commit') or '-'}",
|
||||
]
|
||||
show_popup(self.stdscr, lines)
|
||||
comp = self.merged_srcs[name]
|
||||
fetcher = comp.get("fetcher", "none")
|
||||
if self.pkg_name == "linux-cachyos" and name == "linux":
|
||||
# Show available linux version from upstream PKGBUILD (.SRCINFO)
|
||||
suffix = self.cachyos_suffix()
|
||||
latest = self.fetch_cachyos_linux_latest(suffix)
|
||||
rendered = render_templates(comp, self.merged_vars)
|
||||
cur_version = str(rendered.get("version") or "")
|
||||
url_hint = self.linux_tarball_url_for_version(latest) if latest else "-"
|
||||
lines = [
|
||||
f"linux-cachyos ({'base' if self.vidx == 0 else self.variants[self.vidx]}):",
|
||||
f" current : {cur_version or '-'}",
|
||||
f" available: {latest or '-'}",
|
||||
f" tarball : {url_hint}",
|
||||
]
|
||||
show_popup(self.stdscr, lines)
|
||||
else:
|
||||
self.fetch_candidates_for(name)
|
||||
cand = self.candidates.get(name, {})
|
||||
lines = [
|
||||
f"Candidates for {name}:",
|
||||
f" latest release: {cand.get('release') or '-'}",
|
||||
f" latest tag : {cand.get('tag') or '-'}",
|
||||
f" latest commit : {cand.get('commit') or '-'}",
|
||||
]
|
||||
show_popup(self.stdscr, lines)
|
||||
elif ch in (ord('i'),):
|
||||
# Show full rendered URL for URL-based sources
|
||||
if self.snames:
|
||||
@@ -586,7 +776,24 @@ class PackageDetailScreen(ScreenBase):
|
||||
("Recompute hash", ("hash", None)),
|
||||
("Cancel", ("cancel", None)),
|
||||
]
|
||||
choice = select_menu(self.stdscr, f"Actions for {name}", [label for label, _ in items])
|
||||
# Build header with current and available refs
|
||||
rendered = render_templates(comp, self.merged_vars)
|
||||
cur_tag = rendered.get("tag") or ""
|
||||
cur_rev = rendered.get("rev") or ""
|
||||
cur_version = rendered.get("version") or ""
|
||||
if cur_tag:
|
||||
current_str = f"current: tag={cur_tag}"
|
||||
elif cur_rev:
|
||||
current_str = f"current: rev={cur_rev[:12]}"
|
||||
elif cur_version:
|
||||
current_str = f"current: version={cur_version}"
|
||||
else:
|
||||
current_str = "current: -"
|
||||
header_lines = [
|
||||
current_str,
|
||||
f"available: release={cand.get('release') or '-'} tag={cand.get('tag') or '-'} commit={(cand.get('commit') or '')[:12] or '-'}",
|
||||
]
|
||||
choice = select_menu(self.stdscr, f"Actions for {name}", [label for label, _ in items], header=header_lines)
|
||||
if choice is not None:
|
||||
kind, val = items[choice][1]
|
||||
if kind in ("release", "tag", "commit"):
|
||||
@@ -621,7 +828,23 @@ class PackageDetailScreen(ScreenBase):
|
||||
menu_items.append(("Recompute hash (prefetch)", ("hash", None)))
|
||||
menu_items.append(("Cancel", ("cancel", None)))
|
||||
|
||||
choice = select_menu(self.stdscr, f"Actions for {name}", [label for label, _ in menu_items])
|
||||
# Build header with current and available release info
|
||||
base = str(self.merged_vars.get("base") or "")
|
||||
rel = str(self.merged_vars.get("release") or "")
|
||||
rp = str(self.merged_vars.get("releasePrefix") or "")
|
||||
rs = str(self.merged_vars.get("releaseSuffix") or "")
|
||||
current_tag = f"{rp}{base}-{rel}{rs}" if (base and rel) else ""
|
||||
if current_tag:
|
||||
current_str = f"current: {current_tag}"
|
||||
elif base or rel:
|
||||
current_str = f"current: base={base or '-'} release={rel or '-'}"
|
||||
else:
|
||||
current_str = "current: -"
|
||||
header_lines = [
|
||||
current_str,
|
||||
f"available: tag={(cand.get('tag') or '-') if cand else '-'} base={(cand.get('base') or '-') if cand else '-'} release={(cand.get('release') or '-') if cand else '-'}",
|
||||
]
|
||||
choice = select_menu(self.stdscr, f"Actions for {name}", [label for label, _ in menu_items], header=header_lines)
|
||||
if choice is not None:
|
||||
kind, payload = menu_items[choice][1]
|
||||
if kind == "update_vars" and isinstance(payload, dict):
|
||||
@@ -652,19 +875,50 @@ class PackageDetailScreen(ScreenBase):
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
show_popup(self.stdscr, [f"{name}: fetcher={fetcher}", "Use 'e' to edit fields manually."])
|
||||
if self.pkg_name == "linux-cachyos" and name == "linux":
|
||||
# Offer update of linux version from upstream PKGBUILD (.SRCINFO)
|
||||
suffix = self.cachyos_suffix()
|
||||
latest = self.fetch_cachyos_linux_latest(suffix)
|
||||
rendered = render_templates(comp, self.merged_vars)
|
||||
cur_version = str(rendered.get("version") or "")
|
||||
header_lines = [
|
||||
f"current: version={cur_version or '-'}",
|
||||
f"available: version={latest or '-'}",
|
||||
]
|
||||
opts = []
|
||||
if latest:
|
||||
opts.append(f"Update linux version to {latest} from PKGBUILD (.SRCINFO)")
|
||||
else:
|
||||
opts.append("Update linux version from PKGBUILD (.SRCINFO)")
|
||||
opts.append("Cancel")
|
||||
choice = select_menu(self.stdscr, f"Actions for {name}", opts, header=header_lines)
|
||||
if choice == 0 and latest:
|
||||
self.update_linux_from_pkgbuild(name)
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
show_popup(self.stdscr, [f"{name}: fetcher={fetcher}", "Use 'e' to edit fields manually."])
|
||||
else:
|
||||
pass
|
||||
|
||||
def select_menu(stdscr, title: str, options: List[str]) -> Optional[int]:
|
||||
def select_menu(stdscr, title: str, options: List[str], header: Optional[List[str]] = None) -> Optional[int]:
|
||||
idx = 0
|
||||
while True:
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
stdscr.addstr(0, 0, title[:w-1])
|
||||
for i, opt in enumerate(options[:h-3], start=0):
|
||||
y = 1
|
||||
if header:
|
||||
for line in header:
|
||||
if y >= h-2:
|
||||
break
|
||||
stdscr.addstr(y, 0, str(line)[:w-1])
|
||||
y += 1
|
||||
start_y = y + 1
|
||||
max_opts = max(0, h - start_y - 1)
|
||||
for i, opt in enumerate(options[:max_opts], start=0):
|
||||
sel = ">" if i == idx else " "
|
||||
stdscr.addstr(2+i, 0, f"{sel} {opt}"[:w-1])
|
||||
stdscr.addstr(start_y + i, 0, f"{sel} {opt}"[:w-1])
|
||||
stdscr.addstr(h-1, 0, "Enter: select | Backspace: cancel")
|
||||
stdscr.refresh()
|
||||
ch = stdscr.getch()
|
||||
|
||||
Reference in New Issue
Block a user