7.3 KiB
Phase 19.3: installed-system rollback workflow on FreeBSD
Date: 2026-04-04
Goal
Phase 19.3 is about validating installed-system rollback through the intended operator-facing workflow, not only through host-side build/image redeploy harnesses.
The key question was:
- can an already-installed Fruix system move between recorded generations coherently, using an operator-facing command surface on the target itself?
Decision
The current Fruix solution is intentionally modest.
Fruix now provides a small installed-system helper on the target itself:
/usr/local/bin/fruix
Validated in-guest commands:
fruix system statusfruix system switch /frx/store/...-fruix-system-...fruix system rollback
Important scope choice:
switchassumes the candidate closure is already present on the target's/frx/store- Fruix does not yet fetch or transfer that closure onto the target automatically
That keeps Phase 19.3 focused on generation-state correctness rather than introducing a larger store-transfer story prematurely.
Implemented model
Installed systems now support the following validated operator pattern:
- build a candidate closure with the host-side Fruix frontend
- stage that closure into the installed system's
/frx/store - run:
fruix system switch /frx/store/...candidate...
- reboot into the candidate generation
- if needed, run:
fruix system rollback
- reboot into the recorded rollback generation
The installed system now records explicit rollback state under:
/var/lib/fruix/system/
current -> generations/N
current-generation
rollback -> generations/M
rollback-generation
generations/
...
and explicit rollback reachability under:
/frx/var/fruix/gcroots/
current-system -> /frx/store/...current...
rollback-system -> /frx/store/...rollback...
system-1 -> ...
system-2 -> ...
Code changes
modules/fruix/system/freebsd/render.scm
Added a generated in-guest Fruix deployment helper script under:
usr/local/bin/fruix
That helper now:
- reports installed-system state with
fruix system status - stages a new current generation with
fruix system switch - stages the recorded rollback generation with
fruix system rollback - updates:
/var/lib/fruix/system/current/var/lib/fruix/system/current-generation/var/lib/fruix/system/rollback/var/lib/fruix/system/rollback-generation/frx/var/fruix/gcroots/current-system/frx/var/fruix/gcroots/rollback-system/frx/var/fruix/gcroots/system-N
- refreshes the ESP bootloader file from the selected closure's
boot/loader.efi
A practical implementation detail mattered here:
- replacing
/run/current-systemwith a remove-then-recreate strategy caused the live shell environment to break while the link was absent - switching that update to an atomic symlink replacement path for
/run/current-systemavoided that gap and made the in-guest operator command reliable
modules/fruix/system/freebsd/media.scm
Updated installed rootfs staging so that installed targets expose:
/usr/local/bin/fruix -> /run/current-system/usr/local/bin/fruix
Also bumped the explicit generation-layout version from:
1to2
because the installed-system model now includes operator-driven switch/rollback state as part of the validated layout story.
modules/fruix/system/freebsd/model.scm
Updated generated-file metadata so the system closure records:
usr/local/bin/fruix
as part of the generated operating-system file set.
New validation harness
Added:
tests/system/run-phase19-installed-system-rollback-qemu.sh
This harness validates the actual installed-system operator flow on local QEMU/UEFI/TCG.
Validation flow
The harness now performs all of the following:
- installs a current system image directly to a target disk image
- builds a distinct candidate closure
- in the validated harness this differs by host name so the closure identity changes cleanly without needing a heavier base-version rebuild
- stages the candidate closure and its referenced store items into the installed target's
/frx/store - boots the installed current system
- validates initial state:
- current generation =
1 - current closure = current installed closure
- no rollback generation yet recorded
- current generation =
- runs:
fruix system switch /frx/store/...candidate...
- validates staged switch state:
- current generation =
2 - rollback generation =
1 - current closure = candidate closure
- rollback closure = original current closure
- generation 2 metadata/install files were written
- current generation =
- reboots and validates boot into the candidate closure
- runs:
fruix system rollback
- validates staged rollback state:
- current generation =
1 - rollback generation =
2 - current closure = original closure
- rollback closure = candidate closure
- current generation =
- reboots and validates boot back into the original current system
- confirms post-rollback service state:
fruix-shepherdrunningsshdrunning- activation log still shows success
Passing validation
Passing result:
PASS phase19-installed-system-rollback-qemu
Validated metadata summary:
current_closure_path=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
candidate_closure_path=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
current_host_name=fruix-freebsd-current
candidate_host_name=fruix-freebsd-canary
final_current_generation=1
final_current_closure=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
final_rollback_generation=2
final_rollback_closure=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
installed_system_switch=ok
installed_system_rollback=ok
Regression checks
After landing the installed-system switch/rollback workflow, the following regression checks still pass:
PASS phase19-generation-layout-qemuPASS phase18-installer-iso
That means the new in-guest generation-management path did not regress:
- the previously validated explicit generation layout
- or the UEFI installer ISO boot/install path
Relationship to Guix
This phase does not claim that Fruix now matches Guix's full installed-system UX.
What Fruix now has is:
- explicit generation state on disk
- explicit current/rollback pointers
- a minimal installed-system operator command surface
- validated switching and rollback between already-staged closures
What still remains compared with Guix:
- building/staging the candidate closure from inside the target system itself
- automatic closure transfer/fetch as part of
switch - a richer long-term generation lifecycle policy
Conclusion
Phase 19.3 is complete.
Fruix now validates an actual installed-system rollback workflow on FreeBSD:
- the target system itself can report current/rollback state
- it can switch to a staged candidate generation
- it can reboot into that candidate generation
- it can roll back to the recorded prior generation
- and it can reboot into the restored current system
That closes the Phase 19 deployment story from:
- documented deployment workflow
- to explicit generation layout
- to validated installed-system operator rollback behavior