diff options
Diffstat (limited to 'desktop/modules/updates')
| -rw-r--r-- | desktop/modules/updates/test_updatesctl.py | 42 | ||||
| -rwxr-xr-x | desktop/modules/updates/updatesctl | 75 |
2 files changed, 117 insertions, 0 deletions
diff --git a/desktop/modules/updates/test_updatesctl.py b/desktop/modules/updates/test_updatesctl.py new file mode 100644 index 0000000..09c3676 --- /dev/null +++ b/desktop/modules/updates/test_updatesctl.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +"""Run: /usr/bin/python3 desktop/modules/updates/test_updatesctl.py + +Imports updatesctl for vercmp, then drives it as a CLI against a fixture tree +under a temp dir (UPDATESCTL_ROOT for system paths, HOME for the user's), +never the live system. Plain asserts, no framework. +""" + +import importlib.machinery +import importlib.util +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parent / "updatesctl" + +loader = importlib.machinery.SourceFileLoader("updatesctl", str(SCRIPT)) +spec = importlib.util.spec_from_loader("updatesctl", loader) +u = importlib.util.module_from_spec(spec) +loader.exec_module(u) + +# --- vercmp --- +assert u.vercmp("1.14.10", "1.14.9") > 0 +assert u.vercmp("2.1.278", "2.1.267") > 0 +assert u.vercmp("2026_10_02", "2026_09_12") > 0 +assert u.vercmp("v1.2", "1.2") == 0 +assert u.vercmp("1.0", "1.0") == 0 +# A pre-release is older than the release, a suffix is never behind. +assert u.vercmp("0.12.3_beta.4", "0.12.3") < 0 +assert u.vercmp("9.018.00_6.18.41", "9.018.00") > 0 +assert u.vercmp("3.1-659215d", "3.1") > 0 +assert u.vercmp("1.0.1", "1.0rc1") > 0 + +print("ok") diff --git a/desktop/modules/updates/updatesctl b/desktop/modules/updates/updatesctl new file mode 100755 index 0000000..c1e69ed --- /dev/null +++ b/desktop/modules/updates/updatesctl @@ -0,0 +1,75 @@ +#!/usr/bin/python3 +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +"""updatesctl - what to do next to bring this machine up to date. + +Prints one JSON document for the drawer's updates module, with every pending +update already sorted into the action it needs. The sections and the reasons +for them are in docs/superpowers/specs/2026-10-15-updates-design.md. + + updatesctl from the last nvchecker results + updatesctl --refresh run nvchecker first + +UPDATESCTL_ROOT prefixes every system path and HOME roots the user's, so the +test can point both at a fixture tree. +""" + +import json +import os +import re +import shutil +import subprocess +import sys +import time +from functools import cmp_to_key +from pathlib import Path + +ROOT = os.environ.get("UPDATESCTL_ROOT", "") +HOME = Path(os.environ["HOME"]) +NFS_TIMEOUT = float(os.environ.get("UPDATESCTL_NFS_TIMEOUT", "5")) + + +def sysp(p): + return Path(ROOT + p) + + +def vercmp(a, b): + """Version order: numeric chunks as numbers, numbers after letters. + + When one version runs out, the longer one is newer unless its next chunk + is alphabetic, which makes 0.12.3_beta.4 older than 0.12.3 and a kernel + suffix or git hash (9.018.00_6.18.41) newer than its base, never behind. + """ + def chunks(v): + v = v.lower() + return re.findall(r"\d+|[a-z]+", v[1:] if v.startswith("v") else v) + + x, y = chunks(a), chunks(b) + for p, q in zip(x, y): + if p == q: + continue + if p.isdigit() and q.isdigit(): + return (int(p) > int(q)) - (int(p) < int(q)) + if p.isdigit() != q.isdigit(): + return 1 if p.isdigit() else -1 + return (p > q) - (p < q) + if len(x) == len(y): + return 0 + longer, sign = (x, 1) if len(x) > len(y) else (y, -1) + return sign if longer[min(len(x), len(y))].isdigit() else -sign + + +def main(): + pass + + +if __name__ == "__main__": + main() |
