From c4437a2c3bd0e9bfa6bd1adbf9cfe14898b26e0b Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 19 Oct 2026 18:49:53 +0200 Subject: Initial commit: sb-builder build VM GUI Desktop PyQt6 GUI plus a stdlib Python helper on the build VM, to drive slackrepo, mkhint and the overlay sync tools from one interface, with detachable jobs. This commit carries the repo docs and the approved phase 1 (core) design spec. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-19-sb-builder-phase1-design.md | 310 +++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md (limited to 'docs') diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md new file mode 100644 index 0000000..cf13c8c --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md @@ -0,0 +1,310 @@ +# sb-builder phase 1: core design + +Date: 2026-10-19 +Status: approved in brainstorming, pending spec review + +## 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`). +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. + +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 | 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 +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_smoke.py +``` + +### Transport + +Every GUI call is a QProcess running: + +``` +ssh -o ControlMaster=auto -o ControlPath=/sbb-%C -o ControlPersist=10m \ + sbb-remote [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`. A mismatch with the GUI's own +version shows a banner telling the user to run `./install.sh --remote`. + +## Helper: `sbb-remote` + +### State + +``` +/lock flock target, held for the whole job +/jobs//meta.json kind, args, argv, pid, start, end, exit, status +/jobs//log combined stdout+stderr, raw bytes +``` + +`` is `YYYYMMDD-HHMMSS-`. `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": "..."}` | +| `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 `/` 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 [args]` | validate; spawn `sbb-remote _run ` 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": ""}` after deleting the refused job's dir | +| `job-follow ` | write log bytes from `offset` to stdout, keep following until the job is no longer `running`, then exit 0 | +| `job-kill ` | SIGTERM to the job's process group; `_run` records `killed` | +| `_run ` | internal, see below | + +### `_run` + +1. Take a non-blocking `fcntl.flock` on `/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 [--yes\|--no]` | `bash -c '. "$1" && sbpull "$2" "$3"' _ ` | +| `sbshadow [--yes\|--no]` | `sbshadow ` | + +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" +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: : "}` and exits 1. + +### Errors + +Top-level handler catches any exception, prints `{"error": ""}`, +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. +- `sbpull [--yes|--no]`: passes the flag to `sbshadow`. + +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 --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 running job starts `job-follow` from the stored offset. On + stream drop: reconnect with backoff (2s doubling, max 30s) from the last + offset. +- `job-start` returning busy: dialog "busy: " 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 domstate ` 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, retry with backoff | +| 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_smoke.py`: `QT_QPA_PLATFORM=offscreen`, MainWindow with a + fake Remote; asserts tabs exist and the Dashboard renders a canned `stats` + dict, including `null` fields. +- 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`: installs the GUI locally (to `~/.local`, or system-wide as + root), copies `config.example.toml` to the config path only if no config + exists. +- `install.sh --remote`: scp `remote/sbb-remote` to `:/usr/local/bin/`, + create `/etc/sb-builder/config.toml` from the example only if missing. + Asks before deploying. A SlackBuild comes later. -- cgit v1.2.3