Files
fruix/docs/reports/phase19-installed-system-rollback-freebsd.md

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 status
  • fruix system switch /frx/store/...-fruix-system-...
  • fruix system rollback

Important scope choice:

  • switch assumes 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:

  1. build a candidate closure with the host-side Fruix frontend
  2. stage that closure into the installed system's /frx/store
  3. run:
    • fruix system switch /frx/store/...candidate...
  4. reboot into the candidate generation
  5. if needed, run:
    • fruix system rollback
  6. 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-system with 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-system avoided 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:

  • 1 to 2

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:

  1. installs a current system image directly to a target disk image
  2. 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
  3. stages the candidate closure and its referenced store items into the installed target's /frx/store
  4. boots the installed current system
  5. validates initial state:
    • current generation = 1
    • current closure = current installed closure
    • no rollback generation yet recorded
  6. runs:
    • fruix system switch /frx/store/...candidate...
  7. 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
  8. reboots and validates boot into the candidate closure
  9. runs:
    • fruix system rollback
  10. validates staged rollback state:
    • current generation = 1
    • rollback generation = 2
    • current closure = original closure
    • rollback closure = candidate closure
  11. reboots and validates boot back into the original current system
  12. confirms post-rollback service state:
    • fruix-shepherd running
    • sshd running
    • 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-qemu
  • PASS 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