diff --git a/README.md b/README.md index e8cfed4..19e9a1a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,56 @@ -# Fruix +# Fruix Bootstrap -Fruix is a Guix-like system running on FreeBSD but not GNU, using Shepherd, building GNU packages, with a BSD userland, and a functional store similar to Nix but not Nix. +`fruix-bootstrap` is the foreign-FreeBSD bring-up layer for Fruix. -In Fruix, the FreeBSD platform is represented as foundational store artifacts and updated through the same generation mechanism as the rest of the system. +Its job is to turn a plain FreeBSD installation into a **Fruix builder**: an environment capable of evaluating and materializing a pinned Fruix checkout. -Fruix is a system where everything that exists on the machine exists for a reason that can be explained. +## What belongs here -Every Fruix system must remain fully understandable and recoverable using only text files, a shell, and standard system tools. +Bootstrap-only responsibilities such as: -- Every host has a local config repository. -- Every host has a persistent system identity key. -- Every applied change corresponds to a commit and a generation. -- Secrets are declared in config but realized only at runtime. -- Secrets are encrypted to explicit recipients derived from host/user identity. -- Services explicitly declare their secret dependencies. -- The orchestration layer operates only through these primitives. +- host environment checks +- locating or building bootstrap Guile / support bits +- wrapper entrypoints that invoke a pinned Fruix checkout +- validation of the path from vanilla FreeBSD to first Fruix artifact + +## What does not belong here + +Long-lived Fruix product logic should live in the canonical `fruix` repo/channel instead: + +- package definitions +- system definitions +- installer logic +- deployment logic +- installed-node lifecycle behavior +- long-lived metadata semantics + +## Default channel checkout + +Bootstrap currently assumes a sibling canonical Fruix checkout at: + +- `../fruix` + +and bakes in the original channel repo URL as the default origin reference: + +- `https://git.teralink.net/self/fruix.git` + +Override the checkout location with: + +- `FRUIX_CHANNEL_DIR=/path/to/fruix` + +## Design docs + +- `docs/bootstrap.md` — boundary definition +- `docs/PLAN_5.md` — migration plan + +## Intended lifecycle + +```text +plain FreeBSD + -> fruix-bootstrap + -> Fruix builder + -> pinned fruix checkout + -> build installer ISO / VM image / system closure + -> booted Fruix node + -> future lifecycle managed by fruix +``` diff --git a/bin/fruix b/bin/fruix index 688bd9b..8274699 100755 --- a/bin/fruix +++ b/bin/fruix @@ -1,12 +1,32 @@ #!/bin/sh set -eu -project_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +bootstrap_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +default_channel_dir=$bootstrap_root/../fruix +if [ -d "$default_channel_dir" ]; then + default_channel_dir=$(CDPATH= cd -- "$default_channel_dir" && pwd) +fi + +fruix_channel_dir=${FRUIX_CHANNEL_DIR:-$default_channel_dir} +fruix_channel_url=${FRUIX_CHANNEL_URL:-https://git.teralink.net/self/fruix.git} guix_source_dir=${GUIX_SOURCE_DIR:-"$HOME/repos/guix"} guile_bin=${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile} guile_extra_prefix=${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install} shepherd_prefix=${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install} -script=$project_root/scripts/fruix.scm +script=$fruix_channel_dir/scripts/fruix.scm +modules_dir=$fruix_channel_dir/modules + +if [ ! -d "$fruix_channel_dir" ]; then + echo "Fruix channel checkout not found: $fruix_channel_dir" >&2 + echo "Set FRUIX_CHANNEL_DIR or clone $fruix_channel_url" >&2 + exit 1 +fi + +if [ ! -f "$script" ] || [ ! -d "$modules_dir" ]; then + echo "Fruix channel checkout is missing scripts/ or modules/: $fruix_channel_dir" >&2 + echo "Expected canonical Fruix content from $fruix_channel_url" >&2 + exit 1 +fi if [ ! -x "$guile_bin" ]; then echo "Guile binary is not executable: $guile_bin" >&2 @@ -20,11 +40,11 @@ ensure_built() { GUILE_EXTENSIONS_PATH="$guile_extra_prefix/lib/guile/3.0/extensions${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \ LD_LIBRARY_PATH="$guile_extra_prefix/lib:/tmp/guile-freebsd-validate-install/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ "$guile_bin" -c '(catch #t (lambda () (use-modules (fibers)) (display "ok") (newline)) (lambda _ (display "missing") (newline)))' | grep -qx ok; then - METADATA_OUT= ENV_OUT= "$project_root/tests/shepherd/build-local-guile-fibers.sh" + METADATA_OUT= ENV_OUT= "$bootstrap_root/tests/shepherd/build-local-guile-fibers.sh" fi if [ ! -x "$shepherd_prefix/bin/shepherd" ] || [ ! -x "$shepherd_prefix/bin/herd" ]; then - METADATA_OUT= ENV_OUT= GUILE_EXTRA_PREFIX="$guile_extra_prefix" "$project_root/tests/shepherd/build-local-shepherd.sh" + METADATA_OUT= ENV_OUT= GUILE_EXTRA_PREFIX="$guile_extra_prefix" "$bootstrap_root/tests/shepherd/build-local-shepherd.sh" fi } @@ -34,9 +54,9 @@ guile_prefix=$(CDPATH= cd -- "$(dirname "$guile_bin")/.." && pwd) guile_lib_dir=$guile_prefix/lib if [ -n "${GUILE_LOAD_PATH:-}" ]; then - guile_load_path="$project_root/modules:$guix_source_dir:$GUILE_LOAD_PATH" + guile_load_path="$modules_dir:$guix_source_dir:$GUILE_LOAD_PATH" else - guile_load_path="$project_root/modules:$guix_source_dir" + guile_load_path="$modules_dir:$guix_source_dir" fi exec env \ @@ -47,5 +67,8 @@ exec env \ GUILE_EXTRA_PREFIX="$guile_extra_prefix" \ SHEPHERD_PREFIX="$shepherd_prefix" \ GUIX_SOURCE_DIR="$guix_source_dir" \ - FRUIX_PROJECT_ROOT="$project_root" \ + FRUIX_PROJECT_ROOT="$fruix_channel_dir" \ + FRUIX_BOOTSTRAP_ROOT="$bootstrap_root" \ + FRUIX_CHANNEL_DIR="$fruix_channel_dir" \ + FRUIX_CHANNEL_URL="$fruix_channel_url" \ "$guile_bin" --no-auto-compile -s "$script" "$@" diff --git a/docs/PLAN_5.md b/docs/PLAN_5.md new file mode 100644 index 0000000..2a4a8d6 --- /dev/null +++ b/docs/PLAN_5.md @@ -0,0 +1,357 @@ +# Plan 5: split Fruix bootstrap from canonical Fruix + +This plan turns the boundary from `docs/bootstrap.md` into a concrete migration sequence. + +The intended end state is: + +- `fruix-bootstrap` prepares a plain FreeBSD host to act as a **Fruix builder** +- `fruix` is the canonical repo/channel for: + - packages + - systems + - installers + - deployment + - node lifecycle +- a booted Fruix system no longer depends on `fruix-bootstrap` + +## Primary goal + +Reach the point where a plain FreeBSD machine can: + +1. use `fruix-bootstrap` to become a Fruix builder +2. use that builder to evaluate a pinned `fruix` checkout or revision +3. build a Fruix artifact from `fruix` + - installer ISO + - VM image + - system closure +4. boot or install that artifact +5. continue operating using only `fruix` + +## Guiding rules + +1. `fruix` is the only canonical home for long-lived Fruix logic. +2. `fruix-bootstrap` may wrap `fruix`, but should not fork it. +3. If a booted Fruix node should understand it, it belongs in `fruix`. +4. Bootstrap-only foreign-host glue belongs in `fruix-bootstrap`. +5. Prefer temporary wrappers over duplicated logic. + +## What should move to `fruix` first + +These are the highest-priority items to live in `fruix`, because they define the product rather than the bootstrap path. + +### 1. Package definitions + +Move first: + +- `modules/fruix/packages/...` + +Why: + +- package definitions are canonical Fruix content +- they are part of the future channel story +- booted Fruix systems should use these directly + +### 2. FreeBSD system model and system artifact logic + +Move early: + +- `modules/fruix/system/freebsd/model.scm` +- `modules/fruix/system/freebsd/source.scm` +- `modules/fruix/system/freebsd/build.scm` +- `modules/fruix/system/freebsd/render.scm` +- `modules/fruix/system/freebsd/media.scm` +- `modules/fruix/system/freebsd/utils.scm` +- `modules/fruix/system/freebsd/executor.scm` +- the thin facade `modules/fruix/system/freebsd.scm` + +Why: + +- these define what a Fruix system is +- they define build/promotion/deploy behavior +- they must remain stable across bootstrap and post-bootstrap operation + +### 3. Fruix CLI semantics + +Move early: + +- `scripts/fruix.scm` +- the long-lived CLI behavior it implements + +Why: + +- command semantics belong to Fruix, not bootstrap +- installed nodes should match the host CLI model + +### 4. Installed-node lifecycle logic + +Move early: + +- node helper/module tree logic +- `build` +- `build-base` +- `deploy` +- `reconfigure` +- `status` +- `switch` +- `rollback` + +Why: + +- these are core Fruix product actions after first boot + +### 5. Installer and future TUI installer + +Move early and keep there: + +- installer environment logic +- installer ISO logic +- future TUI installer application + +Why: + +- the installer is a Fruix product artifact +- Fruix should be able to rebuild its own installer + +### 6. Metadata formats + +Move early and treat as canonical: + +- system generation metadata +- deployment metadata +- promoted native-build result metadata +- any future Fruix revision/pin metadata + +Why: + +- metadata is part of Fruix identity +- bootstrap should consume these formats, not redefine them + +## What can temporarily remain in `fruix-bootstrap` + +These items are allowed to remain here while the split is in progress. + +### 1. Bootstrap wrapper entrypoints + +Examples: + +- shell wrappers that locate the pinned `fruix` checkout +- shell wrappers that set `GUILE_LOAD_PATH`, `GUILE_PREFIX`, etc. +- entrypoints that invoke the real Fruix CLI from the foreign host + +These should become thin wrappers around `fruix`, not alternate implementations. + +### 2. Foreign-host dependency bring-up + +Examples: + +- locating or building Guile +- locating or building Shepherd support pieces +- preparing the host environment so the Fruix CLI can run + +This is bootstrap territory. + +### 3. Foreign-host validation harnesses + +Examples: + +- tests for plain-FreeBSD -> Fruix-builder bring-up +- tests that validate local bootstrap assumptions +- docs for first-time setup on vanilla FreeBSD + +These stay useful here even after most Fruix logic moves out. + +### 4. Temporary compatibility glue + +Examples: + +- wrappers that still point from bootstrap into `../fruix` +- temporary environment shims needed while the split settles + +These are acceptable if they are clearly transitional and do not redefine Fruix semantics. + +## What should not remain in `fruix-bootstrap` + +Do not let this repo become a second home for: + +- canonical package definitions +- canonical system semantics +- deployment semantics +- installer semantics +- long-lived metadata definitions +- installed-node behavior after first boot + +If bootstrap needs those capabilities, it should invoke them from `fruix`. + +## Proposed migration sequence + +## Milestone 1: make `fruix-bootstrap` call `fruix` + +Goal: + +- this repo stops being the canonical implementation and becomes a launcher/bootstrap layer + +Deliverables: + +- define a pinned `fruix` checkout input for bootstrap + - initially a local path such as `../fruix` is acceptable +- bootstrap wrappers invoke Fruix modules/scripts from that checkout +- package/system logic is no longer edited here as the primary source of truth + +Success signal: + +- running the bootstrap entrypoint clearly uses `../fruix` as the canonical Fruix source + +## Milestone 2: move canonical modules into `fruix` + +Goal: + +- all long-lived package/system logic lives in `fruix` + +Deliverables: + +- `modules/fruix/packages/...` live in `fruix` +- `modules/fruix/system/...` live in `fruix` +- `scripts/fruix.scm` lives in `fruix` +- this repo only wraps/invokes those files + +Success signal: + +- the same Fruix code is used both: + - from a foreign host via bootstrap + - from a booted Fruix node + +## Milestone 3: record a pinned Fruix identity in artifacts + +Goal: + +- built artifacts know which Fruix revision produced them + +Deliverables: + +- define minimal Fruix identity metadata, such as: + - checkout path for local development + - git commit when available + - optional dirty/clean marker +- record that identity in: + - image metadata + - installer metadata + - deployed generation metadata + - installed-node metadata + +Success signal: + +- a system can answer: “which Fruix revision built me?” + +## Milestone 4: first self-contained Fruix install from bootstrap + +Goal: + +- prove that bootstrap is needed only to create the first Fruix artifact + +Deliverables: + +- from plain FreeBSD + bootstrap, build either: + - a Fruix installer ISO, or + - a Fruix VM image +- boot/install the resulting system +- validate that the running Fruix node can perform post-boot actions using `fruix` only: + - `fruix system build` + - `fruix system build-base` + - `fruix system reconfigure` + - `fruix system deploy` + - `fruix system rollback` + +Success signal: + +- no runtime dependence on the bootstrap checkout is required after first boot + +## Milestone 5: build the first TUI installer in `fruix` + +Goal: + +- the user-facing installation experience is defined by Fruix itself + +Deliverables: + +- minimal text/TUI installer application in `fruix` +- built into the Fruix installer environment / installer ISO +- opinionated input surface, for example: + - target disk + - hostname + - root SSH key or password + - optional operator user + - optional profile/template selection +- generated declaration and install metadata recorded by Fruix + +Success signal: + +- bootstrap builds the installer, but Fruix defines the installer behavior + +## First split-validation milestone + +The first major proof should be this: + +### Bootstrap builds Fruix, then Fruix continues alone + +Concretely: + +1. start from plain FreeBSD +2. use `fruix-bootstrap` to create a Fruix builder +3. point it at pinned `../fruix` +4. build a Fruix VM image or installer ISO from `fruix` +5. boot the resulting Fruix system +6. on the running Fruix system, perform at least: + - `fruix system build` + - `fruix system reconfigure` + - `fruix system rollback` +7. verify none of those steps need the bootstrap checkout + +This is the first meaningful architectural victory of the split. + +## Recommended immediate tasks + +1. add a bootstrap configuration variable for the pinned Fruix checkout + - initially defaulting to `../fruix` +2. change bootstrap wrappers to load Fruix code from that checkout +3. stop treating this repo as the canonical edit location for Fruix modules +4. move package/system/CLI logic into `fruix` +5. add minimal Fruix revision metadata recording to built artifacts +6. then proceed to the Fruix-defined TUI installer + +## Temporary practical compromises + +The split does not require perfection on day one. + +Temporarily acceptable: + +- local-path pinning to `../fruix` +- wrapper scripts in bootstrap that export environment variables for Fruix +- some bootstrap-era host assumptions while the builder path stabilizes + +Not acceptable long-term: + +- duplicated package definitions in both repos +- duplicated system semantics in both repos +- booted Fruix nodes depending on bootstrap logic to keep operating + +## Open questions to resolve later + +These do not block the boundary itself: + +- exact Fruix channel/lock file format +- exact user-facing `upgrade` policy +- binary publication/substitution design +- local `jail` executor for native base builds + +Those are important, but they should be built on top of the split rather than baked into the boundary prematurely. + +## Summary + +The migration priority is: + +1. make bootstrap a thin launcher for pinned `fruix` +2. move canonical logic into `fruix` +3. record Fruix identity in artifacts +4. prove a booted Fruix node can continue without bootstrap +5. build the TUI installer in `fruix` + +If this plan is followed, `fruix-bootstrap` stays small and generic, while `fruix` becomes the real long-term system source of truth. diff --git a/docs/bootstrap.md b/docs/bootstrap.md new file mode 100644 index 0000000..41b09af --- /dev/null +++ b/docs/bootstrap.md @@ -0,0 +1,239 @@ +# Fruix bootstrap boundary + +This document defines the architectural boundary between: + +- `fruix` +- `fruix-bootstrap` + +The goal is to keep Fruix itself small, self-hosting, and canonical, while isolating the foreign-host bring-up logic required to get the first Fruix-capable environment running on plain FreeBSD. + +## Short version + +- `fruix` is the canonical source of Fruix package, system, installer, deployment, and node-management logic. +- `fruix-bootstrap` is a thin foreign-host layer that turns a plain FreeBSD installation into a **Fruix builder**. +- A booted Fruix system must be able to continue operating from `fruix` alone, without depending on `fruix-bootstrap`. + +## Core concept: the Fruix builder + +`fruix-bootstrap` should not be thought of as a permanent alternate Fruix implementation or a long-lived compatibility layer. + +Its job is to create a **Fruix builder**: an environment capable of evaluating and materializing a pinned `fruix` revision on a non-Fruix FreeBSD host. + +A Fruix builder should be able to: + +- run the Fruix CLI and evaluator +- load a pinned `fruix` checkout or channel revision +- materialize Fruix package outputs +- build Fruix system closures +- build Fruix installer ISOs and VM images +- install or deploy the first Fruix system + +That builder may run: + +- on a plain FreeBSD machine +- in CI +- in a jail +- on a later Fruix node + +The important point is that the builder is generic. It is a build/evaluation environment for Fruix, not a second product identity. + +## Ownership boundary + +### `fruix` owns + +Anything that should still matter after first Fruix boot belongs in `fruix`. + +This includes: + +- package definitions +- system definitions +- source objects and source provenance logic +- native-build and promotion logic +- executor model +- system artifact materializers: + - closures + - root filesystems + - disk images + - installers + - installer ISOs +- installed-node management logic: + - build + - build-base + - deploy + - reconfigure + - switch + - rollback + - later upgrade +- installer application / TUI logic +- publication/substitution logic when added later +- metadata formats that define Fruix identity and lifecycle + +Rule of thumb: + +> If a booted Fruix node should conceptually understand it, it belongs in `fruix`. + +### `fruix-bootstrap` owns + +Anything only required to turn plain FreeBSD into a Fruix-capable builder belongs in `fruix-bootstrap`. + +This includes: + +- host environment checks +- locating or building bootstrap tool dependencies +- foreign-host setup glue +- wrapper entrypoints for invoking a pinned `fruix` revision +- initial bootstrap documentation +- tests for the path from vanilla FreeBSD to first Fruix-capable builder or first Fruix artifact + +Rule of thumb: + +> If it is only needed before Fruix exists as Fruix, it belongs in `fruix-bootstrap`. + +## Dependency direction + +The dependency direction must remain one-way. + +Allowed: + +- `fruix-bootstrap` depends on a pinned `fruix` revision +- `fruix-bootstrap` invokes `fruix` to build packages, systems, installers, and images + +Not allowed: + +- `fruix` depending on `fruix-bootstrap` +- a booted Fruix node needing `fruix-bootstrap` in order to keep building or upgrading itself + +This keeps `fruix` canonical and prevents the bootstrap repo from becoming a second source of truth. + +## Canonical source of truth + +`fruix` is the only canonical home for Fruix product logic. + +In particular, these should not be duplicated long-term in `fruix-bootstrap`: + +- canonical package definitions +- canonical system logic +- installer workflow semantics +- deployment semantics +- long-lived metadata definitions +- installed-node lifecycle behavior + +`fruix-bootstrap` may temporarily wrap or seed those capabilities, but it should consume them from `fruix`, not fork them. + +## Pinning + +`fruix-bootstrap` should operate on a clear Fruix identity, not an ambient checkout with unclear provenance. + +Initially, that identity can be simple: + +- a local checkout path +- a git commit +- a tag +- a branch plus locked commit + +Later, this can become a proper Fruix channel lock/update model. + +Whatever form is used, the important property is: + +> the first Fruix artifact is built from a known `fruix` identity. + +That identity should eventually be recorded in: + +- installer metadata +- image metadata +- deployed generation metadata +- installed-node metadata + +## Product flow + +The intended lifecycle is: + +1. start from plain FreeBSD +2. use `fruix-bootstrap` to create a Fruix builder +3. point that builder at a pinned `fruix` revision +4. materialize artifacts from `fruix`, such as: + - package outputs + - system closures + - installer ISOs + - VM images + - installed systems +5. boot or install the resulting Fruix system +6. from that point onward, use `fruix` alone to move the system forward + +In short: + +```text +plain FreeBSD + -> fruix-bootstrap + -> Fruix builder + -> pinned fruix revision + -> build/install/deploy Fruix artifacts + -> booted Fruix node + -> future lifecycle managed by fruix +``` + +## Installer implication + +The installer UI and workflow belong to `fruix`, not `fruix-bootstrap`. + +Why: + +- the installer is part of the Fruix product surface +- Fruix should be able to build its own installer artifacts +- installed systems should be traceable to a pinned Fruix revision +- later Fruix nodes should be able to rebuild the installer without returning to bootstrap-only logic + +So the split should be: + +- `fruix-bootstrap`: makes it possible to build the installer +- `fruix`: defines the installer artifact and the TUI installer behavior + +## Success criteria + +The boundary is working when all of the following are true: + +1. a plain FreeBSD host can become a Fruix builder using `fruix-bootstrap` +2. that builder can build a Fruix installer ISO or VM image from a pinned `fruix` revision +3. the resulting Fruix system boots without requiring `fruix-bootstrap` +4. the booted Fruix system can keep using `fruix` for: + - build + - build-base + - reconfigure + - deploy + - rollback + - later upgrade +5. package and system evolution happens in `fruix`, not in duplicated bootstrap logic + +## Non-goals + +This split does not mean: + +- `fruix-bootstrap` becomes a permanent parallel Fruix distribution +- booted Fruix nodes should keep consulting bootstrap state +- bootstrap should own package or system semantics long-term + +It also does not require all historical bootstrap leakage to disappear immediately. Some transitional host assumptions may remain for a while, but they should be treated as technical debt against this boundary. + +## Working rule for future refactors + +When deciding where code belongs, ask: + +> Should a booted Fruix node still care about this? + +- If yes, it belongs in `fruix`. +- If no, and it only exists to bring up Fruix from foreign FreeBSD, it belongs in `fruix-bootstrap`. + +## Current direction + +Near-term work should follow this boundary: + +- keep bootstrap generic and thin +- move as much canonical logic as possible into `fruix` +- make the first user-facing installer a Fruix-defined product artifact +- ensure installed nodes record and operate from a pinned Fruix identity + +That gives Fruix a clean product story: + +- bootstrap creates a Fruix builder +- Fruix builds Fruix +- Fruix systems move forward using Fruix