diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md | 310 |
1 files changed, 310 insertions, 0 deletions
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 <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 +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=<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`. 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": "..."}` | +| `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" +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. +- `sbpull <target> [--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 <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 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: <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, 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 `<target>:/usr/local/bin/`, + create `/etc/sb-builder/config.toml` from the example only if missing. + Asks before deploying. A SlackBuild comes later. |
