1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
# sb-builder phase 1: core design
Date: 2026-10-19
Status: approved
## Goal
One Qt6 desktop GUI to drive the SlackBuild tooling on the build VM
(`buildsystem`): slackrepo, mkhint, overlay sync (sbpull, sbshadow), full repo
rebuild, and SBo orphan discovery. Long-running work runs as jobs on the VM
that survive the GUI disconnecting, and the GUI can reattach to them later.
## Phases
Each phase gets its own spec, plan and implementation cycle.
1. **Core** (this spec): remote helper with the job model, GUI shell with
tabs, Dashboard, Jobs tab, VM control, config, notifications. Job kinds:
`sbpull`, `sbshadow`.
2. **Packages + slackrepo**: package browser over the large SBo-danix tree
(search and filter by name, category, overlay, maintainer, build status);
Build, Rebuild, Bulk update screens.
3. **Full repo rebuild**: `slackrepo_setup_SBo-danix.sh`, destructive, guarded.
4. **Hints**: mkhint browse, edit, update, create, `-F`, diff before save.
Requires non-interactive flags in mkhint (it has about 10 `read -p`
prompts and wraps slackrepo at `mkhint:1798`). Also: `--no-dl` must write
`DOWNLOAD="no"`, which slackrepo reads, instead of `NODOWNLOAD=yes`,
which it does not (keep reading old `NODOWNLOAD` lines for the NoDL column).
5. **Orphans**: SBo orphan discovery (`EMAIL="NOEMAIL"` in `.info`, the
`sbo-orphans` logic from slackbuilds-superrepo), including orphaned deps
in REQUIRES. Needs an SBo-stable tree, which is not on the VM today.
Dashboard tiles become entry points as their screens land: Packages opens
the package browser (phase 2, select packages to build there), Built the
build screen, Last pull the sync actions, Recent jobs the Jobs tab, SBo
orphans the orphans screen (phase 5).
Out of scope for all phases so far: overlay git operations (status, commit,
push in my-slackbuilds and friends).
## Stack
- GUI: Python 3 + PyQt6 (stock in Slackware -current, 6.11), including
`PyQt6.QtDBus` for notifications. No third-party packages.
- Helper: Python 3 stdlib only (VM runs 3.12, so `tomllib` is available).
- Config: TOML, read with `tomllib`.
- License: GPLv2-only.
## Architecture
```
desktop buildsystem (root)
+----------------------+ ssh (ControlMaster) +---------------------------+
| sb-builder (PyQt6) | ----------------------> | sbb-remote (py3 stdlib) |
| tabs: Dashboard, | sbb-remote <cmd> | stats / jobs / |
| Jobs (+later tabs) | <---------------------- | job-start / job-follow / |
| QProcess per call | JSON on stdout | job-kill, flock, setsid |
+----------------------+ | /var/lib/sb-builder/ |
| +---------------------------+
| virsh (local libvirt)
v
domain "buildsystem"
```
### Repository layout
```
sb_builder/__main__.py entry point, config load
sb_builder/config.py GUI config: defaults, load, validate
sb_builder/remote.py the only code that runs ssh; also virsh
sb_builder/mainwindow.py tabs, menu, status bar, notifications
sb_builder/dashboard.py Dashboard tab
sb_builder/jobs.py Jobs tab
sb_builder/fmt.py text formatting shared by the tabs
sb-builder launcher (python3 -m sb_builder)
remote/sbb-remote VM helper, single file
config.example.toml GUI config example
remote/config.example.toml helper config example
install.sh local install; --remote deploys helper to VM
tests/test_remote.py
tests/test_gui.py
```
### Transport
Every GUI call is a QProcess running:
```
ssh -o ControlMaster=auto -o ControlPath=<runtime dir>/sbb-%C -o ControlPersist=10m \
<ssh_options...> <target> sbb-remote <cmd> [args...]
```
Calls are async. ssh exit 255 means the connection failed. Exit 127 means the
helper is not installed.
### Trust boundary
The helper runs only a fixed table of job kinds. The GUI sends a kind name
plus arguments; the helper validates each argument and builds argv itself.
No shell strings and no paths cross ssh. Validation rules:
- unknown kind: rejected
- `sbpull` target: must be listed in the helper config `overlays`
- flags: only the ones the kind declares (`--yes`, `--no`)
- package names (phase 2 onward): `^[A-Za-z0-9._+-]+$`
### Version check
On connect the GUI runs `sbb-remote version`, which also returns the
configured `overlays` (the GUI builds its sbpull menu from them). A mismatch with the GUI's own
version shows a banner telling the user to run `./install.sh --remote`.
## Helper: `sbb-remote`
### State
```
<state_dir>/lock flock target, held for the whole job
<state_dir>/jobs/<id>/meta.json kind, args, argv, pid, start, end, exit, status
<state_dir>/jobs/<id>/log combined stdout+stderr, raw bytes
```
`<id>` is `YYYYMMDD-HHMMSS-<kind>`. `status` is one of `running`, `ok`,
`failed`, `killed`, `lost`, `busy`.
Logs are kept forever (`# ponytail:` comment in code; add pruning once disk
usage is visible on the Dashboard).
### Commands
All print one JSON object on stdout, except `job-follow`.
| command | behaviour |
|---|---|
| `version` | `{"version": "...", "overlays": [...]}` |
| `stats` | Dashboard data: load, uptime, disk free for repo and state dirs, package counts (total, personal, pentesting, shadowed = overlay packages also present in the pristine `current` branch, as sbshadow computes it), last sync per overlay (time of the newest commit touching `<overlay>/` in `repo_dir`), slackrepo summary from its sqlite DB (built, failed, never built). Any missing source yields `null` for that field, never an error |
| `jobs` | list of all jobs' meta. A job whose meta says `running` but whose pid is gone (`os.kill(pid, 0)` fails) is reported and rewritten as `lost` |
| `job-start <kind> [args]` | validate; spawn `sbb-remote _run <id>` with `start_new_session=True`, `stdin=/dev/null`, stdout/stderr to `/dev/null`; wait up to 3s for `_run` to write status; return `{"id": ...}`, or `{"busy": "<running id>"}` after deleting the refused job's dir |
| `job-follow <id> <offset>` | write log bytes from `offset` to stdout, keep following until the job is no longer `running`, then exit 0 |
| `job-kill <id>` | SIGTERM to the job's process group; `_run` records `killed` |
| `_run <id>` | internal, see below |
### `_run`
1. Take a non-blocking `fcntl.flock` on `<state_dir>/lock`. If held, write
status `busy` and exit.
2. Start the job argv with `stdin=/dev/null`, stdout and stderr to the log
file, in its own process group. Write `pid` and status `running`.
3. Wait. Write `end`, `exit` and final status (`ok` on 0, `killed` on
SIGTERM, else `failed`).
Jobs never read input. A tool that prompts anyway gets EOF on `read` and,
under `set -e`, exits non-zero: the job fails, it never hangs and it never
answers yes by accident. Destructive steps are confirmed in the GUI.
### Phase 1 job kinds
| kind | argv built by helper |
|---|---|
| `sbpull <overlay> [--yes\|--no]` | `bash -c '. "$1" && sbpull "$2" "$3"' _ <sbpull_script> <overlay> <flag>` |
| `sbshadow [--yes\|--no]` | `sbshadow <flag>` |
The flag is required in both kinds (a job has no stdin).
### Helper config: `/etc/sb-builder/config.toml`
```toml
state_dir = "/var/lib/sb-builder"
repo_dir = "/var/lib/sbopkg/SBo-danix"
upstream_branch = "current"
slackrepo_db = "/var/lib/slackrepo/SBo-danix/database_SBo-danix_x86_64.sqlite3"
sbpull_script = "/usr/local/share/slackrepo_setup/sbpull.bash"
overlays = ["personal", "pentesting"]
```
Missing file: these defaults. Unknown key or wrong type: every command
returns `{"error": "config: <key>: <reason>"}` and exits 1.
### Errors
Top-level handler catches any exception, prints `{"error": "<message>"}`,
exits 1.
## Changes in slackrepo_setup
Done in that repo, its own commit, redeployed to the VM.
- `sbshadow --yes | --no`: no flag keeps the current interactive prompt.
`--yes` applies without asking. `--no` prints the plan and leaves the tree
unchanged (preview). `mkhint -F` runs in all cases, as it does today on
abort.
- `sbshadow` honours `SBO_REPO` to override the repo path, so it can be
tested against a throwaway repo.
- `sbpull <target> [--yes|--no]`: passes the flag to `sbshadow`, and returns
`sbshadow`'s exit status (today it returns `popd`'s, so a failure would
show as a successful job).
GUI flow: `sbpull personal --no` pulls and shows the shadow plan in the log;
the Jobs tab offers "Apply shadowing", which starts `sbshadow --yes`.
## GUI
### Main window
- QTabWidget. Dashboard is pinned. Other tabs are closable and
single-instance.
- Menu: `File > Reload config`, `File > Quit`, `Open > Jobs` (later phases
add entries).
- Status bar: connection state (ok, down, VM off, VM paused, VM running but
ssh unreachable, helper version mismatch), running job if any (click opens
it in Jobs), transient messages.
### Dashboard
Grid of QGroupBox tiles filled from `stats`, plus `jobs` for the Jobs tile:
- VM: load, uptime, disk free
- Repo: total packages, personal, pentesting, shadowed
- slackrepo: built, failed, never built
- Sync: last pull per overlay
- Jobs: running job and last 10 (status, duration)
- Orphans: placeholder tile, filled in phase 5
Refreshes every `refresh.dashboard` seconds while visible, plus a Refresh
button. `null` fields show "n/a". When the connection is down the last
values stay, greyed, with "stale since HH:MM".
### Jobs tab
- Left: job list (status icon, kind and args, start, duration), refreshed
every `refresh.jobs` seconds.
- Right: log view (QPlainTextEdit, ANSI escapes stripped, `\r` rewrites the
current line), Kill button with confirm.
- Toolbar `Start`: one entry per overlay for `sbpull <overlay> --no`, plus
`sbshadow --no`, `sbshadow --yes` (confirm dialog).
- After a `--no` run whose log lists `shadow:` or `unshadow:` lines, an
"Apply shadowing" button starts `sbshadow --yes`.
- Selecting a job starts `job-follow` from offset 0. If the stream drops
while the job is still running, the next successful `jobs` poll restarts
it from the last received offset (the poll itself backs off while the VM
is down, see Error handling).
- `job-start` returning busy: dialog "busy: <id>" with [Show job].
### Notifications
freedesktop `org.freedesktop.Notifications.Notify` via QtDBus. Events:
`finished`, `failed`, `killed`, `lost`, and `vm-up` (VM started and ssh
answering). Clicking a notification (`ActionInvoked`) raises the window and
opens the job. The GUI tracks the job states it has seen, so each job end
notifies once.
### VM control
When ssh fails and `vm.domain` is set, the GUI runs
`virsh -c <libvirt_uri> domstate <domain>` locally:
| domstate | behaviour |
|---|---|
| `shut off` | status "VM off", [Start VM] (automatic when `vm.autostart`): `virsh start`, then poll ssh with backoff, notify `vm-up` |
| `paused` | status "VM paused", [Resume]: `virsh resume` |
| `running` | status "VM running, ssh unreachable", keep retrying |
| virsh missing or error | plain "down" |
No shutdown control.
### GUI config: `$XDG_CONFIG_HOME/sb-builder/config.toml`
```toml
[remote]
target = "buildsystem" # any ssh destination
ssh_options = [] # extra ssh args; ControlMaster options always added
[refresh]
dashboard = 30
jobs = 5
[notify]
enabled = true
events = ["finished", "failed", "killed", "lost", "vm-up"]
[vm]
libvirt_uri = "qemu:///system"
domain = "buildsystem" # empty disables VM control
autostart = false
```
Missing file: these defaults. Invalid key or type: dialog naming the key,
then defaults are used. Read at startup and on `File > Reload config`. No
settings dialog yet.
## Error handling summary
| case | behaviour |
|---|---|
| ssh down | status bar, VM check, stale tiles; the `jobs` poll interval doubles from `refresh.jobs` up to 30s, and resets once ssh answers |
| helper missing or version mismatch | banner with `./install.sh --remote` |
| busy | dialog with [Show job] |
| follow drop | reconnect from offset, job unaffected |
| helper exception | `{"error"}` shown in status bar |
| `_run` died, VM rebooted mid-job | `lost`, red in list, notification |
| config invalid | dialog naming key, defaults used |
## Testing
stdlib `unittest`, no frameworks.
- `tests/test_remote.py`: imports `remote/sbb-remote` as a module with a
temp `state_dir`. Covers argument validation (unknown kind, overlay not in
config, unknown flag), lock busy, `lost` detection, `job-follow` from an
offset, and a full `_run` of a fake kind patched into the kind table
(ok, failed, killed).
- `tests/test_gui.py`: config load and validation, `remote.parse`, GUI and
helper versions equal, both example configs equal the defaults. Smoke tests
under `QT_QPA_PLATFORM=offscreen` with a fake Remote: tabs, Dashboard from
a canned `stats` dict with `null` fields, job-end notification sent once,
VM-off offer, carriage-return handling in the log view.
- slackrepo_setup: sbshadow `--yes` and `--no` verified in a throwaway git
repo, the same way the current behaviour was tested.
- First run against the real VM: manual, with the user.
## Install
- `install.sh`: symlinks the `sb-builder` launcher into `~/bin` (`BINDIR`
overrides), so repo edits are live; copies `config.example.toml` to the
config path only if no config exists.
- `install.sh --remote`: scp `remote/sbb-remote` to `<target>:/usr/local/bin/`
(`SBB_TARGET`, default `buildsystem`),
create `/etc/sb-builder/config.toml` from the example only if missing.
Asks before deploying. A SlackBuild comes later.
|