GUI wizard for permanent rpm-ostree package layers with reboot + rollback, on bootc/image-based Linux systems
Find a file
Philip Peterson 5b674c122f
All checks were successful
RPM / rpm (arm64, runner-arm64-linux) (push) Successful in 4m42s
RPM / rpm (amd64, runner-x86-linux) (push) Successful in 1m33s
RPM: each architecture natively, on its own runner
arm64 builds on the new runner-arm64-linux instead of under emulation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 21:50:13 -09:00
.forgejo/workflows RPM: each architecture natively, on its own runner 2026-11-03 21:50:13 -09:00
lib Keep post-reboot confirmation page in place instead of restarting the wizard 2026-10-10 11:55:04 -08:00
linux Initial commit: Layer Wizard 2026-10-06 05:25:05 -08:00
packaging RPM: build with the shared quine-global/app-build action 2026-11-03 21:05:16 -09: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 Support installing a local .rpm file alongside name search 2026-10-10 11:54:59 -08:00
pubspec.yaml Support installing a local .rpm file alongside name search 2026-10-10 11:54: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 Add a build task, sharing the ninja-cap/PKG_CONFIG_PATH setup 2026-10-10 10:28:13 -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.