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:
- storage declarations are first-class Fruix objects
- materialization uses native FreeBSD tools
- the installer is a Fruix application
- the installed result records both declaration identity and realized storage metadata
- ordinary
reconfigure/switch/rollbackdo not repartition disks
Current starting point
Already in place:
- canonical
fruixrepo/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.
2. Runtime config and install-time layout are related but distinct
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:
gpartnewfsmountswapongeliwhen encryption is addedzpool/zfswhen ZFS support is addedmkimg/ 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.scmmodules/fruix/system/storage/validate.scmmodules/fruix/system/storage/render.scmmodules/fruix/system/freebsd/storage.scmor 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.scmmodules/fruix/installer/state.scmmodules/fruix/installer/steps.scmmodules/fruix/installer/final.scmmodules/fruix/installer/newt.scmmodules/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/rollbackmay 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 creategpart addnewfs_msdosor equivalent for EFI where needednewfsfor UFS rootswaponconfiguration- 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 installpath 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
newtif 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:
- welcome
- keyboard/console assumptions if needed
- target disk selection
- storage layout selection/edit
- hostname
- root/user setup
- network choice (optional/simple)
- summary/review
- install/apply
- 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
Recommended implementation order by milestone
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
fruixremains the source of truth fruix-bootstrapremains 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:
- write the v1 storage semantics document
- add the first base storage declaration model to canonical Fruix
- 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.