Files
fruix/docs/PLAN_INSTALLER.md

17 KiB

PLAN_INSTALLER

Summary

Yes: this paints a consistent picture.

The proposed Fruix installer direction is:

  • FreeBSD-native underneath for disk, filesystem, boot, and target-system materialization.
  • Fruix-native above for declaration generation, install metadata, identity/pinning, and long-lived product behavior.
  • Guix-like installer structure for the interactive installer itself: a Scheme program, organized as explicit steps, with a small Newt TUI.
  • Disko-like storage modeling as part of the base Fruix system model, not as an installer-only ad hoc script layer.

That yields a coherent split:

  1. storage declarations are first-class Fruix objects
  2. materialization uses native FreeBSD tools
  3. the installer is a Fruix application
  4. the installed result records both declaration identity and realized storage metadata
  5. ordinary reconfigure / switch / rollback do not repartition disks

Current starting point

Already in place:

  • canonical fruix repo/channel exists
  • bootable installer image and installer ISO paths exist
  • installer ISO boot path has been validated
  • minimal scripted install flow exists
  • installed nodes are already real Fruix nodes for build/reconfigure/build-base/deploy/switch/rollback

So this plan is not starting from zero. It starts from a validated non-interactive installer artifact pipeline and layers a first-class storage model plus an interactive installer UX on top.

Design principles

1. Storage layout belongs in base Fruix

Fruix should gain a first-class storage declaration layer that can be used by:

  • installer TUI
  • non-interactive fruix system install
  • disk image generation
  • installer image generation
  • future remote install / install-on-deploy flows

This should not live only inside the installer.

Fruix should persist both:

  • the declared storage intent
  • the realized runtime mount/boot config
  • the realized install metadata

But routine system generation operations should not automatically mutate partition tables.

3. Keep the execution substrate FreeBSD-native

Actual realization should shell out to native FreeBSD tools such as:

  • gpart
  • newfs
  • mount
  • swapon
  • geli when encryption is added
  • zpool / zfs when ZFS support is added
  • mkimg / image tooling where applicable
  • existing Fruix install/materialization helpers

4. Keep installer logic in canonical fruix

The installer TUI, step logic, declaration generation, and install semantics belong in canonical fruix, not fruix-bootstrap.

5. Prefer a simple network story first

Unlike Guix-on-Linux, Fruix-on-FreeBSD should not assume ConnMan.

First version should keep networking deliberately simple:

  • optional networking
  • wired-first
  • DHCP-first
  • manual static configuration only if needed
  • no Wi-Fi manager requirement in v1
  • no long-lived network-management daemon dependency for the installer UX

Non-goals for v1

The first interactive Fruix installer should not try to solve all storage and deployment problems.

Out of scope for the first version:

  • automatic storage mutation during ordinary fruix system reconfigure
  • post-install partition resizing/migration engine
  • full multi-disk orchestration
  • Wi-Fi-first installer UX
  • exhaustive ZFS layout support on day one
  • polished remote install orchestration from the installer itself
  • replacing the existing validated scripted install path immediately

Target architecture

Layer 1: Fruix storage declaration model

Add a base Fruix storage model that represents:

  • target disk(s)
  • partition table type
  • partitions
  • labels
  • filesystem type
  • mount points
  • swap
  • optional boot partition details
  • optional encryption layer
  • later: ZFS/UFS specialization and multi-disk layouts

This should be declarative and serializable into metadata.

Suggested module direction:

  • modules/fruix/system/storage/model.scm
  • modules/fruix/system/storage/validate.scm
  • modules/fruix/system/storage/render.scm
  • modules/fruix/system/freebsd/storage.scm or equivalent backend module

Generic storage intent should stay separate from FreeBSD-specific realization.

Layer 2: FreeBSD materialization backend

Add a backend that turns storage declarations into explicit, ordered actions:

  • probe disks
  • validate target device selection
  • create partition table
  • create partitions
  • make filesystems
  • enable swap
  • mount target tree
  • render boot/runtime config files
  • populate/install Fruix system closure
  • install boot support

This should look more like Disko's “generate apply scripts from declarations” than like ad hoc shell embedded throughout the installer.

Important: the backend should support both:

  • dry-run / plan rendering
  • real application

Layer 3: Fruix installer application

Add an installer application in Scheme, structured more like Guix's installer:

  • explicit installer state object
  • ordered steps
  • step-local validation
  • generated final declaration
  • final install/apply step

