aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--remote/config.example.toml3
-rwxr-xr-xremote/sbb-remote104
-rw-r--r--sb_builder/__init__.py2
-rw-r--r--tests/test_remote.py65
4 files changed, 168 insertions, 6 deletions
diff --git a/remote/config.example.toml b/remote/config.example.toml
index a77ace9..0fbe801 100644
--- a/remote/config.example.toml
+++ b/remote/config.example.toml
@@ -12,4 +12,5 @@ hintdir = "/etc/slackrepo/SBo-danix/hintfiles"
default_hintdir = "" # searched after hintdir when set
genreprc = "/root/.genreprc" # REPOSOWNERGPG names the signing key
slackrepo_logdir = "/var/log/slackrepo/SBo-danix"
-manifest_packages = ["neovim", "openvino"] # bundled deps: hint-update refuses them (phase 4b)
+manifest_packages = ["neovim"] # bundled deps: hint-update refuses them (phase 4b)
+phantom_deps_file = "/etc/sb-builder/phantom-deps" # deps -current ships; one per line, # comments
diff --git a/remote/sbb-remote b/remote/sbb-remote
index 5183b77..c37b44c 100755
--- a/remote/sbb-remote
+++ b/remote/sbb-remote
@@ -40,7 +40,7 @@ import time
import tomllib
import urllib.request
-VERSION = "0.6.0"
+VERSION = "0.7.0"
DEFAULTS = {
"state_dir": "/var/lib/sb-builder",
"repo_dir": "/var/lib/sbopkg/SBo-danix",
@@ -53,7 +53,8 @@ DEFAULTS = {
"default_hintdir": "",
"genreprc": "/root/.genreprc",
"slackrepo_logdir": "/var/log/slackrepo/SBo-danix",
- "manifest_packages": ["neovim", "openvino"], # bundled deps: left to mkhint until phase 4b
+ "manifest_packages": ["neovim"], # bundled deps: left to mkhint until phase 4b
+ "phantom_deps_file": "/etc/sb-builder/phantom-deps", # deps -current ships: DELREQUIREd
}
FLAGS = ("--yes", "--no")
JOB_ID = re.compile(r"^\d{8}-\d{6}-[a-z]+(-[a-z]+)*(-\d+)?$")
@@ -196,14 +197,18 @@ def hint_args(cfg, kind, args):
def _self_job(kind):
"""A job kind run by this file itself (_hint), argv-free like the others."""
def build(cfg, args):
- hint_args(cfg, kind, args)
+ if kind == "fix-current":
+ if args:
+ raise Error("fix-current takes no arguments")
+ else:
+ hint_args(cfg, kind, args)
return [sys.executable, os.path.abspath(__file__), "_hint", kind, *args], {}
return build
KINDS = {"sbpull": kind_sbpull, "sbshadow": kind_sbshadow,
"slackrepo": kind_slackrepo, "publish": kind_publish,
- **{k: _self_job(k) for k in ("hint-update", "hint-new")}}
+ **{k: _self_job(k) for k in ("hint-update", "hint-new", "fix-current")}}
def signs(kind, args):
@@ -757,8 +762,95 @@ def _save(target, text):
_write(target, text)
+# ── phantom deps: what -current ships, DELREQUIREd from the SBo packages ─────
+
+def phantom_deps(cfg):
+ """The listed names, file order; a missing file is an empty list."""
+ text = _read(cfg["phantom_deps_file"]) or ""
+ return list(dict.fromkeys(n for n in (ln.split("#")[0].strip() for ln in text.splitlines()) if n))
+
+
+def _set_phantom(cfg, names, add):
+ names = list(names)
+ if not names or not all(PKGNAME.match(n) for n in names):
+ raise Error("expected package names")
+ text = _read(cfg["phantom_deps_file"]) or ""
+ have = phantom_deps(cfg)
+ if add:
+ text += "".join(f"{n}\n" for n in dict.fromkeys(names) if n not in have)
+ else: # drop the names' lines, comments and the rest kept as written
+ text = "".join(ln for ln in text.splitlines(keepends=True)
+ if ln.split("#")[0].strip() not in names)
+ _write(cfg["phantom_deps_file"], text)
+ return {"deps": phantom_deps(cfg)}
+
+
+def cmd_phantom_add(cfg, *names):
+ return _set_phantom(cfg, names, True)
+
+
+def cmd_phantom_remove(cfg, *names):
+ return _set_phantom(cfg, names, False)
+
+
+def _requires(cfg, pkgs):
+ """{pkg: REQUIRES words without %README%}."""
+ out = {}
+ for pkg, path in pkgs.items():
+ info = _read(os.path.join(cfg["repo_dir"], path, pkg + ".info")) or ""
+ out[pkg] = [r for r in hint_tokens(info, "REQUIRES") if r != "%README%"]
+ return out
+
+
+def cmd_phantom_list(cfg):
+ """The list, and suggestions: REQUIRES names no package of the repo has (SBo
+ drops a package when it enters the official tree), with who needs them."""
+ pkgs = _pkgmap(cfg)
+ listed = set(phantom_deps(cfg))
+ needed = {}
+ for pkg, reqs in sorted(_requires(cfg, pkgs).items()):
+ for r in reqs:
+ if r not in pkgs and r not in listed:
+ needed.setdefault(r, []).append(pkg)
+ return {"deps": phantom_deps(cfg),
+ "suggestions": [{"name": n, "needed_by": needed[n]} for n in sorted(needed)]}
+
+
+def merge_delrequires(text, deps):
+ """text with deps added to DELREQUIRES, or None when all are there already."""
+ have = hint_tokens(text, "DELREQUIRES")
+ add = [d for d in dict.fromkeys(deps) if d not in have]
+ return hint_set(text, "DELREQUIRES", " ".join(have + add)) if add else None
+
+
+def cmd_fix_current_now(cfg):
+ """mkhint -F: listed deps into the DELREQUIRES of whoever requires them (add only)."""
+ ok, failed = [], []
+ listed = phantom_deps(cfg)
+ pkgs = _pkgmap(cfg)
+ for pkg, reqs in sorted(_requires(cfg, pkgs).items()):
+ deps = [d for d in listed if d in reqs]
+ if not deps:
+ continue
+ try:
+ _, target = _hint_target(cfg, pkg, pkgs)
+ text = merge_delrequires(_read(target) or "", deps)
+ if text is not None:
+ _save(target, text)
+ print(f"{pkg}: DELREQUIRES {' '.join(deps)}", flush=True)
+ ok.append(pkg)
+ except (Error, OSError) as e:
+ print(f"{pkg}: failed: {e}", flush=True)
+ failed.append([pkg, str(e)])
+ print("SBB-RESULT " + json.dumps({"ok": ok, "failed": failed}), flush=True)
+ if failed:
+ raise Error(f"{len(failed)} failed")
+
+
def cmd_hint(cfg, kind=None, *args):
"""Body of the hint jobs: log lines, then SBB-RESULT {"ok": [...], "failed": [[pkg, why]]}."""
+ if kind == "fix-current":
+ return cmd_fix_current_now(cfg)
if kind not in ("hint-update", "hint-new"):
raise Error(f"unknown hint job: {kind}")
ok, failed = [], []
@@ -900,6 +992,10 @@ COMMANDS = {
"hint-revert": cmd_hint_revert,
"hint-delete": cmd_hint_delete,
"job-result": cmd_job_result,
+ "phantom-list": cmd_phantom_list,
+ "phantom-add": cmd_phantom_add,
+ "phantom-remove": cmd_phantom_remove,
+ "fix-current-now": cmd_fix_current_now,
"_run": cmd_run,
"_hint": cmd_hint,
}
diff --git a/sb_builder/__init__.py b/sb_builder/__init__.py
index f9bea08..f4a1f98 100644
--- a/sb_builder/__init__.py
+++ b/sb_builder/__init__.py
@@ -16,4 +16,4 @@
"""sb-builder: Qt6 GUI driving the SlackBuild tooling on a build VM."""
-VERSION = "0.6.0" # must match remote/sbb-remote VERSION
+VERSION = "0.7.0" # must match remote/sbb-remote VERSION
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 54c5614..57d492c 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -434,6 +434,71 @@ class HintJobsTest(Base):
self.assertEqual(self.read(self.hint), 'VERSION="2.0"\nDOWNLOAD="no"\n')
+class PhantomTest(Base):
+ def setUp(self):
+ super().setUp()
+ self.cfg["phantom_deps_file"] = os.path.join(self.tmp, "phantom-deps")
+ with open(self.cfg["phantom_deps_file"], "w") as f:
+ f.write("# deps needed on stable but not on -current\nrust-opt\n")
+ self.make_pkg("desktop/marco", 'VERSION="1"\nREQUIRES="zenity libfoo"\n')
+ self.make_pkg("games/steam", 'VERSION="1"\nREQUIRES="zenity"\n',
+ hint='DELREQUIRES="rust-opt"\n')
+ self.make_pkg("libs/libfoo", 'VERSION="1"\nREQUIRES="%README% rust-opt"\n')
+
+ def read(self, path):
+ with open(path) as f:
+ return f.read()
+
+ def test_list_and_suggestions(self):
+ self.assertEqual(sbb.cmd_phantom_list(self.cfg), {
+ "deps": ["rust-opt"],
+ "suggestions": [{"name": "zenity", "needed_by": ["marco", "steam"]}]})
+ os.remove(self.cfg["phantom_deps_file"])
+ self.assertEqual(sbb.cmd_phantom_list(self.cfg)["deps"], []) # missing file: empty list
+
+ def test_add_remove_keep_comments(self):
+ self.assertEqual(sbb.cmd_phantom_add(self.cfg, "zenity", "rust-opt", "zenity"),
+ {"deps": ["rust-opt", "zenity"]})
+ self.assertEqual(sbb.cmd_phantom_list(self.cfg)["suggestions"], [])
+ self.assertEqual(sbb.cmd_phantom_remove(self.cfg, "rust-opt"), {"deps": ["zenity"]})
+ self.assertEqual(self.read(self.cfg["phantom_deps_file"]),
+ "# deps needed on stable but not on -current\nzenity\n")
+ for bad in ([], ["../x"], ["a b"]):
+ with self.subTest(bad=bad), self.assertRaises(sbb.Error):
+ sbb.cmd_phantom_add(self.cfg, *bad)
+
+ def test_fix_current(self):
+ sbb.cmd_phantom_add(self.cfg, "zenity")
+ out = io.StringIO()
+ with contextlib.redirect_stdout(out):
+ sbb.cmd_fix_current_now(self.cfg)
+ lines = out.getvalue().splitlines()
+ self.assertEqual(json.loads(lines[-1].split(" ", 1)[1]),
+ {"ok": ["libfoo", "marco", "steam"], "failed": []})
+ hints = self.cfg["hintdir"]
+ self.assertEqual(self.read(os.path.join(hints, "marco.hint")), 'DELREQUIRES="zenity"\n')
+ self.assertFalse(os.path.exists(os.path.join(hints, "marco.hint.bak"))) # new: revert deletes
+ self.assertEqual(self.read(os.path.join(hints, "steam.hint")), 'DELREQUIRES="rust-opt zenity"\n')
+ self.assertEqual(self.read(os.path.join(hints, "steam.hint.bak")), 'DELREQUIRES="rust-opt"\n')
+ self.assertIn("marco: DELREQUIRES zenity", lines)
+ out = io.StringIO()
+ with contextlib.redirect_stdout(out):
+ sbb.cmd_fix_current_now(self.cfg) # second run: nothing to do, no .bak churn
+ self.assertIn('{"ok": [], "failed": []}', out.getvalue())
+ self.assertEqual(self.read(os.path.join(hints, "steam.hint.bak")), 'DELREQUIRES="rust-opt"\n')
+
+ def test_merge_delrequires(self):
+ self.assertEqual(sbb.merge_delrequires('DELREQUIRES="a"\n', ["b", "a"]), 'DELREQUIRES="a b"\n')
+ self.assertIsNone(sbb.merge_delrequires('DELREQUIRES="a b"\n', ["b"]))
+ self.assertEqual(sbb.merge_delrequires("", ["b", "b"]), 'DELREQUIRES="b"\n')
+
+ def test_job_kind(self):
+ me = [sys.executable, os.path.abspath(sbb.__file__), "_hint"]
+ self.assertEqual(sbb.KINDS["fix-current"](self.cfg, []), (me + ["fix-current"], {}))
+ with self.assertRaisesRegex(sbb.Error, "no arguments"):
+ sbb.KINDS["fix-current"](self.cfg, ["x"])
+
+
class MainTest(Base):
def test_unknown_command(self):
rc, data = self.run_main("nope")