Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/29-souveraine-updater.md
Fimeg 1b7a14e4c2 tasks: archive what shipped, say what is left, add fingerprint/haptics/camera
13 (grip) and 35 (sessiond deadlock) are done; both moved to archive/ with
closing notes. Index regrouped by state with a one-line what's-left per task
instead of a flat table of history; the archive prose moved to archive/README.

Corrected stale statuses: 29 shipped (read-only pending polkit), 31's component
and squeeze are live and only its contents are open, 42's layer 1 is built, 34
no longer blocks grip, 43 was numbered 42.

New: 44 fingerprint (DT + shim sat unused in staging/ since 07-07), 45 haptics
(settle which chip drives the motor before porting cs40l2x), 46 camera (drivers
and CAMSS are already in-tree; blueline's DT has no camera node).
2026-07-28 18:20:23 -04:00

12 KiB

TASK 29 — SouveraineOS Updater: finish it and put it through the pipeline

Status: shipped 2026-07-25; app rewritten 2026-07-26. Builds, runs, and installs on the phone by pacman -Syu from the edge archive. Left: it is read-only on the device — nothing in the session runs a polkit agent, so every privileged action refuses. See "What it still needs". Also owns TASK-42 layer 2: compare how old the newest thing in edge is against the branch head it tracks. That one comparison is what would have caught a publish that quietly stopped.

The Updater is a Rust/Qt rewrite of the GTK/Python pachub. New code, new name — it is not a fork to be maintained against upstream. It is also the first chance to build something into the pipeline instead of retrofitting it, which is the whole point of TASK-27.

Where it is now

~/Projects/souveraine-updater — fresh git repo, one commit (e678fd6), no remote, no Gitea repo, no CI. Copied out of ~/Projects/PacHub (which is a GitHub fork of mrks1469/PacHub and still holds the deleted Python app uncommitted; leave it alone as the reference).

Renamed throughout: crates updater-core + updater-ui, binary souveraine-updater, QML module com.souveraine.updater, cache dir ~/.cache/souveraine-updater, icon net.souveraine.Updater.svg.

~2,500 lines. updater-core (pacman/AUR/auth/cache/models) looks coherent and is plain Rust. updater-ui is the cxx-qt half and is where the work is.

What was already wrong, and is now fixed in that commit

  1. It could not build at all. updater-ui/Cargo.toml declared qmetaobject = "0.2.10" while build.rs called cxx_qt_build and main.rs used cxx_qt_lib — and there was no [build-dependencies] section. cxx-qt 0.9.1 was in the lockfile from some earlier state, so the lock lied about it resolving. Now: cxx, cxx-qt, cxx-qt-lib (qt_full), and build-deps cxx-qt-build + qt-build-utils, all 0.9 to match culver.

  2. PackageManager {} could never resolve in QML — the QObject had no #[qml_element]. Main.qml instantiates it directly, so the root component would have constructed to null.

  3. Every signal handler was dead. The bridge declared packages_loaded, operation_completed, detail_loaded; the QML listens on onPackagesLoaded, onOperationCompleted, onDetailLoaded. Without #[auto_cxx_name] those never bind — the same silent defect that cost culver its Matrix login on 2026-07-20. Added #[auto_cxx_name], which also camelCases invokables and properties, so all 46 pm.* references across the four QML files were converted (pm.load_packagespm.loadPackages). JSON field names from serde (build_date, disk_free, depends_on, …) were deliberately not touched — those follow the Rust struct fields.

  4. The UI was loaded from /usr/share/pachub/qml/*.qml by absolute path, with a cwd fallback. Replaced with a qrc module wired in build.rs (com.souveraine.updater), so the binary carries its own UI. main.rs now calls both init_crate! and init_qml_module! — both are required in a binary crate, and the aarch64 lld/LTO link drops them as unreferenced otherwise. Also added on_object_creation_failed so a null root reports the real reason instead of a guess. The hand-written qml/qmldir was deleted; cxx-qt-build generates the module's own.

Next: make it compile

Build on archdev. Never on the laptop — that includes cargo check and cargo clippy. Casey stopped a local lint run on 2026-07-25.

Unverified and likely to need work on first compile:

  • #[qproperty(String, …)] on thirteen properties. cxx-qt's QML-visible properties normally want QString; Rust String may not expose to QML even if it compiles. If QML reads empty strings, this is why — convert to QString and adjust PackageManagerRust.
  • The bridge does its pacman work synchronously inside invokables. full_upgrade and rate_mirrors will freeze the UI. updater-core::auth shells out via polkit; check what it does on a phone with no agent running.
  • The icon is referenced as qrc:/qt/qml/com/souveraine/updater/net.souveraine.Updater.svg but build.rs registers only .qml files, so it will not resolve. Either register it as a module resource or drop to a themed icon name.
  • The SVG is still mrks1469's artwork, renamed. Replace it before this is a first-party app.

Then: be the first repo built the right way

This is the piece that matters beyond the app. Nothing in ~/Projects except souveraine reaches the phone through a pipeline (TASK-27). Do not repeat that.

  • Create the Gitea repo Fimeg/souveraine-updater, default branch primary, push e678fd6 and onward.
  • CI on the archdev runner: cargo test, cargo clippy -- -D warnings, the no-ai-attribution guard (copy culver's), then a packaging step — the thing culver's CI still lacks. Build aarch64 + x86_64, repo-add --include-sigs --sign into souveraine-{arch}, publish to the edge release.
  • Publishing into edge needs the additive shape first (TASK-27 finding 3): souveraine's CI currently deletes and recreates that release, so a second producer's assets are erased on the next souveraine push. This is a hard prerequisite — shipping the Updater into edge before fixing it means the next souveraine commit silently deletes the Updater.
  • A PKGBUILD exists in ~/Projects/PacHub/PKGBUILD (pkgname pachub, installs loose QML to /usr/share/pachub/qml). It needs rewriting for the new name and the qrc build — the QML install loop is now wrong, since the UI is embedded.

What actually broke, 2026-07-25

Eight compile errors, then one runtime failure. In order:

  • #[qproperty(String, …)] — as predicted. cxx-qt has no String conversion across the property boundary; all thirteen are QString now.
  • apply_filter was declared on PackageManagerRust taking Pin<&mut ffi::PackageManager>, which is not a valid self type without the unstable arbitrary_self_types. Moved onto the QObject.
  • rust() / rust_mut() need use cxx_qt::CxxQtType.
  • QGuiApplication::exec wants the pinned app out of the UniquePtr.
  • The root still constructed to null after all of that. Main.qml instantiates PackageManager {} but never imported com.souveraine.updater. Loaded by its qrc URL, a file's implicit import is its directory — which resolves sibling .qml views but not a Rust-registered type, which belongs to the module. on_object_creation_failed is what surfaced it; without that hook this is a silent blank window. Culver imports its own module in every file for the same reason.
  • The icon URL resolved to nothing, as predicted. qrc_resources with a QmlModule set prefixes each path with the module import path, landing exactly on the URL Sidebar.qml already used, so the QML was left alone. The svg moved into updater-ui/ to keep one copy for both qrc and packaging.

CI ends in a smoke test that loads the QML root offscreen and fails on any stderr. A null root builds green — cargo build cannot see this class of bug, which is how it survived from the start.

Acceptance — met

  • Builds on archdev for aarch64; build-cross.sh asserts the binary is aarch64 and that every libQt6* it NEEDs resolves in the sysroot it linked against (TASK-27 finding 6: a mixed-ABI cross build otherwise looks healthy).
  • Runs on the phone: root constructs, 13,278 packages cached, and it lists itself as souveraine-updater 0.1.r1.ge4f00c5ffcc9-1 from repo Souveraine.
  • Installed by pacman from edge, signature verified against the archive key. All four files owned; nothing unowned.
  • pachub retired — conflicts/replaces removed it in the same transaction, taking /usr/share/pachub with it.
  • A souveraine push can no longer delete it (TASK-27 finding 3, fixed first).

Not verified: the on-screen window. Offscreen proves construction; the visible check is Casey's, at the device.

The app itself, 2026-07-26

The pipeline half shipped first; this is the program. 0.1.r7.g82b1429 on the phone.

  • The package list is a QAbstractListModel. PackageManager is the model and the controller — one QObject, rows as roles. The JSON string is gone.
  • Nothing touches pacman on the GUI thread. Every invokable hands off to a std::thread and comes back through CxxQtThread::queue. Privileged output streams into the log a line at a time while it runs.
  • The catalog cache is keyed to pacman's own databases (mtime of local/ plus each sync/*.db), so a -Syu in a terminal invalidates it. Age alone is still a 6h ceiling.
  • pacman -Ss . / -Qs . instead of -Sl. -Sl carries no description — its [installed] marker was landing in the description column. Installed version now wins over the offered one, and detail merges -Si with -Qi (install date, reason and reverse deps exist only in the local database).
  • Privileged calls take an argv. pacman -S --noconfirm ${name} through sh -c was a command-injection hole in the one program that then runs pacman as root.
  • Theme + drawn icons. Theme.qml is culver's palette; icons are stroked paths (Icons.qml, Glyph.qml). The phone image has no emoji font, so every 📦 / 🔍 / in the first build was a tofu box.
  • New first-party app icon, in the culver / player family.
  • 9 unit tests, up from zero.

Five things that had to be found the hard way:

  • Qt logs to the journal, not stderr, whenever JOURNAL_STREAM is set — and it always is under systemd, which the Gitea runner is. The CI smoke test was therefore blind to every QML error; it only ever saw main.rs's own line. QT_FORCE_STDERR_LOGGING=1 is now set there, with LC_ALL=C.UTF-8 so Qt's locale notice does not count as output.
  • font.families does not exist on the QML font value type in Qt 6.11. Assigning it makes the whole component unavailable. Theme picks one installed family with Qt.fontFamilies() instead.
  • AbstractButton.icon is FINAL — a subclass declaring property string icon fails at creation, which is a null root.
  • pm: pm binds a property to itself. The right-hand side resolves in the child's own scope first. Everything through it reads undefined.
  • A Control lays its contentItem into the content rect, so anchoring inside that rect fights the layout; and Dialog sizes its content from itself, so a wrapped paragraph in one is a binding loop.

What it still needs

  • No polkit agent runs in the phone session (verified 2026-07-26: pkexec --disable-internal-agent → "No authentication agent found"). So every privileged action — install, remove, upgrade, sync — fails. The Updater now says exactly that instead of reporting a bare failure, but the agent is the shell's to run, and until it does the app is read-only on the device.
  • rate-mirrors is not in the phone image; the action is hidden unless the binary exists. paccache and reflector are absent too.
  • No cancel: a running -Syu can be watched but not stopped.
  • AUR support was deleted with aur.rs — it was dead code that never parsed a version, and there is no helper on the phone. Foreign packages are still listed and upgraded by pacman; building from the AUR is not offered.
  • The on-screen window on the phone is still Casey's check. Both layouts are verified by headless render on archdev (Xvfb + the shipped binary).

Connects to

TASK-27 (pipeline; the edge clobber is the blocking prerequisite), TASK-25 (one repo, all packages), Pixel3Arch/pkgs/pachub (the old Python package — retire it when this lands, or the phone carries both).