Files
fruix/docs/system-deployment-workflow.md

11 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
  • 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.scm on 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-file or block-device
  • install_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.iso
  • boot_efi_image=/frx/store/.../efiboot.img
  • root_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:

  1. edit the system declaration
  2. run fruix system build
  3. inspect emitted metadata
  4. if needed, produce one of:
    • rootfs
    • image
    • install
    • installer
    • installer-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.

  1. run fruix system image
  2. boot the image in QEMU or convert/import it for XCP-ng
  3. validate:
    • /run/current-system
    • shepherd/sshd state
    • activation log
  4. 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.

  1. run fruix system install --target ...
  2. let Fruix partition, format, populate, and install the target
  3. boot the installed result
  4. validate /var/lib/fruix/install.scm and 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.

  1. run fruix system installer
  2. boot the installer disk image
  3. let the in-guest installer run onto the selected target device
  4. 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.

  1. run fruix system installer-iso
  2. boot the ISO under the target virtualization path
  3. let the in-guest installer run onto the selected target device
  4. eject the ISO and reboot the installed target

This is now validated on both:

  • local QEMU/UEFI/TCG
  • the approved real XCP-ng VM 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-device as an explicit deployment parameter when moving between virtualization environments

Do not assume that a device name validated in one harness is portable to another.

Roll-forward workflow

The current Fruix roll-forward model is declaration-driven.

Canonical process:

  1. keep the current known-good system declaration
  2. prepare a candidate declaration
    • this may differ by FreeBSD base identity
    • source revision
    • services
    • users/groups
    • or other operating-system fields
  3. run fruix system build for the candidate
  4. materialize either:
    • fruix system image
    • fruix system install
    • fruix system installer
    • fruix system installer-iso
  5. boot or install the candidate
  6. 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:

  1. retain the earlier declaration that produced the known-good closure
  2. rebuild or rematerialize that earlier declaration
  3. redeploy or reboot that earlier artifact again

Concretely, the usual rollback choices are:

  • rebuild the earlier declaration with fruix system build and confirm the old closure path reappears
  • boot the earlier declaration again through fruix system image
  • reinstall the earlier declaration through fruix system install, installer, or installer-iso if 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

Future Phase 19 work is expected to make these more explicit:

  • current generation
  • previous generation
  • rollback target
  • installed-system roots and generation links
  • an operator-facing installed-system rollback workflow

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:

  • installed-system generation directories and symlink model
  • a dedicated switch or reconfigure command
  • an installed-system rollback command that moves between generations in place
  • explicit GC-root management for installed systems

Those are the next logical steps after Phase 19.1.

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 until explicit installed-system generation management is added.