bootstrap: load pinned fruix checkout

This commit is contained in:
2026-04-07 05:27:58 +02:00
parent 746c9cb589
commit 403af26caf
4 changed files with 677 additions and 19 deletions
+51 -12
View File
@@ -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
```
+30 -7
View File
@@ -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" "$@"
+357
View File
@@ -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.
+239
View File
@@ -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