aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md10
-rw-r--r--README.md9
-rw-r--r--sb_builder/hints_tab.py117
-rw-r--r--tests/test_gui.py40
4 files changed, 163 insertions, 13 deletions
diff --git a/AGENTS.md b/AGENTS.md
index f55d952..9f92d71 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -33,12 +33,12 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is
- Hints are parsed (`shlex`), never sourced. `sb_builder/hints.py` mirrors
slackrepo's own rules (`../GITHUB/slackrepo/libexec/functions.d/parsefunctions.sh`);
check that file when changing it.
-- Hint editing lives in the helper (`hint-update`, `hint-new` jobs run the
- helper's own `_hint` command): text edits, `.bak` before each
+- Hint editing lives in the helper (`hint-update`, `hint-new`, `fix-current`
+ jobs run the helper's own `_hint` command): text edits, `.bak` before each
write, a final `SBB-RESULT {json}` log line read back with `job-result`.
- It replaces mkhint, which stays on the VM for neovim and openvino
- (bundled deps, `manifest_packages`) and for the `DELREQUIRES` hints
- sbshadow regenerates with `mkhint -F` after every sync, until phase 4b.
+ The phantom deps list is the helper's `phantom_deps_file`; sbshadow runs
+ `sbb-remote fix-current-now` after each sync. mkhint stays on the VM only
+ for neovim (bundled deps, `manifest_packages`), until phase 4b part 2.
- Jobs never get input. Any tool run as a job must be non-interactive
(`--yes`/`--no` style flags). If a tool needs a flag it lacks, add it in
that tool's own repo (e.g. `slackrepo_setup` for sbshadow) rather than
diff --git a/README.md b/README.md
index de8b22d..0d42aa6 100644
--- a/README.md
+++ b/README.md
@@ -73,8 +73,13 @@ reopens it.
**No download** on, `DOWNLOAD="no"` instead; a hint sbshadow wrote,
`DELREQUIRES` only, gets the `.info` version lines added). **Delete hint**
removes the ticked packages' hints, each kept as a `.bak`, revertable from
- the dialog that follows. neovim and openvino are refused for now: their
- bundled deps still need mkhint on the VM.
+ the dialog that follows. **Phantom deps…** keeps the list of deps
+ -current ships, which SBo packages still require (SBo drops a package once
+ it is official): add names, or tick suggestions, the required names no
+ package of the tree has; **Apply to hints** writes them into the
+ `DELREQUIRES` of every package that requires them (sbshadow does the same
+ after each sync). neovim is refused for now: its bundled deps still need
+ mkhint on the VM.
- **Results**: slackrepo's last build attempt per package, newest first,
filterable by result. **Show build log** (or double-click) opens the end of
the log of the package's last attempt (slackrepo keeps one per command:
diff --git a/sb_builder/hints_tab.py b/sb_builder/hints_tab.py
index fbf8a70..ca16354 100644
--- a/sb_builder/hints_tab.py
+++ b/sb_builder/hints_tab.py
@@ -21,9 +21,9 @@ import os
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QColor, QFontDatabase, QKeySequence, QShortcut, QSyntaxHighlighter
-from PyQt6.QtWidgets import (QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
- QMessageBox, QPlainTextEdit, QPushButton, QSplitter, QTableWidget,
- QTableWidgetItem, QVBoxLayout, QWidget)
+from PyQt6.QtWidgets import (QAbstractItemView, QDialog, QHBoxLayout, QLabel, QLineEdit,
+ QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton,
+ QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
from . import upstream
from .dashboard import STATUS
@@ -34,7 +34,7 @@ STATES = ("outdated", "current", "no stanza", "not checked", "nvchecker error")
STATE_COLOR = {"outdated": STATUS["aborted"], "current": STATUS["ok"],
"nvchecker error": STATUS["failed"]}
COLUMNS = ("", "package", "hint", ".info", "upstream", "state")
-HINT_JOBS = ("hint-update", "hint-new")
+HINT_JOBS = ("hint-update", "hint-new", "fix-current")
class HintsTab(QWidget):
@@ -78,7 +78,10 @@ class HintsTab(QWidget):
self.act_update = action(self, "Update selected", "document-save", SP.SP_DialogSaveButton,
self.update_selected)
self.act_delete = action(self, "Delete hint", "edit-delete", SP.SP_TrashIcon, self.delete)
- self.actions = [self.act_check, self.act_update, self.act_delete]
+ self.actions = [self.act_check, self.act_update, self.act_delete,
+ action(self, "Phantom deps…", "view-list-details", SP.SP_FileDialogListView,
+ self.phantom)]
+ self.phantom_dialog = None
self.no_dl = action(self, "No download", "network-offline", SP.SP_DriveNetIcon, checkable=True)
self.no_dl.setToolTip('Update selected writes DOWNLOAD="no": slackrepo uses the source '
"already in its cache")
@@ -212,6 +215,14 @@ class HintsTab(QWidget):
self.start_requested.emit(["hint-update", *self.pairs()]
+ (["--no-dl"] if self.no_dl.isChecked() else []))
+ def phantom(self):
+ if self.phantom_dialog is None:
+ self.phantom_dialog = d = PhantomDialog(self.remote, self)
+ d.apply_requested.connect(lambda: self.start_requested.emit(["fix-current"]))
+ d.finished.connect(lambda _r: setattr(self, "phantom_dialog", None))
+ d.show()
+ self.phantom_dialog.raise_()
+
def confirm(self, title, text):
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
@@ -332,3 +343,99 @@ class HintResultDialog(QDialog):
if pkgs:
self.build_requested.emit(pkgs)
self.accept()
+
+
+class PhantomDialog(QDialog):
+ """The -current phantom deps: the list, suggestions from the tree, Apply = fix-current."""
+
+ apply_requested = pyqtSignal()
+
+ def __init__(self, remote, parent=None):
+ super().__init__(parent)
+ self.remote = remote
+ self.setWindowTitle("Phantom deps: shipped by -current, DELREQUIREd from SBo packages")
+ self.resize(900, 600)
+ self.deps = QListWidget(selectionMode=QAbstractItemView.SelectionMode.ExtendedSelection)
+ self.name = QLineEdit(placeholderText="package name")
+ self.name.returnPressed.connect(self.add_name)
+ add = QPushButton("Add")
+ add.clicked.connect(self.add_name)
+ remove = QPushButton("Remove")
+ remove.clicked.connect(self.remove_selected)
+ self.suggestions = QListWidget()
+ self.suggestions.itemClicked.connect(lambda item: item.setCheckState(
+ Qt.CheckState.Unchecked if item.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked))
+ add_ticked = QPushButton("Add ticked")
+ add_ticked.clicked.connect(self.add_ticked)
+ apply = QPushButton("Apply to hints", default=True)
+ apply.setToolTip("Start fix-current: the listed deps go into DELREQUIRES of the "
+ "packages that require them")
+ apply.clicked.connect(self.apply)
+ close = QPushButton("Close")
+ close.clicked.connect(self.accept)
+
+ left = QVBoxLayout()
+ left.addWidget(QLabel("<b>Listed</b>"))
+ left.addWidget(self.deps)
+ left.addWidget(self.name)
+ row = QHBoxLayout()
+ row.addWidget(add)
+ row.addWidget(remove)
+ left.addLayout(row)
+ right = QVBoxLayout()
+ right.addWidget(QLabel("<b>Suggested</b>: required, but no package of the tree has them"))
+ right.addWidget(self.suggestions)
+ right.addWidget(add_ticked)
+ lists = QHBoxLayout()
+ lists.addLayout(left, 1)
+ lists.addLayout(right, 2)
+ bar = QHBoxLayout()
+ bar.addStretch()
+ bar.addWidget(close)
+ bar.addWidget(apply)
+ layout = QVBoxLayout(self)
+ layout.addLayout(lists, 1)
+ layout.addLayout(bar)
+ self.reload()
+
+ def reload(self):
+ self.remote.call(self._loaded, "phantom-list")
+
+ def _loaded(self, data, err):
+ if err:
+ QMessageBox.warning(self, "Phantom deps", err)
+ return
+ self.deps.clear()
+ self.deps.addItems(data["deps"])
+ self.suggestions.clear()
+ for s in data["suggestions"]:
+ needed = s["needed_by"]
+ more = f" (+{len(needed) - 5})" if len(needed) > 5 else ""
+ item = QListWidgetItem(f"{s['name']} needed by {', '.join(needed[:5])}{more}")
+ item.setData(Qt.ItemDataRole.UserRole, s["name"])
+ item.setToolTip(", ".join(needed))
+ item.setFlags(Qt.ItemFlag.ItemIsEnabled) # the click handler toggles: no double toggle
+ item.setCheckState(Qt.CheckState.Unchecked)
+ self.suggestions.addItem(item)
+
+ def _change(self, cmd, names):
+ if names:
+ self.remote.call(lambda _d, err: self._loaded(None, err) if err else self.reload(),
+ cmd, *names)
+
+ def add_name(self):
+ name = self.name.text().strip()
+ self.name.clear()
+ self._change("phantom-add", [name] if name else [])
+
+ def add_ticked(self):
+ self._change("phantom-add", [
+ self.suggestions.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.suggestions.count())
+ if self.suggestions.item(i).checkState() == Qt.CheckState.Checked])
+
+ def remove_selected(self):
+ self._change("phantom-remove", [i.text() for i in self.deps.selectedItems()])
+
+ def apply(self):
+ self.apply_requested.emit()
+ self.accept()
diff --git a/tests/test_gui.py b/tests/test_gui.py
index ca51c3c..a25be37 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -36,6 +36,7 @@ from PyQt6.QtWidgets import QApplication # noqa: E402
from sb_builder import VERSION, config # noqa: E402
from sb_builder import hints as hintlib # noqa: E402
+from sb_builder import hints_tab # noqa: E402
from sb_builder import packages as pk # noqa: E402
from sb_builder.mainwindow import MainWindow # noqa: E402
from sb_builder.remote import DOWN, MISSING, parse # noqa: E402
@@ -534,7 +535,7 @@ class SmokeTest(unittest.TestCase):
"Dashboard": (["Refresh"], []),
"Packages": (["Build", "Update", "Rebuild", "Remove", "Update all", "New hint", "Publish"],
["Select visible", "Clear", "Refresh"]),
- "Hints": (["Check upstream", "Update selected", "Delete hint"],
+ "Hints": (["Check upstream", "Update selected", "Delete hint", "Phantom deps…"],
["Select visible", "Clear", "No download", "Refresh"]),
"Results": (["Show build log"], ["Refresh"]),
"Jobs": (["Pull overlay", "Check shadowing", "Run it", "Kill"], []),
@@ -620,6 +621,43 @@ class SmokeTest(unittest.TestCase):
self.assertEqual(len(w.hint_dialogs), 1) # Revert lives there
self.assertIn("-A=1", w.hint_dialogs[0].diff.toPlainText())
+ def test_phantom_deps_dialog(self):
+ listed = ["rust-opt"]
+
+ def change(args, _stdin):
+ if args[0] == "phantom-add":
+ listed.extend(n for n in args[1:] if n not in listed)
+ else:
+ listed[:] = [n for n in listed if n not in args[1:]]
+ return {"deps": list(listed)}, None
+ w, remote = self.window(**{
+ "phantom-list": lambda args, _stdin: ({"deps": list(listed), "suggestions": [
+ {"name": n, "needed_by": ["marco", "steam"]} for n in ("jq", "zenity") if n not in listed]}, # sorted, as the helper
+ None),
+ "phantom-add": change, "phantom-remove": change,
+ "job-start": ({"id": "20260101-000001-fix-current"}, None)})
+ w.poll_jobs()
+ tab = w.open_hints()
+ tab.actions[-1].trigger()
+ d = tab.phantom_dialog
+ self.assertEqual(d.deps.item(0).text(), "rust-opt")
+ self.assertEqual(d.suggestions.count(), 2)
+ self.assertEqual(d.suggestions.item(1).text(), "zenity needed by marco, steam")
+ d.suggestions.itemClicked.emit(d.suggestions.item(1)) # tick zenity by its row
+ d.add_ticked()
+ self.assertIn(("phantom-add", "zenity"), remote.calls)
+ self.assertEqual([d.deps.item(i).text() for i in range(d.deps.count())], ["rust-opt", "zenity"])
+ self.assertEqual(d.suggestions.count(), 1) # zenity left the suggestions
+ d.name.setText("spdlog")
+ d.add_name()
+ d.deps.item(0).setSelected(True)
+ d.remove_selected()
+ self.assertEqual(listed, ["zenity", "spdlog"])
+ d.apply()
+ self.assertIn(("job-start", "fix-current"), remote.calls)
+ self.assertIsNone(tab.phantom_dialog)
+ self.assertIn("fix-current", hints_tab.HINT_JOBS) # its end opens the diff dialog
+
def test_hint_job_end_opens_diff(self):
job = dict(JOB, id="20260101-000000-hint-update", kind="hint-update", args=["nuclei=3.12.0"])
w, remote = self.window(jobs=({"jobs": [job]}, None),