12 KiB
Fruix system deployment workflow
Date: 2026-04-04
Purpose
This document defines the current canonical Fruix workflow for:
- building a declarative system closure
- materializing deployable artifacts
- installing a declarative system onto an image or disk
- booting through installer media
- rolling forward to a candidate system
- rolling back to an earlier declared system
This is the Phase 19.1 operator-facing view of the system model already implemented in earlier phases.
Core model
A Fruix system workflow starts from a Scheme file that binds an operating-system object.
Today, the canonical frontend is:
./bin/fruix system ...
The important output objects are:
- system closure
- a content-addressed store item under
/frx/store/*-fruix-system-<host-name> - includes boot assets, activation logic, profile tree, metadata, and references
- a content-addressed store item under
- rootfs tree
- a materialized runtime tree for inspection or image staging
- disk image
- a bootable GPT/UEFI raw disk image
- installer image
- a bootable Fruix installer disk image that installs a selected target system from inside the guest
- installer ISO
- a bootable UEFI ISO with an embedded installer mdroot payload
- install metadata
/var/lib/fruix/install.scmon installed targets- records the selected closure path, install spec, and referenced store items including source provenance
The current deployment story is therefore already declaration-driven and content-addressed, even before first-class installed-system generations are modeled more explicitly.
Canonical command surface
Build a system closure
sudo env HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
GUIX_SOURCE_DIR="$HOME/repos/guix" \
GUILE_BIN="/tmp/guile-freebsd-validate-install/bin/guile" \
GUILE_EXTRA_PREFIX="/tmp/guile-gnutls-freebsd-validate-install" \
SHEPHERD_PREFIX="/tmp/shepherd-freebsd-validate-install" \
./bin/fruix system build path/to/system.scm --system my-operating-system
Primary result:
closure_path=/frx/store/...-fruix-system-...
Use this when you want to:
- validate the declarative system composition itself
- inspect provenance/layout metadata
- compare candidate and current closure paths
- drive later rootfs/image/install steps from the same declaration
Materialize a rootfs tree
sudo env HOME="$HOME" ... \
./bin/fruix system rootfs path/to/system.scm ./rootfs --system my-operating-system
Primary result:
rootfs=...closure_path=/frx/store/...
Use this when you want to:
- inspect the runtime filesystem layout directly
- stage a tree for debugging
- validate
/run/current-system-style symlink layout without booting a full image
Materialize a bootable disk image
sudo env HOME="$HOME" ... \
./bin/fruix system image path/to/system.scm \
--system my-operating-system \
--root-size 6g
Primary result:
disk_image=/frx/store/.../disk.img
Use this when you want to:
- boot the system directly as a VM image
- test a candidate deployment under QEMU or XCP-ng
- validate a roll-forward or rollback candidate by image boot
Install directly to an image file or block device
sudo env HOME="$HOME" ... \
./bin/fruix system install path/to/system.scm \
--system my-operating-system \
--target ./installed.img \
--disk-capacity 12g \
--root-size 10g
Primary result:
target=...target_kind=raw-fileorblock-deviceinstall_metadata_path=/var/lib/fruix/install.scm
Use this when you want to:
- produce an installed target image without booting an installer guest
- validate installation mechanics directly
- populate a raw image or a real
/dev/...target
Materialize a bootable installer disk image
sudo env HOME="$HOME" ... \
./bin/fruix system installer path/to/system.scm \
--system my-operating-system \
--install-target-device /dev/vtbd1 \
--root-size 10g
Primary result:
installer_disk_image=/frx/store/.../disk.img
Use this when you want to:
- boot a Fruix installer environment as a disk image
- let the in-guest installer partition and install onto a second disk
- validate non-interactive installation from inside a booted Fruix guest
Materialize a bootable installer ISO
sudo env HOME="$HOME" ... \
./bin/fruix system installer-iso path/to/system.scm \
--system my-operating-system \
--install-target-device /dev/vtbd0
Primary result:
iso_image=/frx/store/.../installer.isoboot_efi_image=/frx/store/.../efiboot.imgroot_image=/frx/store/.../root.img
Use this when you want to:
- boot through UEFI ISO media instead of a writable installer disk image
- install from an ISO-attached Fruix environment
- test the same install model on more realistic VM paths
Deployment patterns
1. Build-first workflow
The default Fruix operator workflow starts by building the closure first:
- edit the system declaration
- run
fruix system build - inspect emitted metadata
- if needed, produce one of:
rootfsimageinstallinstallerinstaller-iso
This keeps the declaration-to-closure boundary explicit.
2. VM image deployment workflow
Use this when you want to boot a system directly rather than through an installer.
- run
fruix system image - boot the image in QEMU or convert/import it for XCP-ng
- validate:
/run/current-system- shepherd/sshd state
- activation log
- keep the closure path from the build metadata as the deployment identity
This is the current canonical direct deployment path for already-built images.
3. Direct installation workflow
Use this when you want an installed target image or disk without a booted installer guest.
- run
fruix system install --target ... - let Fruix partition, format, populate, and install the target
- boot the installed result
- validate
/var/lib/fruix/install.scmand target services
This is the most direct install path.
4. Installer-environment workflow
Use this when the install itself should happen from inside a booted Fruix environment.
- run
fruix system installer - boot the installer disk image
- let the in-guest installer run onto the selected target device
- boot the installed target
This is useful when the installer environment itself is part of what needs validation.
5. Installer-ISO workflow
Use this when the desired operator artifact is a bootable UEFI ISO.
- run
fruix system installer-iso - boot the ISO under the target virtualization path
- let the in-guest installer run onto the selected target device
- eject the ISO and reboot the installed target
This is now validated on both:
- local
QEMU/UEFI/TCG - the approved real
XCP-ngVM path
Install-target device conventions
The install target device is not identical across all boot styles.
Current validated defaults are:
- direct installer disk-image path under QEMU:
/dev/vtbd1
- installer ISO path under QEMU:
/dev/vtbd0
- installer ISO path under XCP-ng:
/dev/ada0
Therefore the canonical workflow is:
- always treat
--install-target-deviceas an explicit deployment parameter when moving between virtualization environments
Do not assume that a device name validated in one harness is portable to another.
Installed-system generation layout
Installed Fruix systems now record an explicit first-generation deployment layout under:
/var/lib/fruix/system
Current validated shape:
/var/lib/fruix/system/
current -> generations/1
current-generation
generations/
1/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm # present on installed targets
Installed systems also now create explicit GC-root-style deployment links under:
/frx/var/fruix/gcroots
Current validated shape:
/frx/var/fruix/gcroots/
current-system -> /frx/store/...-fruix-system-...
system-1 -> /frx/store/...-fruix-system-...
Important detail:
/run/current-systemstill points directly at the active closure path in/frx/store- the explicit generation layout therefore adds deployment metadata and retention roots without changing the already-validated runtime contract used by activation, rc.d wiring, and tests
Roll-forward workflow
The current Fruix roll-forward model is declaration-driven.
Canonical process:
- keep the current known-good system declaration
- prepare a candidate declaration
- this may differ by FreeBSD base identity
- source revision
- services
- users/groups
- or other operating-system fields
- run
fruix system buildfor the candidate - materialize either:
fruix system imagefruix system installfruix system installerfruix system installer-iso
- boot or install the candidate
- validate the candidate closure in the booted system
The important property is that the candidate closure appears beside the earlier one in /frx/store rather than mutating it in place.
Rollback workflow
The current canonical rollback workflow is also declaration-driven.
Today, rollback means:
- retain the earlier declaration that produced the known-good closure
- rebuild or rematerialize that earlier declaration
- redeploy or reboot that earlier artifact again
Concretely, the usual rollback choices are:
- rebuild the earlier declaration with
fruix system buildand confirm the old closure path reappears - boot the earlier declaration again through
fruix system image - reinstall the earlier declaration through
fruix system install,installer, orinstaller-isoif the deployment medium itself must change
This rollback story has already been validated at the closure/image/deployment level:
- side-by-side base-version coexistence in
/frx/store - roll-forward to a candidate closure
- rollback by rebuilding and booting the earlier declaration again
- validation on both local QEMU and the approved XCP-ng VM path
Important scope note
This is not yet the same thing as a first-class installed-system generation switch command.
Current rollback is:
- redeploy the earlier declaration again
What still remains for later Phase 19 work is making rollback itself operator-driven at the installed-system layer, rather than only declaration/redeploy driven.
Still pending:
- previous-generation tracking beyond the initial explicit generation-1 layout
- an explicit rollback target link distinct from
current - an operator-facing installed-system rollback workflow
- generation switching without full redeploy
Provenance and deployment identity
For any serious deployment or rollback decision, the canonical identity is not merely the host name. It is the emitted metadata:
closure_path- declared FreeBSD base/source metadata
- materialized source store paths
- install metadata at
/var/lib/fruix/install.scm - store item counts and reference lists
Operators should retain metadata from successful candidate and current deployments because Fruix already emits enough data to answer:
- which declaration was built
- which closure booted
- which source snapshot was materialized
- which target device or image was installed
Current limitations
The deployment workflow is now coherent, but it is not yet the final generation-management story.
Not yet first-class:
- a dedicated
switchorreconfigurecommand - an installed-system rollback command that moves between generations in place
- multi-generation retention and previous-generation tracking beyond generation 1
- generation switching policy independent of full redeploy
Those are the next logical steps after the current explicit-generation layout.
Summary
The current canonical Fruix deployment model is:
- declare a system in Scheme
- build the closure with
fruix system build - materialize the artifact appropriate to the deployment target
- boot or install that artifact
- identify deployments by closure path and provenance metadata
- roll back by rebuilding/redeploying the earlier declaration, not by mutating the current closure in place
That is the operator-facing workflow Fruix should document and use while installed-system generation switching remains more limited than Guix's mature in-place system-generation workflow.