# 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. ### 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: - `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 ## 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 `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.