GUI wizard for permanent rpm-ostree package layers with reboot + rollback, on bootc/image-based Linux systems
Find a file
Philip Peterson b935ab01b2 Track our own layer history in a local SQLite DB
rpm-ostree/ostree only retain a couple of deployments, so there's no
durable record of what's been layered since the stock image. Add a
local history DB (lib/history/) keyed on the ostree deployment
checksum, with a parent pointer forming a tree, plus named "current"
and "last_known_safe" pointers.

Reconciling against live `rpm-ostree status --json` on every launch
means the history stays correct even across manual rpm-ostree/bootc
changes made outside this app: any booted checksum we don't recognize
gets recorded as a "detected" node rather than silently ignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:30:59 -08:00
lib Track our own layer history in a local SQLite DB 2026-10-06 10:30:59 -08:00
linux Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
test Initial commit: Layer Wizard 2026-10-06 05:25:05 -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

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.