aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rw-r--r--desktop/modules/updates/test_updatesctl.py42
-rwxr-xr-xdesktop/modules/updates/updatesctl75
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()