Suggested module direction:

  • modules/fruix/installer.scm
  • modules/fruix/installer/state.scm
  • modules/fruix/installer/steps.scm
  • modules/fruix/installer/final.scm
  • modules/fruix/installer/newt.scm
  • modules/fruix/installer/newt/*.scm

Layer 4: Metadata and persistence

Persist:

  • the selected/generated Fruix system declaration
  • the selected/generated storage declaration
  • realized disk/filesystem identifiers
  • install target details
  • Fruix channel identity / origin / revision where available
  • installer version / plan version

Record this both:

  • in closure metadata where appropriate
  • in installed-system state under Fruix-managed metadata paths

Proposed commands and behavior

Near-term expected command surface:

  • existing fruix system installer
  • existing fruix system installer-iso
  • existing fruix system install
  • new internal or public storage application entrypoint
  • installer TUI launched inside Fruix installer media

Possible later public storage commands:

  • fruix system storage-plan ...
  • fruix system storage-apply ...
  • fruix system install --layout FILE ...

The exact CLI naming can be deferred. The important part is the architecture: a first-class storage layer reused by the installer and non-interactive install paths.

Step-by-step implementation plan

Step 1: Write down the storage semantics before coding

Define the product semantics for Fruix storage declarations.

Deliverables:

  • document the distinction between:
    • declared storage intent
    • realized storage metadata
    • runtime mount/boot configuration
  • document the safety boundary:
    • installer/install actions may partition/format
    • reconfigure / switch / rollback may not partition/format by default
  • define v1 supported layouts

Recommended v1 supported layouts:

  • single-disk GPT
  • EFI system partition
  • UFS root filesystem
  • optional swap partition
  • optional separate /var

Exit criteria:

  • Fruix has a stable written storage model contract before implementation starts

Step 2: Add the base storage declaration model to canonical Fruix

Implement first-class storage records and constructors.

Deliverables:

  • generic storage record types
  • normalization and validation helpers
  • deterministic metadata rendering
  • support for embedding storage layout in or alongside operating-system declarations

Requirements:

  • must be reusable by installer, image builder, and non-interactive install
  • must not hard-code installer UI concerns into the model
  • must preserve future room for ZFS/encryption/multi-disk extensions

Exit criteria:

  • a declaration can represent a concrete install target layout without performing side effects

Step 3: Add a FreeBSD storage realization backend

Implement the backend that turns storage declarations into ordered materialization operations.

Deliverables:

  • disk probing helpers
  • validation of target devices
  • plan generation
  • dry-run text rendering
  • actual apply path that shells out to FreeBSD tools

The backend should initially support:

  • gpart create
  • gpart add
  • newfs_msdos or equivalent for EFI where needed
  • newfs for UFS root
  • swapon configuration
  • mount tree creation
  • target population handoff into existing Fruix install/materialization code

Prefer explicit, testable helper functions over one giant shell script.

Exit criteria:

  • Fruix can take a storage declaration and non-interactively realize it onto a raw disk/image using native FreeBSD tooling

Step 4: Rework non-interactive install to use the storage layer

Refactor fruix system install so it consumes the new storage declaration layer rather than ad hoc installer-only disk logic.

Deliverables:

  • fruix system install path routed through the shared storage backend
  • existing installer image / installer ISO install path updated to use the same backend
  • no duplication of partitioning logic between installer and CLI install path

Exit criteria:

  • one shared storage/apply path is used by both scripted install and future TUI install

Step 5: Persist storage declaration and realized layout metadata

Add explicit metadata capture.

Deliverables:

  • declared storage layout snapshot in system metadata
  • realized partition/filesystem metadata snapshot after install
  • linkage from installed system generation to install provenance
  • clear versioning of metadata schema

Examples of useful recorded data:

  • target device used at install time
  • partition labels and filesystem types
  • UUIDs / provider names discovered after creation
  • boot partition details
  • install timestamp
  • Fruix declaration identity and source origin

Exit criteria:

  • Fruix can answer both “what layout was intended?” and “what was actually installed?”

Step 6: Package and validate the Newt stack for Fruix on FreeBSD

Before building the real TUI, make the UI dependency story explicit.

Deliverables:

  • package definition for newt if not already available through Fruix inputs
  • package definition or integration path for Guile bindings to Newt
  • builder/runtime validation on FreeBSD
  • minimal proof-of-life TUI program in Fruix

Notes:

  • target end state is a Guix-like Scheme + Newt installer structure
  • if Guile bindings need bring-up work on FreeBSD, do that here
  • avoid coupling installer design to bootstrap repo specifics

Exit criteria:

  • Fruix can run a tiny Scheme/Newt UI inside a FreeBSD Fruix environment

Step 7: Build the installer state machine and step framework

Implement the Guix-like installer structure.

Deliverables:

  • installer state object
  • step descriptors
  • step ordering and back/next flow
  • step-local validation
  • final rendering of generated declaration(s)

Suggested initial steps:

  1. welcome
  2. keyboard/console assumptions if needed
  3. target disk selection
  4. storage layout selection/edit
  5. hostname
  6. root/user setup
  7. network choice (optional/simple)
  8. summary/review
  9. install/apply
  10. completion/reboot/shell

Exit criteria:

  • installer logic exists independently of the final widgets used for each step

Step 8: Implement the first small Newt TUI

Build the actual interactive UI on top of the step framework.

Deliverables:

  • main installer entrypoint launched by Fruix installer media
  • Newt forms/widgets for each initial step
  • summary screen showing:
    • chosen Fruix system settings
    • chosen storage layout
    • clear destructive-action warnings
  • failure screen with logs/shell escape path

Requirements:

  • keep it small and reliable
  • prefer clear text prompts over elaborate UI behaviors
  • always provide a shell/log escape hatch during install

Exit criteria:

  • a user can interactively choose a v1 layout and reach a final review screen

Step 9: Keep networking simple and explicit

Implement only a minimal network story for the installer.

Deliverables:

  • detect interfaces
  • optional “configure wired networking” step
  • DHCP-first support via native FreeBSD tools
  • optional manual static configuration if easy to support cleanly
  • no ConnMan dependency
  • network may be entirely skipped when installing from fully prebuilt local artifacts

Likely implementation approach:

  • use ifconfig, dhclient, route, and resolver file updates as needed
  • treat networking as a helper step, not as a central installer subsystem

Exit criteria:

  • installer can fetch or validate network reachability when needed without adopting a heavy network-management stack

Step 10: Wire installer apply step to the shared backend

Connect the TUI's final “install” action to the already-implemented shared storage and install backend.

Deliverables:

  • summary confirmation before destructive actions
  • apply progress view
  • disk layout realization
  • target system population
  • boot setup
  • metadata persistence
  • success/failure handling

Requirements:

  • no TUI-specific partitioning logic
  • all real disk mutation goes through the shared backend
  • logs remain available for debugging and validation

Exit criteria:

  • interactive and non-interactive installs are two frontends to the same Fruix install engine

Step 11: Update installer media to launch the new Fruix installer app

Promote the TUI from an internal component to the default installer UX.

Deliverables:

  • installer image launches Fruix installer application by default
  • shell remains available as an escape hatch
  • installer ISO/test paths updated accordingly
  • existing scripted fallback remains available during transition if needed

Exit criteria:

  • booting the Fruix installer media starts the Fruix Newt installer by default

Step 12: Add end-to-end validation harnesses

Extend the existing validation style with real installer coverage.

Deliverables:

  • syntax/unit validation for storage declarations and backend rendering
  • non-interactive storage apply tests on raw disk images
  • installer media boot test that reaches the TUI
  • automated or semi-automated install test for the v1 layout
  • real XCP-ng validation for the full install path
  • regression coverage for installed-node boot/switch/rollback after installation

Suggested validation milestones:

  • build storage declaration -> render plan -> apply to raw image
  • installer image boots -> Newt UI launches
  • interactive or scripted test chooses default v1 layout -> install completes
  • installed system boots and behaves as a real Fruix node

Exit criteria:

  • the installer is validated as a product workflow, not only as a boot artifact

Step 13: Expand storage capabilities after the v1 path is solid

Once the basic path is validated, add richer layouts incrementally.

Next candidates:

  • ZFS root layouts
  • encryption layer
  • separate /home
  • mirrored boot/root layouts
  • multi-disk declarations
  • image-builder reuse of the same storage declaration object

Rule:

  • every new layout feature must extend the shared storage model/backend rather than adding installer-only special cases

Milestone A: base storage model

  • Step 1
  • Step 2
  • Step 3

Milestone B: shared install engine

  • Step 4
  • Step 5

Milestone C: UI substrate

  • Step 6
  • Step 7

Milestone D: first interactive installer

  • Step 8
  • Step 9
  • Step 10
  • Step 11

Milestone E: validation and expansion

  • Step 12
  • Step 13

Concrete v1 scope recommendation

To keep the first Fruix installer manageable, v1 should target exactly this workflow:

  • boot Fruix installer ISO
  • choose target disk
  • choose default GPT layout
  • create:
    • EFI partition
    • root UFS partition
    • optional swap
  • enter hostname and initial credentials
  • optionally bring up wired DHCP
  • install prebuilt Fruix system artifact
  • record Fruix declaration + storage metadata
  • reboot into installed Fruix node

That is enough to prove the full architecture without getting stuck in storage-combinator explosion.

Consistency check

This plan is internally consistent with current Fruix direction because it keeps the existing product boundaries intact:

  • canonical fruix remains the source of truth
  • fruix-bootstrap remains only a builder-preparation layer
  • installer logic becomes a Fruix feature, not a bootstrap feature
  • storage layout becomes a reusable Fruix declaration, not a one-off install script
  • FreeBSD-specific disk actions remain native and practical
  • installed systems remain real Fruix nodes after boot

Immediate next action

The recommended next implementation task is:

  1. write the v1 storage semantics document
  2. add the first base storage declaration model to canonical Fruix
  3. route existing non-interactive install paths through that shared model/backend before building the Newt UI

That sequencing prevents the TUI from hard-coding one-off install logic and keeps the installer aligned with long-lived Fruix system behavior.