GUI wizard for permanent rpm-ostree package layers with reboot + rollback, on bootc/image-based Linux systems
Find a file
Philip Peterson a50e2d7b5e Cap ninja parallelism to stop flutter run from OOMing
flutter_tools shells out to bare `ninja -C <dir> install` with no -j
flag, so ninja defaults to nproc+2 parallel compile/link jobs. On this
6-core/7.6GiB box that's enough concurrent clang jobs (sqlite3's
single-TU amalgamation is the worst offender) to trip systemd-oomd
well before swap fills up.

Add tools/ninja-jcap, a PATH-shadowing ninja shim that caps jobs via
NINJA_JOBS (default 2), and have the Taskfile prepend it inline in the
run command. Task's `env:` block can't override PATH itself (the OS
env wins), so the shim has to go on PATH via the shell command line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-10 10:27:11 -08:00
lib Add automated tests for HistoryService instead of throwaway scripts 2026-10-07 13:30:07 -08:00
linux Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
test Add automated tests for HistoryService instead of throwaway scripts 2026-10-07 13:30:07 -08:00
tools/ninja-jcap Cap ninja parallelism to stop flutter run from OOMing 2026-10-10 10:27:11 -08:00
.gitignore Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
.metadata Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
analysis_options.yaml Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
pubspec.lock Track our own layer history in a local SQLite DB 2026-10-06 10:30:59 -08:00
pubspec.yaml Track our own layer history in a local SQLite DB 2026-10-06 10:30:59 -08:00
README.md Document the app, and specifically how the post-reboot check works 2026-10-06 05:26:45 -08:00
Taskfile.yml Cap ninja parallelism to stop flutter run from OOMing 2026-10-10 10:27:11 -08:00

Layer Wizard

A GUI installer wizard for permanently layering an RPM package onto a bootc/image-based (read-only root) Linux system via rpm-ostree install, with a guided reboot step and a one-click rollback if the result looks wrong.

Built for Universal Blue / Fedora bootc systems, where plain dnf install refuses to run (the root filesystem is read-only) and the persistent mechanism is rpm-ostree install <pkg>, which stages a new deployment that only takes effect after a reboot.

What it does

  1. Welcome — shows the currently booted version/image.
  2. Package — enter a package name; "Check" confirms it resolves (dnf repoquery) before anything privileged happens.
  3. Confirm — review the package and what's about to happen.
  4. Apply — runs pkexec rpm-ostree install -y <package>, with the PolicyKit password prompt and a live streamed log. On success, offers Reboot Now / Reboot Later.
  5. After rebooting, the app launches itself automatically and asks: "Is everything working correctly?" — Yes, keep this update, or No, something's wrong — roll back, which runs pkexec bootc rollback --apply (reboots immediately into the previous deployment).

How the post-reboot check works

This system has no greenboot and no automated bad-boot detection (systemd-bless-boot.service exists but isn't wired up to anything here) — so the rollback offer is self-reported, not automatic. Two pieces make that work:

  • Autostart entry. Right after a successful install, ConfirmationMarker.installAutostart() (lib/confirmation_marker.dart) writes ~/.config/autostart/com.ironmagma.layer_wizard.desktop, with Exec= pointing at the currently-running binary (Platform.resolvedExecutable). KDE's session runs everything in ~/.config/autostart/ on login, which is what makes the app reappear on its own right after the reboot — you don't have to remember to open it.

  • The check itself runs on every launch, not just via autostart. lib/main.dart's _StartupRouter calls ConfirmationMarker.read() before deciding what to show:

    future: ConfirmationMarker.read(),
    builder: (context, snapshot) {
      final pending = snapshot.data;
      if (pending != null) return ConfirmationPage(pending: pending);
      return const WizardHomePage();
    }
    

    read() just checks whether ~/.local/state/layer_wizard/pending_confirmation.json exists and parses it. That file is written by the wizard right after a successful install (ConfirmationMarker.write(...) in lib/wizard_page.dart), and deleted — along with the autostart entry — as soon as you click Keep or Roll back in lib/confirmation_page.dart.

    Running the check unconditionally at startup (rather than only when launched via autostart) is deliberate: if autostart doesn't fire for some reason, or you dismiss it and reopen the app later, you still land on the confirmation screen instead of that pending state getting silently dropped.

Project layout

  • lib/ostree_service.dart — all privileged/unprivileged shelling out: rpm-ostree status --json, dnf repoquery, pkexec rpm-ostree install, pkexec bootc rollback --apply, pkexec systemctl reboot.
  • lib/confirmation_marker.dart — the pending-confirmation marker file and autostart .desktop entry (write/read/clear, install/remove).
  • lib/wizard_page.dart — the install wizard (Stepper-based).
  • lib/confirmation_page.dart — the post-reboot "did this work?" screen.
  • lib/main.dart — startup routing between the two.

Privilege escalation follows the same pattern as the sibling dnf-package-store app: pkexec through the desktop PolicyKit agent, with stdout/stderr streamed line-by-line into the UI.

Building

flutter build linux

Produces build/linux/x64/release/bundle/layer_wizard.