Compare commits

...

54 Commits

Author SHA1 Message Date
2a1a6c3b81 docs: record build-profile revalidation 2026-04-06 19:04:18 +02:00
cb9e7332f4 system: separate build profile from development 2026-04-06 12:38:35 +02:00
db4d5bdf4c system: add in-node build and reconfigure 2026-04-06 08:55:35 +02:00
0e8b30434f system: consume promoted native base results 2026-04-06 06:38:23 +02:00
f41a916f45 native-build: introduce executor model 2026-04-06 05:26:34 +02:00
006ffee615 native-build: promote results into store objects 2026-04-06 01:16:44 +02:00
4614592a25 system: prototype self-hosted native builds 2026-04-05 17:59:56 +02:00
a3dd5556ae system: validate host-initiated native base builds 2026-04-05 16:32:08 +02:00
9e9a0b59fc system: validate development environment overlay 2026-04-05 11:56:06 +02:00
4975084baa store: centralize Fruix path naming 2026-04-05 08:58:40 +02:00
9dae4e5c84 system: validate installed rollback workflow 2026-04-05 01:39:24 +02:00
b3b1ba2489 system: record explicit generation layout 2026-04-04 19:44:14 +02:00
e86f74af97 docs: define Fruix deployment workflow 2026-04-04 19:11:14 +02:00
43c155bb9f tests: validate installer ISO on XCP-ng 2026-04-04 19:06:48 +02:00
604ad82f4f system: validate UEFI installer ISO boot path 2026-04-04 16:18:49 +02:00
1970c5c181 system: add UEFI installer ISO builder 2026-04-04 10:23:46 +02:00
ebe064a652 system: split FreeBSD system module 2026-04-04 09:38:27 +02:00
56d9d6a54b Archive detailed progress log 2026-04-04 08:51:04 +02:00
1d0090752d Add Fruix bootable installer environment 2026-04-04 03:09:25 +02:00
2517710282 Add non-interactive Fruix installation flow 2026-04-03 23:34:38 +02:00
02a02e365d Document Fruix FreeBSD source policy 2026-04-03 20:04:54 +02:00
865012ea0c Boot Fruix from distinct FreeBSD source revisions 2026-04-03 20:03:09 +02:00
8150508676 Validate side-by-side FreeBSD source revisions 2026-04-03 19:44:26 +02:00
5cbf5b90ed Build native bases from materialized FreeBSD sources 2026-04-03 14:57:47 +02:00
3f1793607d Materialize FreeBSD source inputs 2026-04-03 12:07:37 +02:00
d89225fe11 Model declarative FreeBSD source inputs 2026-04-03 11:47:52 +02:00
390bfb248f Document FreeBSD base self-hosting decision 2026-04-03 10:44:01 +02:00
03fbd9bf08 Validate FreeBSD base rollback workflow 2026-04-03 10:42:38 +02:00
72f89c51b5 Make FreeBSD base version declarative 2026-04-03 09:53:44 +02:00
3b95ced578 Split native FreeBSD boot and runtime artifacts 2026-04-03 06:57:19 +02:00
f163a63b1f Add native FreeBSD runtime slice 2026-04-03 06:33:01 +02:00
04b6ade095 Validate native FreeBSD boot assets 2026-04-03 06:06:49 +02:00
94e498f57d Boot Fruix from native FreeBSD world and kernel 2026-04-03 02:21:23 +02:00
4def04a357 Build native FreeBSD world and kernel artifacts 2026-04-03 01:51:50 +02:00
6021a57c38 Model native FreeBSD world and kernel artifacts 2026-04-02 23:19:11 +02:00
1108153277 Clarify transitional FreeBSD base package boundary 2026-04-02 23:02:31 +02:00
901d0a8448 Harden FreeBSD guest /etc and activation diagnostics 2026-04-02 22:45:34 +02:00
a04e650326 Record FreeBSD base provenance in system artifacts 2026-04-02 21:41:04 +02:00
4b786c356f docs: README 2026-04-02 21:29:30 +02:00
1b3e49fbf7 Remove guest runtime prefix shim dependency 2026-04-02 17:35:12 +02:00
377a6e49ff Validate Shepherd PID 1 boot on XCP-ng 2026-04-02 13:44:45 +02:00
f5ffd111ee Prototype Shepherd PID 1 boot on FreeBSD 2026-04-02 13:29:17 +02:00
c62c89b078 Complete Phase 10 FreeBSD system tooling 2026-04-02 11:19:30 +02:00
7db5c76541 README 2026-04-02 10:39:33 +02:00
b037427c22 Add fruix system command for FreeBSD 2026-04-02 10:31:24 +02:00
a69864cc0c docs: LLM_UI 2026-04-02 10:25:56 +02:00
e933bf3fd1 README 2026-04-02 09:48:13 +02:00
43677ffd78 Complete Phase 9 Fruix boot on XCP-ng 2026-04-02 09:11:50 +02:00
4b69118d06 Enable Fruix FreeBSD guest SSH boot on XCP-ng 2026-04-02 07:34:51 +02:00
d465264b5e Integrate FreeBSD image generation with system layer 2026-04-01 19:39:16 +02:00
b70d1fb12a Build reproducible FreeBSD bhyve images 2026-04-01 19:12:16 +02:00
514ec97f6b Validate FreeBSD Fruix rootfs trees 2026-04-01 18:49:40 +02:00
e4288dc330 Generate FreeBSD system closures in /frx/store 2026-04-01 18:42:33 +02:00
13963e7f62 Define FreeBSD Fruix operating-system model 2026-04-01 18:29:15 +02:00
133 changed files with 31773 additions and 1863 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/TODO.md

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# Fruix
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.
In Fruix, the FreeBSD platform is represented as foundational store artifacts and updated through the same generation mechanism as the rest of the system.
Fruix is a system where everything that exists on the machine exists for a reason that can be explained.
Every Fruix system must remain fully understandable and recoverable using only text files, a shell, and standard system tools.
- 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.

51
bin/fruix Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/sh
set -eu
project_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
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
if [ ! -x "$guile_bin" ]; then
echo "Guile binary is not executable: $guile_bin" >&2
exit 1
fi
ensure_built() {
if [ ! -d "$guile_extra_prefix/share/guile/site" ] || \
! GUILE_LOAD_PATH="$guile_extra_prefix/share/guile/site/3.0${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \
GUILE_LOAD_COMPILED_PATH="$guile_extra_prefix/lib/guile/3.0/site-ccache${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \
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"
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"
fi
}
ensure_built
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"
else
guile_load_path="$project_root/modules:$guix_source_dir"
fi
exec env \
GUILE_AUTO_COMPILE=0 \
GUILE_LOAD_PATH="$guile_load_path" \
LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
GUILE_PREFIX="$guile_prefix" \
GUILE_EXTRA_PREFIX="$guile_extra_prefix" \
SHEPHERD_PREFIX="$shepherd_prefix" \
GUIX_SOURCE_DIR="$guix_source_dir" \
FRUIX_PROJECT_ROOT="$project_root" \
"$guile_bin" --no-auto-compile -s "$script" "$@"

384
docs/GUIX_DIFFERENCES.md Normal file
View File

@@ -0,0 +1,384 @@
# Fruix differences for Guix sysadmins
Date: 2026-04-06
This document is aimed at operators who already know Guix well and want a quick map of where Fruix behaves similarly and where it intentionally differs.
The short version is:
- Fruix is strongly **Guix-inspired**
- it tries to preserve the important semantic properties
- but it does **not** copy Guix mechanically where FreeBSD or Fruix-specific concerns make a different representation clearer
## Big picture
Fruix keeps the core Guix ideas you would expect:
- declarative inputs
- content-addressed store paths
- immutable build outputs
- rollback-friendly deployment identity
- provenance tied to the deployed closure rather than mutable in-place state
But Fruix differs in at least four major ways today:
1. it targets **FreeBSD**, not GNU/Linux
2. its system frontend is currently smaller:
- `fruix system build|rootfs|image|install|installer|installer-iso`
3. it treats **FreeBSD source provenance** as an explicit deployment concern
4. its installed-system generation model is still earlier-stage than Guix's mature system-generation workflow
## Similarities to Guix
If you know Guix System, these Fruix properties should feel familiar.
### Immutable deployment identity
A deployed Fruix system is identified primarily by its closure path in `/frx/store`, not by mutable files under `/etc` or `/usr/local`.
### `/run/current-system`
Fruix keeps the Guix-like runtime convention:
- `/run/current-system`
That path remains the active runtime boundary used by activation and service wiring.
### Rollback-friendly semantics
Fruix avoids in-place mutation of an older deployed closure.
The validated rollback story now has two layers:
- declaration-level rollback by rebuilding/redeploying an earlier declaration
- installed-system rollback between already-recorded generations on the target itself
That is Guix-like in spirit, although Fruix still exposes a smaller installed-system workflow than Guix's more mature `reconfigure` model.
### Generation-style metadata and roots
Fruix now records explicit installed-system generation state under:
- `/var/lib/fruix/system`
and explicit retention roots under:
- `/frx/var/fruix/gcroots`
This preserves the basic Guix idea that deployment state and reachability should be represented explicitly rather than inferred from whatever happens to be on disk.
## Important differences from Guix
## 1. Fruix does not mirror Guix's on-disk generation layout 1:1
This is intentional.
Guix heavily reuses its profile-generation model and represents a lot of meaning through symlink structure such as profile links and system generation links.
Fruix keeps the **semantics** but uses a more explicit metadata-oriented layout for installed systems.
Current Fruix layout starts as:
```text
/var/lib/fruix/system/
current -> generations/1
current-generation
generations/
1/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm
```
After a validated installed-system switch, Fruix also records:
```text
/var/lib/fruix/system/
rollback -> generations/1
rollback-generation
generations/
2/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm
```
Why Fruix does this:
- it makes deployment state easier to inspect directly
- it gives FreeBSD-specific install and provenance details a clearer home
- it keeps room for richer operator tooling later without losing the Guix properties
So the rule of thumb is:
- **same semantics as Guix where practical**
- **not necessarily the same directory names or symlink vocabulary**
## 2. Fruix currently keeps `/run/current-system` simple
Even though Fruix now records explicit generation metadata under `/var/lib/fruix/system`, it still keeps:
- `/run/current-system -> /frx/store/...`
rather than making `/run/current-system` point through a generation directory first.
This was chosen to preserve compatibility with the already-validated activation and runtime model while adding explicit generation metadata separately.
## 3. Fruix treats source provenance more explicitly
Guix sysadmins are used to derivation/store provenance. Fruix adds an extra emphasis because its current FreeBSD story depends on explicit source selection and materialization.
Fruix routinely records:
- declared FreeBSD source object metadata
- materialized source store paths
- source materialization metadata files
- closure-level store layout metadata
- install metadata on the target system
This is more prominent in Fruix than most Guix system docs because FreeBSD base/source identity has been an active design concern for this project.
## 4. Fruix has installer-media workflows as first-class system actions
Guix has installation media and image workflows, but Fruix's current system frontend makes these especially explicit because they are still part of the active architectural bring-up story.
Current Fruix actions include:
- `fruix system install`
- `fruix system installer`
- `fruix system installer-iso`
That is a little more deployment-medium-oriented than the mental model many Guix users start with.
## 5. Device naming is more environment-sensitive than Guix users may expect
Because Fruix is on FreeBSD, the install target device naming is not the same as on Linux.
Validated examples:
- installer disk-image path under QEMU:
- `/dev/vtbd1`
- installer ISO path under QEMU:
- `/dev/vtbd0`
- installer ISO path under XCP-ng:
- `/dev/ada0`
So compared with Guix-on-Linux intuition, Fruix operators should be more explicit about target-device selection during installation and installer-media validation.
## 6. Fruix now has a minimal installed-system generation command surface, but it is still smaller than Guix's
This remains the biggest operational gap, but it is no longer a complete gap.
Installed Fruix systems now provide a larger in-guest helper surface:
- `fruix system build`
- `fruix system reconfigure`
- `fruix system status`
- `fruix system switch /frx/store/...-fruix-system-...`
- `fruix system rollback`
What this gives you today:
- explicit current-generation tracking
- explicit rollback-generation tracking
- in-place switching between already-staged closures on the installed target
- rollback without reinstalling the whole system image again
- a validated in-node build path that can read the embedded current declaration inputs
- a validated in-node `reconfigure` path that can build a candidate closure locally and stage it as the next generation
What it still does **not** give you yet compared with Guix:
- a mature end-to-end `upgrade` story for advancing declared inputs automatically
- automatic closure transfer/fetch as part of `switch`
- a higher-level `deploy` workflow across multiple machines or targets
- the broader generation-management UX Guix operators expect
So if you come from Guix, assume that Fruix now has:
- strong closure/store semantics
- explicit install artifacts
- explicit generation metadata roots
- a real installed-system build/reconfigure/switch/rollback surface
- but not yet the fuller long-term node/deployment UX that Guix users may expect
## 7. Fruix keeps Guix-like store semantics, but not Guix/Nix hash-prefix machinery exactly
Fruix still uses immutable store paths under:
- `/frx/store`
and it still treats a store path as a deployment identity boundary.
But Fruix now intentionally differs from Guix/Nix in how the visible store-path prefix is constructed.
Current Fruix policy is:
- centralize store-path naming behind shared helpers
- hash a small semantic identity record rather than copying Nix's historical path-hash formula exactly
- include at least:
- object kind
- logical/display name
- output name
- payload or manifest identity
- hash-scheme version marker
- truncate the visible SHA-256 prefix to **160 bits**
- render that visible prefix as **40 hex characters**
Why Fruix does this instead of copying Guix/Nix exactly:
- the main goal was shorter store prefixes, not Nix compatibility for its own sake
- distinct outputs should have distinct identities because `out`, `lib`, `debug`, and `doc` are semantically different artifacts
- Fruix wanted one central policy point that can be swapped later without touching every materializer again
- Fruix did **not** want to inherit Nix's legacy details unless they provide clear value here
- custom base32 alphabet and bit ordering
- compressed/XOR-folded path hashes
- exact historical `output:out` / `source` path-hash conventions
So compared with Guix:
- the important semantic property is the same:
- different store objects should get different immutable identities
- the exact printable prefix algorithm is intentionally simpler in Fruix today
For Guix-familiar operators, the practical takeaway is:
- still think of `/frx/store/...` paths as immutable deployment identities
- do **not** assume Fruix store prefixes are byte-for-byte comparable to Guix/Nix ones
- expect Fruix to prefer a simpler, centralized naming policy unless exact Guix/Nix behavior becomes necessary later
## 8. Fruix can expose separate in-system development and build overlays
For the validated Phase 20 path, Fruix now distinguishes three layers instead of two:
- runtime profile
- development profile
- build profile
On those systems, Fruix exposes:
- development:
- `/run/current-system/development-profile`
- `/run/current-development`
- `/usr/local/bin/fruix-development-environment`
- build:
- `/run/current-system/build-profile`
- `/run/current-build`
- `/usr/local/bin/fruix-build-environment`
- canonical base-build compatibility links:
- `/usr/include -> /run/current-system/build-profile/usr/include`
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
The intent is:
- keep the main runtime profile lean
- keep the development helper interactive and convenient
- keep the build helper narrower, more sanitized, and closer to the actual `buildworld` / `buildkernel` contract
- avoid treating a development-heavy or build-heavy image as the default runtime shape
Compared with Guix, this is conceptually similar to keeping development-oriented and build-oriented state separate from the main runtime identity, but Fruix currently expresses it as system-attached overlays rather than through Guix's broader profile/tooling model.
Fruix still has the guest self-hosted native-build prototype helper at:
- `/usr/local/bin/fruix-self-hosted-native-build`
But that helper now explicitly uses the build helper contract instead of trying to reuse the full interactive development shell. The practical Fruix takeaway is:
- the development overlay makes native base work possible inside the system
- the build overlay gives Fruix a stricter, more reproducible contract for real base builds
Fruix now also makes an explicit distinction between:
- mutable native-build staging/results under:
- `/var/lib/fruix/native-builds`
- immutable promoted native-build identities under:
- `/frx/store`
So a guest self-hosted run can stage a result tree locally, while the host can later promote that result into first-class Fruix store objects such as:
- `/frx/store/...-fruix-native-world-...`
- `/frx/store/...-fruix-native-kernel-...`
- `/frx/store/...-fruix-native-headers-...`
- `/frx/store/...-fruix-native-bootloader-...`
- `/frx/store/...-fruix-native-build-result-...`
Compared with Guix, this is a more explicit split between:
- a mutable result/staging area for native build execution
- and the immutable store identities that Fruix treats as the real promoted result
Fruix also now models native-build placement more explicitly as an executor choice.
Current executor kinds are:
- `host`
- `ssh-guest`
- `self-hosted`
So instead of treating:
- host-driven builds
- host-initiated guest builds
- guest self-hosted builds
as three unrelated architectural forks, Fruix is moving toward one native-build result model with different executor policies attached to it.
Fruix also now lets system declarations consume a promoted native-build result bundle directly.
That means Fruix can now distinguish more explicitly between:
- a mutable build/result root used during execution
- an immutable promoted native-build identity in `/frx/store`
- and a later system declaration that points at that promoted identity as an input
Compared with Guix, this is still Guix-like in the important sense that the deployed system is identified by immutable store objects, but Fruix is making the FreeBSD native-base result set itself a more explicit first-class declaration input than it was before.
## Where Fruix is intentionally trying to improve on Guix's representation
Fruix is not trying to improve on Guix's core semantics. Guix already got those right.
Where Fruix is intentionally experimenting is mostly the **representation layer**:
- make generation state more legible to operators
- make provenance more visible without needing to reconstruct it mentally from symlink layout alone
- separate:
- runtime entry point (`/run/current-system`)
- installed deployment state (`/var/lib/fruix/system`)
- retention roots (`/frx/var/fruix/gcroots`)
That is why Fruix currently prefers a small per-generation metadata directory instead of only a bare generation link.
## Practical operator advice for Guix users
If you are already comfortable with Guix, the safest Fruix mental model today is:
1. think in terms of immutable closures and declarations first
2. use `fruix system build` as the canonical starting point
3. treat image/install/installer/installer-iso as deployment materializers built from the same declaration
4. identify a deployment by:
- closure path
- source provenance metadata
- install metadata
5. think of rollback in two layers:
- if the target already has the desired closure staged locally:
- use `fruix system rollback`
- otherwise:
- redeploy the earlier declaration again
## Status summary
Today Fruix is closest to Guix in:
- store and closure semantics
- declarative deployment identity
- rollback-friendly immutability
- `/run/current-system` runtime model
It differs most from Guix in:
- FreeBSD platform details
- source-provenance emphasis
- installer-medium-oriented workflows
- generation-layout representation
- and an installed-system generation command surface that now exists, but is still much smaller than Guix's

466
docs/LLM_UI.md Normal file
View File

@@ -0,0 +1,466 @@
Dual output is exactly the right instinct. If you want a system to be self-documenting for both humans and LLMs, the key is this:
> **Do not treat documentation as prose attached afterward. Treat it as structured semantic data emitted by the same machinery that defines the system.**
Otherwise the docs drift, the machine lies, the humans improvise, and the model confidently invents nonsense. A beloved industry tradition.
## My preference, if Fruix wanted to be maximally legible to me
I would want **three layers at once** for every meaningful object in the system:
### 1. Operational output
The normal thing a human expects.
Example:
```text
$ fruix service status sshd
running since 2026-04-02T09:14:33Z
pid 812
generation 42
store item /frx/store/...-openssh-service
```
### 2. Structured machine-readable output
Explicit, stable, schema-like, with no pretty nonsense.
Example:
```json
{
"kind": "service-status",
"name": "sshd",
"state": "running",
"since": "2026-04-02T09:14:33Z",
"pid": 812,
"generation": 42,
"store_path": "/frx/store/...-openssh-service",
"definition_ref": "services.sshd"
}
```
### 3. Embedded semantic help/doc layer
Not a wall of text. Short, attached meaning.
Example:
```json
{
"doc": {
"summary": "OpenSSH daemon providing remote login access.",
"purpose": "Accepts inbound SSH connections for remote administration.",
"inputs": ["host keys", "network", "authorized keys config"],
"depends_on": ["network-online", "host-keys"],
"used_by": ["admin access", "remote deployment"],
"failure_modes": [
"missing host keys",
"port unavailable",
"invalid configuration"
],
"see_also": [
"fruix service logs sshd",
"fruix config explain services.sshd"
]
}
}
```
That combination is ideal. The command answers the immediate question, but also emits enough structure that an LLM can keep asking intelligent follow-ups without having to guess what “sshd” even is in Fruix-world.
---
## The most important design principle
Every object in Fruix should ideally answer these questions about itself:
* what are you?
* why do you exist?
* what created you?
* what do you depend on?
* what depends on you?
* how do I inspect you?
* how do I change you?
* what breaks if you fail?
* where is your definition?
* what commands are relevant next?
That is the difference between “a system with documentation” and “a system that can explain itself.”
## The command model I would love
I would strongly prefer **one canonical plain output mode**, plus **a structured explain mode**, rather than mixing prose into every default command.
So:
```text
fruix service status sshd
fruix service status sshd --json
fruix service explain sshd
```
Or maybe:
```text
fruix inspect service sshd
fruix inspect service sshd --json
fruix explain service sshd
```
This separation matters.
### Why not always dual-output in the same stream?
Because it becomes annoying for humans, brittle for scripts, and ugly for terminals. People say they want hybrid output until they actually have to read it every day.
Better pattern:
* default output for operators
* stable machine output for tooling/LLMs
* explicit explanation output for context
That said, you *can* still have a dual channel if done cleanly.
---
## My favorite model: result + attached explain block
Something like this:
```text
$ fruix service status sshd --with-doc
sshd: running
pid: 812
since: 2026-04-02T09:14:33Z
generation: 42
Explanation:
SSH daemon for remote login and administration.
Depends on network-online and host key material.
Configured from services.sshd in the system definition.
Relevant commands:
fruix service logs sshd
fruix config explain services.sshd
fruix service restart sshd
```
That is excellent for humans and still digestible for an LLM if the formatting is predictable.
But for automation, I would still want:
```text
fruix service status sshd --json
fruix service explain sshd --json
```
So in practice Id want **two parallel interfaces**, not one muddled one.
---
## What should be self-documenting in Fruix?
Not just commands. The **actual nouns** of the system.
### 1. Store paths
A store item should be explainable.
```text
fruix store explain /frx/store/845bd...-freebsd-bash-5.3.9
```
Should answer:
* package/runtime/component name
* version
* origin
* build inputs
* purpose
* references
* whether it is part of current system
* whether it is GC-rooted
* whether it is user-requested or dependency-only
### 2. Services
Explain:
* purpose
* definition origin
* dependencies
* restart policy
* logs
* ports/files touched
* activation behavior
### 3. System definitions
Explain:
* what modules/options contributed
* what options are set
* defaults vs overrides
* resulting closures
* affected services
* resulting boot entries/generations
### 4. Configuration options
This one matters a lot.
A good system should let me ask:
```text
fruix config explain services.sshd.enable
fruix config explain system.network.hostname
fruix config search ssh
```
And get:
* type
* default
* current value
* description
* examples
* constraints
* source module
* related options
This is one of the best things NixOS/Guix-style systems can do, and Fruix should absolutely lean into it.
### 5. Commands themselves
CLI subcommands should be introspectable too.
```text
fruix help gc
fruix help gc --json
fruix explain command gc
```
Because LLMs are often dropped into a black box and forced to infer the command surface. A command that can describe itself structurally is vastly easier to use safely.
---
## What would help me most as an LLM?
If Fruix wanted to be unusually legible to a model, Id want these properties.
### Stable schemas
If `--json` exists, make it boring and stable. No whimsical field names, no gratuitous nesting, no surprise changes. Humans love creative naming, then wonder why tooling breaks.
### Uniform “kind” fields
Every structured output should say what it is.
```json
{ "kind": "service-status", ... }
{ "kind": "config-option", ... }
{ "kind": "store-item", ... }
{ "kind": "system-generation", ... }
```
This helps tremendously.
### Embedded references
Objects should point to related objects.
For example, service status should include:
* definition reference
* related config path
* related store path
* dependency names
This lets a model traverse the system instead of hallucinating relationships.
### Short summaries written for machines and humans
Not marketing copy. Not essay paragraphs. Tight explanatory text.
Good:
> "Enables the OpenSSH daemon for inbound remote shell access."
Bad:
> "This module offers a powerful and flexible way to control secure access to your Fruix machine."
Nobody needs brochure language except people trying to justify headcount.
### Explicit provenance
For anything important, say where it came from:
* built from which derivation
* declared in which module
* set by user vs default
* inherited from which profile/system generation
That makes the system explainable instead of mystical.
---
## I would absolutely add a “why” command
This is the killer feature.
Examples:
```text
fruix why /frx/store/...-freebsd-bash-5.3.9
fruix why service sshd
fruix why config services.sshd.enable
```
Answers like:
```text
freebsd-bash-5.3.9 is present because:
- required by fruix-system-fruix-freebsd generation 42
- referenced by interactive-shell-runtime
- pulled in by admin profile
```
Or:
```text
services.sshd.enable is true because:
- set explicitly in hosts/fruix-freebsd.scm
- module openssh-service.scm contributes service definition
```
This is huge for both humans and LLMs. “Why is this here?” is half of systems debugging.
---
## My preferred architecture for integrated docs
I would not store docs only in markdown files. I would make them part of the typed object/module/option definitions themselves.
For each module/option/service/package, attach fields like:
* `summary`
* `description`
* `type`
* `default`
* `examples`
* `see-also`
* `danger-level`
* `depends-on`
* `used-by`
* `introduced-in`
* `notes`
Then generate from that:
* terminal help
* handbook pages
* man pages
* JSON explain output
* web docs
* maybe even shell completions with hints
That way there is **one source of truth**, many renderings.
This is the real win.
---
## A concrete Fruix interaction model Id love
Something like this command family:
```text
fruix inspect <object> [name]
fruix explain <object> [name]
fruix search <term>
fruix why <object> [name]
fruix graph <object> [name]
```
Examples:
```text
fruix inspect service sshd
fruix explain service sshd
fruix why service sshd
fruix graph service sshd
fruix inspect store /frx/store/...-freebsd-bash-5.3.9
fruix explain store /frx/store/...-freebsd-bash-5.3.9
fruix why store /frx/store/...-freebsd-bash-5.3.9
fruix inspect config services.sshd.enable
fruix explain config services.sshd.enable
fruix why config services.sshd.enable
```
That is clean, memorable, and composable.
---
## If you insist on dual output in one invocation
Then I would make it a structured container, not mixed prose spaghetti.
Maybe:
```text
fruix service status sshd --format annotated
```
Output:
```yaml
result:
name: sshd
state: running
pid: 812
since: 2026-04-02T09:14:33Z
generation: 42
doc:
summary: OpenSSH daemon for remote administration.
depends_on:
- network-online
- host-keys
config_path: services.sshd
see_also:
- fruix service logs sshd
- fruix config explain services.sshd
```
YAML is readable enough for humans and parseable enough for tools. Though for machine use Id still pick JSON.
---
## The single biggest preference of all
If I had to choose only one thing, it would be this:
> Every meaningful Fruix object should be introspectable by name or path, with both a stable machine schema and a concise semantic explanation.
That alone would make the system unusually legible to me, to users, and to future maintainers who have not yet been spiritually damaged by it.
## My concrete recommendation
Build this into Fruix from the start:
* `--json` on all important commands
* `explain` subcommands for commands, config, services, store items, generations
* `why` subcommands for dependency/provenance tracing
* structured doc fields embedded in module and option definitions
* generated docs from the same source
* concise summaries, not essay sludge
That is the sweet spot. Self-documenting, machine-friendly, human-usable, and hard to let drift.
If Fruix gets that right, an LLM dropped cold into the system could become useful very quickly instead of fumbling around like a junior admin in an unfamiliar datacenter.

316
docs/PLAN_3.md Normal file
View File

@@ -0,0 +1,316 @@
# Fruix on FreeBSD, Path C: Short Hardening Pass, Then Native FreeBSD Base Builds
This document extends `docs/PLAN_2.md` after the completion of Phases 1 through 10 and the post-Phase-10 Shepherd/PID-1 and runtime-prefix cleanup work.
The project has already crossed the main feasibility threshold:
- Fruix can build declarative FreeBSD system artifacts
- those artifacts boot on the real XCP-ng VM
- the guest reaches DHCP and SSH
- both `freebsd-init+rc.d-shepherd` and `shepherd-pid1` have been validated
- the guest no longer depends on `/tmp/*-validate-install` runtime-compatibility shims
The next architectural question is about the **FreeBSD base itself**.
Today, Fruix still assembles the kernel, bootloader, libraries, rc assets, and userland largely by **copying curated artifacts from the build host** into `/frx/store`. That was the right bootstrap move, but it is now the largest remaining non-native part of the system.
Long-term, the desired direction is clear:
- build FreeBSD kernel/world as Fruix-managed artifacts
- keep them in `/frx/store`
- make system upgrades a declarative rebuild/redeploy story instead of a host-copy refresh
However, this plan does **not** recommend jumping directly into full self-hosted FreeBSD base builds inside the guest. Instead, it takes a narrower and safer sequence:
1. do a **short hardening pass** on the current working system
2. then begin **Option B** immediately as the next major architecture phase
3. start Option B on the **builder side** from `/usr/src`, not with guest self-hosting
This keeps momentum while avoiding two failure modes:
- over-polishing the current host-copy pipeline as if it were the end state
- mixing base-system build mechanics, runtime polish, boot debugging, and self-hosting into one oversized step
Throughout this plan, the canonical Fruix roots remain:
- `/frx/store`
- `/frx/var`
- `/frx/etc`
The user-facing system remains **Fruix**, with CLI `fruix`, while internal upstream-derived names continue to be renamed selectively rather than mechanically.
---
## Current State at the Start of Plan 3
Fruix on FreeBSD already has:
- a functioning content-addressed store and derivation path
- daemon-mediated builds with FreeBSD-aware isolation prototypes
- real package outputs in `/frx/store`
- a declarative FreeBSD operating-system model
- rootfs and image generation driven by `fruix system ...`
- real XCP-ng validation using the approved VM/VDI path
- a working FreeBSD guest with DHCP, SSH, activation, Shepherd, and ready markers
- validated `shepherd-pid1` boot on both local QEMU/TCG and the real XCP-ng VM
- removal of the guest runtime's dependence on `/tmp` compatibility-prefix symlinks
The main remaining architectural compromise is that the FreeBSD base layer is still defined mostly as a set of curated host copies:
- kernel from `/boot/kernel`
- bootloader and boot assets from `/boot`
- userland/runtime pieces from `/bin`, `/sbin`, `/usr/bin`, `/usr/sbin`, `/lib`, `/usr/lib`, and `/etc`
- headers from `/usr/src`
That means Fruix can currently rebuild and redeploy a working system, but it does **not** yet have a truly native upgrade story for the FreeBSD base itself.
---
## Guiding Decision for the Next Milestone
The next milestone should be:
- **just enough hardening** to make the current system a reliable platform for deeper work,
- followed immediately by the first **host-built FreeBSD base artifacts** in `/frx/store`.
This means the next work should optimize for:
- debuggability
- provenance
- repeatable deployment/validation
- clean separation between host-staged base artifacts and Fruix-built artifacts
- incremental replacement of the copy-based base layer
It should **not** optimize for:
- turning the current host-copy path into a heavily polished long-term abstraction
- immediate self-hosted `buildworld`/`buildkernel` inside the Fruix guest
- solving every usability concern before starting native base work
---
## Phase 12: Short Hardening Pass on the Existing Working Pipeline
The purpose of this phase is not to beautify the bootstrap path. It is to make the existing validated system reliable enough that FreeBSD base-build work can proceed without avoidable confusion.
### Intermediate Goal 12.1: Improve Deployment Provenance and Failure Diagnosis
**Verification Goal 12.1:** Make the current pipeline record exactly what FreeBSD base inputs and deployment artifacts were used for each generated system/image.
This should include at least:
- host `freebsd-version` / `uname` provenance
- `/usr/src` revision or identifying metadata when available
- the exact staged base-package store paths used in the system closure
- image metadata that clearly distinguishes:
- host-copied FreeBSD base artifacts
- Fruix-built Guile/Shepherd/system artifacts
- better collection of boot/runtime logs where possible from the existing QEMU and XCP-ng harnesses
**Success Criteria:** a generated Fruix system/image can be traced back to its host-side FreeBSD base inputs and runtime validation logs without ad hoc investigation.
### Intermediate Goal 12.2: Tighten Basic Runtime Completeness and Operator Diagnostics
**Verification Goal 12.2:** Close the most important remaining “prototype rough edges” in the current guest so it remains a dependable validation target while Option B begins.
This should focus on small, high-value improvements such as:
- clearer boot/service logs
- fewer known noisy runtime warnings where reasonably fixable
- more explicit validation of essential services and files
- better failure surfacing from activation and service startup
This phase should avoid broad feature creep. The target is not a polished distribution; it is a sharper debugging and validation baseline.
**Success Criteria:** the current FreeBSD guest remains reproducibly bootable and easier to debug, with fewer ambiguous failures during rebuild/redeploy cycles.
### Intermediate Goal 12.3: Make the Host-Staged FreeBSD Base Boundary Explicit
**Verification Goal 12.3:** Refine the current package/model layer so the “host-staged FreeBSD base” is treated as an explicit transitional boundary rather than an implicit permanent design.
This should include:
- clearer grouping or manifesting of host-staged base packages
- documentation of which components are currently copied from the host
- explicit notes on which ones are planned to be replaced first by native base builds
**Success Criteria:** the repo documents and code clearly separate transitional host-copy FreeBSD base handling from genuine Fruix-built artifacts.
---
## Phase 13: Option B Begins — Host-Built FreeBSD Kernel and World Artifacts in `/frx/store`
This is the real architectural pivot.
The goal is **not** yet to self-host FreeBSD base builds inside a Fruix guest. The goal is to teach Fruix to produce FreeBSD base artifacts as build outputs under `/frx/store`, using the builder host and `/usr/src` as the source of truth.
### Intermediate Goal 13.1: Model FreeBSD World and Kernel as Fruix Build Artifacts
**Verification Goal 13.1:** Introduce Fruix build descriptions for the FreeBSD base that can represent at least:
- a `freebsd-world` artifact
- a `freebsd-kernel` artifact
- required boot assets associated with the selected kernel/world build
The first version may still be specialized and FreeBSD-specific rather than fully generalized.
This step should determine and document:
- how `/usr/src` is treated as an input
- what build parameters affect output identity
- how kernel/world configuration is hashed into the output model
- how these outputs are split or grouped in `/frx/store`
**Success Criteria:** Fruix can describe native FreeBSD world/kernel build outputs as real store artifacts rather than only host-copy packages.
### Intermediate Goal 13.2: Build and Stage Minimal FreeBSD World/Kernel Outputs From `/usr/src`
**Verification Goal 13.2:** Produce the first concrete world/kernel build outputs from `/usr/src` and stage them into `/frx/store`.
The first target should be intentionally narrow:
- build a kernel matching the validated VM target
- build a minimal world sufficient for the current Fruix guest to boot
- stage install trees under content-addressed store paths
This phase should prefer correctness and repeatability over completeness. A minimal successful output split is acceptable if it is documented.
**Success Criteria:** `/frx/store` contains native Fruix-managed FreeBSD world/kernel outputs built from `/usr/src`, and their contents can be inspected as build results rather than copied host snapshots.
### Intermediate Goal 13.3: Boot a Fruix System Using Store-Built FreeBSD Base Artifacts
**Verification Goal 13.3:** Wire the operating-system/image pipeline so a generated system can boot using the newly built world/kernel outputs instead of the corresponding host-copy packages.
This should preserve the existing validation strategy:
- local QEMU/TCG + UEFI where useful
- real XCP-ng validation on the approved VM and existing VDI
The first boot target may still leave some secondary base components on the older copy path if necessary, as long as the transition boundary is explicit.
**Success Criteria:** a Fruix system/image boots successfully using native store-built FreeBSD base artifacts for at least the kernel and core world runtime.
---
## Phase 14: Incrementally Replace the Host-Copy FreeBSD Base Layer
Once a minimal native world/kernel path works, the rest of the host-copy base layer should be retired incrementally rather than in one giant switch.
### Intermediate Goal 14.1: Replace Kernel and Boot Assets First
**Verification Goal 14.1:** Move the system closure and image generator to prefer native store-built kernel and boot assets over host-copied `/boot/...` material.
This should include:
- kernel
- loader/boot assets as appropriate
- any required linker or boot metadata
**Success Criteria:** the image no longer relies on host-copied kernel/boot components for the validated boot path.
### Intermediate Goal 14.2: Replace the Core Runtime World Slice
**Verification Goal 14.2:** Move the essential userland/runtime components to the native world outputs, including the files required for:
- boot
- activation
- networking
- SSH
- service startup
- basic operator access
**Success Criteria:** the booted Fruix system reaches ready state using a native store-built core FreeBSD runtime rather than a hand-curated host copy set.
### Intermediate Goal 14.3: Revisit Headers, Toolchain, and Development Splits
**Verification Goal 14.3:** Define cleaner boundaries between:
- runtime world outputs
- development headers
- toolchain artifacts
- optional build/developer profiles
This step should reduce accidental coupling between “what the guest needs to boot” and “what the host needs to build software”.
**Success Criteria:** Fruix has a clearer and more maintainable model for FreeBSD runtime vs. development artifacts in `/frx/store`.
---
## Phase 15: Establish a Real Fruix Upgrade Story for the FreeBSD Base
After native base artifacts exist and are used by the system closure, Fruix can move from “rebuild from current host copies” toward a more honest upgrade story.
### Intermediate Goal 15.1: Make the FreeBSD Base Version a Declarative Input
**Verification Goal 15.1:** Define how a Fruix system declares which FreeBSD base/kernel source version it targets.
This may initially remain tied to locally available `/usr/src`, but it should move the model toward a declarative notion of:
- target FreeBSD branch/release
- kernel configuration
- world configuration
- boot/runtime variant
**Success Criteria:** FreeBSD base versioning becomes an explicit part of the Fruix system model rather than an implicit property of the builder host.
### Intermediate Goal 15.2: Support Rebuild/Redeploy/Rollback Across Base Versions
**Verification Goal 15.2:** Demonstrate that at least two distinct FreeBSD base builds can coexist in `/frx/store` and that the generated system can switch between them through the normal rebuild/redeploy flow.
This should preserve the Guix-inspired strengths:
- content-addressed outputs
- side-by-side versions
- rollback-friendly system closures
**Success Criteria:** Fruix can rebuild and redeploy a system against a newer FreeBSD base without mutating the old one in place.
### Intermediate Goal 15.3: Decide When to Pursue Self-Hosted Base Builds
**Verification Goal 15.3:** Reassess whether the next step should be:
- building FreeBSD base artifacts inside a Fruix-managed environment, or
- continuing to use the host builder while improving reproducibility and source acquisition
This decision should be made only after native world/kernel artifacts are already working in `/frx/store`.
**Success Criteria:** the project has a documented, evidence-based decision on whether and when to pursue self-hosted FreeBSD base builds.
---
## Strategic Notes
### Why this order?
This sequence is intended to preserve momentum and architectural clarity.
The short hardening pass prevents the next phase from being undermined by avoidable debugging ambiguity. But the hardening phase is intentionally short so the project does not over-invest in the transitional host-copy design.
Then Option B begins immediately in a way that is ambitious but still controlled:
- build FreeBSD base artifacts on the host
- store them in `/frx/store`
- boot from them
- replace the host-copy model incrementally
### What this plan does **not** require yet
This plan does **not** require, in its first native-base stages:
- guest self-hosting
- in-place `freebsd-update` integration
- complete replacement of every base file in one step
- abandoning the validated XCP-ng/QEMU test harnesses
- immediate adoption of `shepherd-pid1` as the only supported boot path
### What success will mean
Success under this plan means the project moves from:
- “Fruix assembles a FreeBSD system by copying a curated slice of the host”
into:
- “Fruix builds and stores FreeBSD base artifacts itself, then assembles and deploys systems from those declared outputs.”
That is the real bridge from the current prototype to a more genuinely native Fruix system on FreeBSD.

406
docs/PLAN_4.md Normal file
View File

@@ -0,0 +1,406 @@
# Fruix on FreeBSD, Path D: Declarative Source Acquisition, Installation Artifacts, and the Road to Self-Hosting
This document extends `docs/PLAN_3.md` after the completion of Phases 1 through 15.
The project has now crossed another important threshold:
- Fruix builds native FreeBSD base artifacts from `/usr/src` into `/frx/store`
- the validated boot/runtime path is host-base-free at the deployed system layer
- the FreeBSD base is now an explicit declarative Fruix input
- side-by-side base versions can coexist in `/frx/store`
- Fruix can rebuild, redeploy, and roll back between declared base versions
That means the next question is no longer whether Fruix can assemble and boot a native FreeBSD system. It can.
The next question is how to make that native base path **more reproducible, more source-declarative, and more installable**.
The biggest remaining impurity is now clear:
- the native base build path still relies on the builder host's ambient `/usr/src`
That was the right bridge through Phases 13 to 15, but it should not be the long-term source story.
Accordingly, Plan 4 focuses on three connected goals:
1. make FreeBSD source acquisition and source identity more declarative
2. turn the current image-generation path into a real installation story
3. only then move carefully toward self-hosted base builds
This preserves the Guix-inspired core while continuing to embrace FreeBSD semantics rather than trying to imitate Linux-specific workflows.
Throughout this plan, the canonical Fruix roots remain:
- `/frx/store`
- `/frx/var`
- `/frx/etc`
The user-facing system remains **Fruix**, with CLI `fruix`, while internal upstream-derived names continue to be renamed selectively rather than mechanically.
The current real-VM validation constraints still apply unless explicitly relaxed later:
- approved VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- approved A/B VDIs:
- `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
- `7061d761-3639-4bec-87f7-2ba1af924eaa`
---
## Current State at the Start of Plan 4
Fruix on FreeBSD already has:
- a functioning content-addressed store and derivation path under `/frx/store`
- daemon-mediated builds with validated FreeBSD isolation concepts
- a working declarative FreeBSD operating-system model
- validated system closure, rootfs, and image generation through `fruix system ...`
- real QEMU and XCP-ng boot validation
- a host-base-free native boot/runtime composition built from:
- `freebsd-native-kernel`
- `freebsd-native-bootloader`
- `freebsd-native-runtime`
- a declarative `freebsd-base` model that records:
- base identity
- version label
- branch
- source root
- target and kernel configuration
- side-by-side native base versions in `/frx/store`
- rollback-friendly redeploy across those declared base versions
- a documented decision to continue using host-built native base artifacts for now rather than jumping immediately to guest self-hosting
The main remaining architectural compromise is now not the runtime/boot artifact split, but the **source boundary**:
- native FreeBSD base builds still use ambient `/usr/src`
- source acquisition is not yet a first-class Fruix-managed input
- therefore reproducibility and provenance still stop short of a fully declared source story
At the same time, Fruix still lacks a real installation workflow beyond image generation and validation harnesses.
---
## Guiding Decision for the Next Milestone
The next major milestone should be:
- **declarative FreeBSD source acquisition and source pinning first**,
- then **native base builds from fetched or materialized source inputs**,
- then a **real installation story**,
- and only after that, a controlled re-evaluation of self-hosted base builds.
This means the next work should optimize for:
- explicit source identity
- stronger provenance
- side-by-side source revisions
- repeatable rebuilds from fetched or materialized source trees
- installation artifacts that are operator-usable beyond current validation harnesses
It should **not** optimize for:
- jumping straight into guest self-hosting while source identity is still weak
- building a large interactive installer before the installation primitives are clear
- replacing the current validated host-built native-base path prematurely
---
## Phase 16: Make FreeBSD Source Acquisition Declarative
This is the next architectural pivot after the native base/runtime split.
The goal is to move from:
- “build from whatever `/usr/src` is on the host”
into:
- “build from a declared FreeBSD source input with explicit provenance.”
### Intermediate Goal 16.1: Model FreeBSD Source Inputs Explicitly
**Verification Goal 16.1:** Introduce a first-class Fruix model for FreeBSD source inputs.
This can be a new `freebsd-source` record, an extension of `freebsd-base`, or another explicit source object, but it should represent at least:
- source kind:
- local checkout snapshot
- fetched Git checkout
- fetched `src.txz` archive
- source URL or local path
- branch or release line
- commit, tag, or revision when applicable
- expected tree or archive hash
- any intended patch queue or source transformation layer
This step should also document how that source object relates to:
- `freebsd-base`
- native kernel/world/runtime/bootloader/headers builds
- output identity in `/frx/store`
**Success Criteria:** Fruix can describe FreeBSD source inputs as explicit declared objects rather than only relying on ambient `/usr/src`.
### Intermediate Goal 16.2: Fetch or Materialize Source Trees Under Fruix Control
**Verification Goal 16.2:** Materialize a clean FreeBSD source tree from a declared source input using Fruix-managed fetch/materialization logic.
The first implementation may be intentionally modest, such as:
- fetching a Git checkout at a pinned revision from `https://git.FreeBSD.org/src.git`
- or downloading a `src.txz` archive and verifying its hash
- for example from `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
- or from snapshot paths such as `https://download.freebsd.org/snapshots/amd64/15.0-STABLE/src.txz`
- or snapshotting a local source tree into a content-addressed source artifact with explicit metadata
This phase should also define where downloaded or temporary source material lives, for example under:
- `/frx/var/cache/fruix/freebsd-source`
The important point is that the eventual build input should no longer be “the current mutable host tree” but a **materialized source snapshot with recorded identity**.
**Success Criteria:** Fruix can fetch or materialize a pinned FreeBSD source tree with recorded provenance and a stable identity suitable for native base builds.
### Intermediate Goal 16.3: Build Native Base Artifacts From the Declared Source Input
**Verification Goal 16.3:** Teach the native FreeBSD base build path to consume the declared source artifact rather than ambient `/usr/src`.
This should preserve the validated Plan-3/Plan-4 deployment story:
- native kernel
- native bootloader slice
- native runtime slice
- host-base-free validated path
The goal is not yet to change the successful boot/runtime model, only the **source input boundary**.
**Success Criteria:** Fruix builds native FreeBSD base artifacts from a declared source input, and the resulting store artifacts record that source identity explicitly.
---
## Phase 17: Support Multiple FreeBSD Source Revisions Side by Side
Once source acquisition is explicit, Fruix should prove that it can treat source revisions the way it already treats declared base versions: as side-by-side inputs rather than in-place mutations.
### Intermediate Goal 17.1: Support Side-by-Side FreeBSD Source Revisions in `/frx/store`
**Verification Goal 17.1:** Demonstrate that at least two distinct declared FreeBSD source inputs can coexist and produce distinct native base artifacts in `/frx/store`.
These can differ by:
- Git revision
- source archive hash
- local source snapshot hash
- or another explicit source identity boundary
The goal is to prove that the source declaration, not ambient host state, now drives the base build outputs.
**Success Criteria:** Fruix can hold at least two distinct FreeBSD source revisions side by side as meaningful native base inputs and outputs.
### Intermediate Goal 17.2: Rebuild and Boot From Two Distinct Source Revisions
**Verification Goal 17.2:** Build and boot systems from at least two distinct declared source revisions using the validated native base path.
This should preserve the current validation strategy:
- local QEMU/UEFI/TCG where useful
- real XCP-ng validation on the approved VM and approved A/B VDI path
The booted systems do not necessarily need to differ in obvious runtime behavior; the important point is that their source identity and native base outputs differ and are tracked correctly.
**Success Criteria:** Fruix boots systems built from two distinct declared FreeBSD source revisions and records those revisions in system metadata.
### Intermediate Goal 17.3: Clarify Source Provenance, Caching, and Update Policy
**Verification Goal 17.3:** Document and encode the intended policy for:
- source caching
- source refresh/invalidation
- patch application if any
- tree hashing rules
- when a new source identity should or should not invalidate native base outputs
This step should reduce ambiguity before installation artifacts and later self-hosting experiments depend on these source objects.
**Success Criteria:** the repo clearly explains how FreeBSD source objects are fetched, cached, identified, invalidated, and consumed by native base builds.
---
## Phase 18: Turn System Images Into a Real Installation Story
Fruix can already produce bootable images. The next step is to make installation itself a first-class Fruix story rather than a manual consequence of image generation.
### Intermediate Goal 18.1: Define a Minimal Non-Interactive Installation Flow
**Verification Goal 18.1:** Introduce a minimal installation workflow that can take a Fruix system artifact and install it to a target disk or image in a repeatable way.
The first version should prefer clarity and automation over interactivity. For example, it may:
- partition a target disk
- create required filesystems
- copy or materialize the selected system closure
- install boot assets
- stage activation/runtime metadata
- configure root SSH key or basic operator access
This first step should be VM-friendly and serial-console-friendly.
**Success Criteria:** Fruix can perform a repeatable installation of a declarative system onto a target disk or image without relying on ad hoc manual assembly.
### Intermediate Goal 18.2: Produce a Minimal Installer Environment
**Verification Goal 18.2:** Produce a small installer environment that can boot into a Fruix-managed install context and perform the installation workflow.
This environment may initially be one of:
- a special-purpose disk image
- a minimal recovery/install rootfs
- a proto-installer medium with only the tools needed for partitioning, filesystem creation, and deployment
The target here is not a polished live environment. It is a reliable installer substrate.
**Success Criteria:** Fruix can boot a minimal installer environment and use it to install a selected Fruix system to a target disk.
### Intermediate Goal 18.3: Build a Bootable Installer ISO
**Verification Goal 18.3:** Produce a bootable installer ISO for UEFI systems.
The first ISO can be intentionally narrow in scope. It should be enough to:
- boot under QEMU and, where practical, on the validated virtualization path
- expose the install workflow
- install a target Fruix system
- leave the machine bootable into the installed system
A polished graphical or highly interactive installer is not required here.
**Success Criteria:** Fruix can build a bootable installer ISO that installs a declarative Fruix-on-FreeBSD system.
---
## Phase 19: Strengthen System Deployment and Generation Management
The current deployment model already has the important Guix-like semantic properties at the closure/store layer. This phase is about making that story more operator-facing and less harness-specific.
### Intermediate Goal 19.1: Define a First-Class Fruix Deployment Workflow
**Verification Goal 19.1:** Define and document the canonical user-facing deployment workflow for system rebuild, image generation, installation, and rollback.
This may introduce or refine user-facing commands such as:
- `fruix system build`
- `fruix system image`
- future deployment/install/switch subcommands if justified
The key goal is clarity: operators should have an obvious Fruix way to move between generations and deployments.
**Success Criteria:** the repo documents a coherent user-facing deployment workflow for system build, install, roll-forward, and rollback.
### Intermediate Goal 19.2: Model System Generations More Explicitly
**Verification Goal 19.2:** Make the system-generation story more explicit in metadata and deployment roots.
This should include deciding how Fruix wants to represent:
- current system generation
- previous system generation
- rollback target
- GC roots associated with installed systems
This does not have to copy Guix System mechanically, but it should preserve the same important properties.
**Success Criteria:** Fruix has a clearer model for installed system generations and rollback roots rather than relying mainly on test-harness knowledge.
### Intermediate Goal 19.3: Validate Installed-System Rollback as an Operator Workflow
**Verification Goal 19.3:** Validate rollback through the intended installed-system workflow, not only through build/image test harnesses.
This step should prove that the installation and generation model work together coherently.
**Success Criteria:** an installed Fruix system can move between generations using the intended operator-facing deployment model.
---
## Phase 20: Controlled Steps Toward Self-Hosted Base Builds
Only after source identity and installation/deployment boundaries are stronger should Fruix seriously revisit self-hosted base builds.
### Intermediate Goal 20.1: Validate a Fruix-Managed Development Environment for Native Base Work
**Verification Goal 20.1:** Ensure that a Fruix-managed system can expose the development/runtime/toolchain environment needed for deeper FreeBSD-native build work.
This should build on the cleaner runtime/development split already established in Phase 14.
The goal is not yet full self-hosting; it is to prove that the system can host the tools and profiles needed for that work in a controlled way.
**Success Criteria:** a Fruix-managed system can expose a usable development environment for native FreeBSD build tasks.
### Intermediate Goal 20.2: Run Host-Initiated Native Base Builds Inside a Fruix-Managed Environment
**Verification Goal 20.2:** As an intermediate step, perform native base builds inside a Fruix-managed environment or jail while still using the host as the outer orchestrator.
This narrows the remaining gap without immediately demanding full guest self-hosting.
**Success Criteria:** Fruix can build native FreeBSD base artifacts from inside a Fruix-managed build environment, with the host still orchestrating the outer loop.
### Intermediate Goal 20.3: Reassess and Potentially Prototype Guest Self-Hosted Base Builds
**Verification Goal 20.3:** Revisit guest self-hosting only after the earlier source/install/deployment goals are complete enough to make that experiment meaningful.
At that point, the question should be answered with real evidence:
- what exactly self-hosting would improve
- what it would cost in complexity
- how it would fit with the source/deployment model already established
**Success Criteria:** the project either:
- validates a first controlled guest self-hosted base build, or
- records a clear evidence-based decision to continue preferring host-orchestrated native builds.
---
## Strategic Notes
### Why this order?
This sequence is intended to preserve the most important win from Plan 3 and Phase 15:
- Fruix already has a native, store-based, rollback-friendly FreeBSD base path
The next improvement should therefore be to make the **source input** as explicit as the **deployment output** already is.
After that, Fruix can turn its successful image-generation path into a real installation story. Only then will self-hosting be judged in the right context.
### Why not jump straight to self-hosting?
Because at the end of Phase 15, the biggest remaining weakness is not “where the build runs”, but “how explicitly the source is declared and acquired”.
If self-hosting were attempted immediately, it would mix together:
- source acquisition problems
- toolchain/profile maturity problems
- build-environment questions
- deployment/installation questions
- and virtualization/operator constraints
That would make debugging and design less clear.
### Why add installation work before self-hosting?
Because Fruix should become more usable as a system even if self-hosting remains a later milestone.
A real installation story would:
- make the current system model more operator-meaningful
- improve validation of generation/deployment semantics
- prepare the project for broader VM and eventually hardware use
- make later self-hosting experiments easier to stage and reproduce
### What success will mean
Success under this plan means the project moves from:
- “Fruix builds native FreeBSD base artifacts from the hosts current `/usr/src` and boots them successfully”
into:
- “Fruix acquires FreeBSD sources declaratively, builds native base artifacts from pinned source inputs, installs systems through a real Fruix workflow, and approaches self-hosting from a much cleaner architectural position.”

File diff suppressed because it is too large Load Diff

58
docs/PROG_SUMMARY.md Normal file
View File

@@ -0,0 +1,58 @@
# Fruix FreeBSD Progress Summary
## What we have achieved so far
Fruix now has a working end-to-end prototype of a Guix-inspired system on FreeBSD, while preserving the Fruix product boundary (`fruix`, `/frx`) and the core model: declarative builds, content-addressed store paths, daemon-mediated privilege separation, garbage-collection roots, derivations, and reproducible system artifacts.
Completed milestones include:
- **Core runtime/build foundation**: validated Guile on FreeBSD, diagnosed the packaged-Guile subprocess crash, and established a working local Guile path with the upstream FreeBSD fix.
- **Store and daemon path**: built the first Fruix daemon/store workflow on FreeBSD, including jail/build-user isolation, store population under `/frx/store`, derivation generation, and minimal RPC integration.
- **Real package builds**: adapted enough of the GNU build/package path to build real package outputs into `/frx/store` and install a minimal profile.
- **System model**: introduced a declarative FreeBSD `operating-system` model, reproducible system closures, rootfs generation, and raw image generation as first-class system artifacts.
- **Bootable VM images**: built FreeBSD images that boot reproducibly and stage the full `/frx/store` into the guest image.
- **Real VM validation on XCP-ng**: proved the system boots on the real target VM, gets DHCP, starts SSH, and exposes a usable guest for validation.
- **User-facing CLI**: added `bin/fruix` with `fruix system build|rootfs|image`, making system artifact generation a real Fruix workflow instead of a phase-specific prototype.
- **Boot architecture progress**: validated both boot modes on the real XCP-ng VM:
- `freebsd-init+rc.d-shepherd`
- `shepherd-pid1`
- **Native runtime cleanup**: removed the guest runtimes dependence on `/tmp/*-validate-install` compatibility-prefix symlinks for Guile, guile-extra, and Shepherd. The guest now boots and runs Guile/Shepherd from the store-backed runtime layout without those temporary aliases.
- **Native FreeBSD base artifacts**: replaced the validated host-copy boot/runtime path with native `/usr/src`-built artifacts in `/frx/store` for:
- `freebsd-native-kernel`
- `freebsd-native-bootloader`
- `freebsd-native-runtime`
- **Declarative FreeBSD base model**: the FreeBSD base is now an explicit system input via `freebsd-base`, not just an ambient property of the builder host.
- **Declarative FreeBSD source model and materialization**: Fruix can now describe FreeBSD sources explicitly via `freebsd-source` and materialize them from:
- local source trees
- `https://git.FreeBSD.org/src.git`
- official `src.txz` archives such as `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
into `/frx/store`, with cache-backed provenance under `/frx/var/cache/fruix/freebsd-source`.
- **Source-driven native base builds**: native FreeBSD kernel/bootloader/runtime artifacts now consume those materialized source snapshots rather than ambient `/usr/src`, and their build metadata records both the declared source and the effective materialized source identity.
- **Side-by-side source revisions**: Fruix can now keep distinct FreeBSD source identities side by side in `/frx/store` and produce distinct native base outputs from them, even when the visible base version label is held constant.
- **Source-driven boot validation**: Fruix can now also boot systems built from distinct declared FreeBSD source revisions while preserving those source identities in image/build metadata.
- **Explicit source policy**: the repo now records how FreeBSD source objects are fetched, cached, identified, invalidated, and consumed by native base builds in `docs/freebsd-source-policy.md`.
- **Minimal installation workflow**: Fruix now has a non-interactive `fruix system install` path that can partition, format, populate, and boot a target image or disk from a declarative system closure.
- **Minimal installer environment**: Fruix can now also build and boot a dedicated installer image that carries a selected target closure, installs it onto a second disk from inside the guest, and leaves the installed target bootable.
- **Base upgrade story**: Fruix can now keep distinct declared base versions side by side in `/frx/store` and roll forward / back between them through the normal system deployment flow.
## Major pain points now behind us
- **Guile subprocess instability on FreeBSD**: root-caused and bypassed with the validated local Guile build.
- **“Prototype only” store/build flow**: replaced with real derivations, store outputs, and system artifacts under `/frx/store`.
- **Non-booting early images**: fixed rootfs/image composition, GPT sizing issues for XCP-ng, and early rc/fstab problems.
- **No practical VM validation path**: when local bhyve proved impossible under Xen, validation successfully pivoted to the real XCP-ng VM and existing VDI.
- **Guest runtime crashes from locale/prefix issues**: fixed enough locale/runtime staging and later removed dependence on guest-side compatibility-prefix shims.
- **Shepherd only as a bridged service**: Fruix has now proven Shepherd can run as PID 1 on FreeBSD, not just behind FreeBSD `init` + `rc.d`.
## Major pain points still ahead
- **True store-native runtime artifacts**: some historical build/install prefixes are still embedded in binaries and metadata. They are no longer required at runtime, but the local Guile/guile-extra/Shepherd build/install flow should still be moved to a genuinely store-native prefix from the start.
- **Installer media beyond disk images**: Fruix now has both a host-driven install path and a bootable installer environment, but it still lacks a UEFI installer ISO and a more polished operator-facing installation medium.
- **Boot-path simplification**: Fruix now supports both the legacy `freebsd-init+rc.d-shepherd` path and the more Guix-like `shepherd-pid1` path. We still need to decide whether Shepherd PID 1 becomes the preferred/default architecture.
- **Reduce transitional FreeBSD glue**: more of the current bootstrap/activation/runtime setup should become cleaner and less prototype-specific over time.
- **Tooling and platform constraints**: local bhyve remains blocked by missing nested virtualization under Xen, and XO permissions still prevent creating/importing new VDIs; current validation must keep reusing the approved VM/VDI path.
- **Remaining ecosystem gaps**: some deeper Guix-like features are still immature on FreeBSD, especially around native package-management ergonomics and fully polished runtime/deployment behavior.
## Bottom line
Fruix has crossed the most important threshold: it is no longer just a collection of isolated FreeBSD experiments. It can now build declarative FreeBSD system artifacts, boot them on the real target VM, reach the network, serve SSH, run Shepherd as PID 1, operate from `/frx` without depending on temporary runtime-prefix shims, build native FreeBSD base artifacts into `/frx/store`, roll forward / back between declared base versions, materialize declared FreeBSD source inputs into `/frx/store`, drive native base builds from those materialized source snapshots, boot systems from distinct source revisions, explain the source provenance/invalidation rules explicitly, install a declarative system onto a target image through a repeatable Fruix workflow, and boot a dedicated Fruix-managed installer environment that performs that installation from inside the guest. The biggest remaining work is no longer “can this build/install at all?” but “how does this become a fuller installer/deployment/generation/installer-media story?”

View File

@@ -1,4 +1,4 @@
Your task is described in ./docs/PLAN_2.md. Current progress is stored in ./docs/PROGRESS.md.
Your task is described in ./docs/PLAN_3.md. Current progress is stored in ./docs/PROGRESS.md.
Perform the next step towards the final goal. Update the progress file and `git commit` after each subphase (or even in between, if adequate).

View File

@@ -0,0 +1,330 @@
# Fruix FreeBSD Source Policy
This document records the current intended policy for how Fruix models, fetches, caches, identifies, invalidates, and consumes FreeBSD source inputs.
It reflects the behavior implemented through Plan 4 Phases 16 and 17.
## 1. Source object model
Fruix represents FreeBSD source inputs explicitly with `freebsd-source`.
Currently supported source kinds are:
- `local-tree`
- `git`
- `src-txz`
A source object records some combination of:
- `name`
- `kind`
- `url`
- `path`
- `ref`
- `commit`
- `sha256`
Fruix distinguishes between:
- the **declared source**
- what the operator asked for
- the **effective source**
- what Fruix actually resolved/materialized and built from
That distinction matters most for Git refs and local-tree snapshots.
## 2. Source identity boundaries
### `local-tree`
Declared selector:
- local filesystem path
Effective identity boundary:
- filtered source-tree hash computed from `mtree`
Current rule:
- Fruix computes source identity from:
- `mtree -c -k type,link,size,mode,sha256digest`
- comment lines are removed before hashing
This avoids unstable `mtree` comment headers such as:
- date
- user
- machine
So the effective local-tree identity is content-oriented rather than host-comment-oriented.
### `git`
Declared selectors:
- `commit`, or
- `ref`
Effective identity boundary:
- resolved commit
Current rule:
- if `commit` is present, Fruix uses it as the fetch selector
- otherwise, Fruix uses `ref`
- after fetch, Fruix records the resolved commit as the effective Git identity
Policy consequence:
- a Git **ref** is a convenience selector
- a Git **commit** is the reproducibility boundary
Therefore:
- use `ref` alone for exploratory/latest tracking when drift is acceptable
- use `commit` for reproducible builds, side-by-side comparisons, installation artifacts, and rollback-sensitive workflows
- it is valid to record both:
- `ref` for human provenance
- `commit` for stable identity
### `src-txz`
Declared selectors:
- URL
- expected `sha256`
Effective identity boundary:
- verified archive `sha256`
Policy consequence:
- `src-txz` materialization is only valid when `sha256` is declared
- archive URL alone is not enough
## 3. Cache policy
Default cache root:
- `/frx/var/cache/fruix/freebsd-source`
Current cache layout:
- Git:
- `/frx/var/cache/fruix/freebsd-source/git/<hash>.git`
- release/snapshot archives:
- `/frx/var/cache/fruix/freebsd-source/archives/<hash>-src.txz`
### Git cache behavior
Fruix keeps a bare repository cache keyed by source URL.
Current behavior:
- initialize bare repo if absent
- ensure `origin` matches the declared URL
- fetch the requested selector from `origin`
- archive the resolved commit into the materialized store object
Policy:
- the cache is a transport/proxy optimization, not the identity boundary
- the identity boundary is the resolved commit recorded in the effective source and materialization metadata
### `src-txz` cache behavior
Fruix keeps the downloaded archive under the cache root.
Current behavior:
- if a cached archive exists, hash it
- if its hash does not match the declared `sha256`, delete it
- fetch the archive if missing
- verify the downloaded archive hash
- fail if the verified hash does not match the declared `sha256`
Policy:
- the cache is reusable only when the declared hash still matches
- hash mismatch invalidates the cached archive immediately
### `local-tree`
Current behavior:
- no separate network cache
- Fruix snapshots the local tree into a materialized store object
Policy:
- the local path is only a selector to a mutable host tree
- the materialized snapshot and its filtered tree hash are the meaningful Fruix identity boundary
## 4. Materialized source store policy
Materialized FreeBSD sources are stored in:
- `/frx/store/*-freebsd-source-*`
Current manifest inputs for a materialized source object include:
- materializer version
- declared source
- effective source
- source identity tuple
- for example resolved commit, archive sha256, or local tree hash
Policy consequence:
- changing any of those identity inputs should produce a distinct source store path
- changing materialization semantics should bump the materializer version
Current materializer version:
- `freebsd-source-materializer-version = "2"`
## 5. Effective source root policy
Not every source unpacks to the same top-level directory shape.
Current behavior:
- if `tree/Makefile` exists, effective root is:
- `tree`
- else if `tree/usr/src/Makefile` exists, effective root is:
- `tree/usr/src`
This is why:
- Git exports materialize effectively at `.../tree`
- official `src.txz` archives materialize effectively at `.../tree/usr/src`
Policy:
- native builds must consume the detected effective source root, not assume `/usr/src`
- the declared transitional `source-root` may still be recorded for provenance, but it is not the effective build root once materialization is in use
## 6. Native build invalidation policy
Native FreeBSD kernel/world/runtime/bootloader outputs must be invalidated by source identity, not just by package name/version.
Current behavior:
- native package materialization rewrites the install plan to use the materialized source root
- native manifests record both:
- `declared-source`
- `materialized-source`
- package materialization caching keys on the full manifest identity rather than only package name/version
- native build common metadata includes:
- `source-root`
- `source-tree-sha256`
- kernconf hash
- target metadata
Policy consequence:
- two builds with the same visible base version label but different source identities must still produce different native output store paths
- that behavior is intentional and required
## 7. Closure provenance policy
System closures and images should preserve enough source metadata to explain exactly what source snapshot was used.
Current closure/image metadata includes:
- `metadata/freebsd-source.scm`
- `metadata/freebsd-source-materializations.scm`
- `materialized_source_store_count`
- `materialized_source_stores`
Native `.freebsd-native-build-info.scm` records:
- declared source
- materialized source store path
- materialized source root
- materialized source tree hash
- effective Git commit or archive hash when applicable
Policy:
- source provenance is part of the closure boundary, not just a transient fetch detail
## 8. Update policy
### Recommended operator policy
For reproducible and rollback-sensitive workflows:
- prefer Git sources pinned by `commit`
- prefer archive sources pinned by `sha256`
- treat `local-tree` as a development/debugging input unless the resulting materialized snapshot is itself the explicit artifact being compared
### Moving refs
A moving Git ref is expected to drift over time.
Policy:
- if only `ref` is declared, Fruix may legitimately produce a new materialized source store later
- that new materialized source identity should then invalidate native outputs
- this is expected behavior, not a cache bug
### Installed/deployed systems
For installation artifacts and generation management, the source identity should be stable enough to answer:
- what exact source revision produced this system?
- can it be rebuilt later?
- should this update be considered a new generation boundary?
That means later installation/deployment work should prefer:
- Git commit-pinned sources
- hash-pinned archives
rather than floating refs alone.
## 9. Patch/transformation policy
Fruix does not yet expose a first-class patch queue or transformation layer on top of `freebsd-source`.
Current policy until that exists:
- the declared source object should be treated as the full upstream-source identity boundary
- when a patch/transformation layer is introduced later, it must become part of the materialized source identity and manifest versioning
In other words:
- applying patches without changing source identity metadata would be wrong
## 10. Relation to Guix-inspired semantics
This policy follows the same important high-level idea as Guix:
- selectors are not enough
- resolved, content-stable identities matter
- cached transport objects are not the same as reproducible store identities
Fruix applies that idea in FreeBSD-specific form:
- `src.txz` handling
- FreeBSD source-tree effective-root detection
- mtree-based FreeBSD tree identity
- native FreeBSD base builds driven from materialized source snapshots under `/frx/store`
## 11. Practical summary
Use these rules:
- **Want reproducibility?**
- pin Git by `commit`
- pin archives by `sha256`
- **Want side-by-side comparison?**
- keep distinct source objects and let Fruix materialize them separately
- **Want native base outputs to differ only when they should?**
- rely on materialized source identity, not mutable `/usr/src`
- **Want stable deployment provenance?**
- preserve the closure metadata files and materialized source store references

View File

@@ -0,0 +1,163 @@
# Phase 10 completion: canonical FreeBSD system workflows now use `fruix system`
Date: 2026-04-02
## Goal
Complete the current Optional Phase 10 track by moving the existing FreeBSD system workflows away from phase-specific direct materializer entry points and toward the real Fruix command surface introduced in Phase 10.1.
Concretely, this subphase targeted three things:
- make the existing system validation harnesses call `bin/fruix` as their canonical frontend,
- keep the deeper static and boot validations intact after that refactor,
- and prove that the real XCP-ng deployment path still works when the image is produced through the `fruix system` path.
## Result
This refactor succeeded.
The main static system harnesses no longer invoke the Scheme materializer runners directly as their primary frontend. They now call the real Fruix command and then perform their existing validation logic on the resulting artifacts.
Updated harnesses:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase7-rootfs.sh`
- `tests/system/run-phase8-system-image.sh`
These harnesses now use:
- `bin/fruix system build`
- `bin/fruix system rootfs`
- `bin/fruix system image`
The Phase 9 XCP-ng boot path benefits from this automatically because:
- `tests/system/run-phase9-xcpng-boot.sh`
still calls `tests/system/run-phase8-system-image.sh`,
- and that Phase 8 harness now builds the image through `bin/fruix`.
## Code-level changes
### `scripts/fruix.scm`
Extended the `fruix system build` metadata so callers can recover more of the closure structure without having to re-enter the Scheme materializer directly.
Added emitted fields for:
- `ready_marker`
- `base_package_store_count`
- `base_package_stores`
That made it practical for the refactored shell harnesses to keep their old validation/reporting fidelity while using the new CLI frontend.
### `tests/system/run-phase7-system-closure.sh`
Reworked to:
- call `bin/fruix system build` twice,
- validate closure reproducibility through the command frontend,
- validate closure contents and key symlink targets,
- and record metadata showing `frontend_invocation=/.../bin/fruix system build`.
### `tests/system/run-phase7-rootfs.sh`
Reworked to:
- call `bin/fruix system rootfs`,
- validate the generated rootfs symlink structure and content expectations,
- and record metadata showing `frontend_invocation=/.../bin/fruix system rootfs`.
### `tests/system/run-phase8-system-image.sh`
Reworked to:
- call `bin/fruix system image`,
- preserve the existing GPT/filesystem/mount/static-layout validation logic,
- and record metadata showing `frontend_invocation=/.../bin/fruix system image`.
## Validation
### Static workflow validation
Successful closure validation:
- `PASS phase7-system-closure`
- workdir: `/tmp/phase10-canon-closure2-1775119728`
Successful rootfs validation:
- `PASS phase7-rootfs`
- workdir: `/tmp/phase10-canon-rootfs3-1775120391`
Successful image validation:
- `PASS phase8-system-image`
- workdir: `/tmp/phase10-canon-image-1775120548`
The resulting metadata confirms that all of those paths are now driven by `bin/fruix`.
### Real XCP-ng regression validation
To ensure this was not merely a static refactor, the full real-VM path was rerun after the harness changes.
Successful real boot validation:
- `PASS phase9-xcpng-boot`
- workdir: `/tmp/phase10-canon-xcpng-1775120869`
That successful rerun confirmed that the `fruix system image`-driven path still produces an image that can:
- be converted to dynamic VHD,
- be imported into the existing XCP-ng VDI,
- boot on VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`,
- reach DHCP and SSH,
- keep Shepherd running,
- and reach the ready marker.
Representative final guest state from that pass:
```text
ready_marker=ready
shepherd_status=running
sshd_status=running
run_current_system_target=/frx/store/0fe459ea22156510e64cea794b7a001151b59625bd5f12a488d6851e1c6d2198-fruix-system-fruix-freebsd
boot_backend=xcp-ng-xo-cli
```
## Why this completes Phase 10 on the current track
By the end of Phase 9, Fruix already had:
- a declarative system model,
- real closure and image outputs in `/frx/store`,
- and a booted FreeBSD VM with Shepherd and SSH.
What was still transitional was the operator/tooling layer: too much of the system workflow was still centered on phase-specific scripts invoking internal materializers directly.
After Phase 10.1 and this completion step, the current track now has:
- a user-facing Fruix command: `bin/fruix`
- real system actions under that command:
- `system build`
- `system rootfs`
- `system image`
- existing validation/deployment workflows using that command as their canonical frontend
- and successful regression validation on the real XCP-ng guest path
That is enough to treat Optional Phase 10 as complete for the current FreeBSD prototype track.
## Remaining follow-up work beyond Phase 10
There is still good follow-up work available, but it is no longer required to say that Fruix has crossed from “prototype scripts only” into a real OS-tooling shape.
Useful future cleanup includes:
- replacing the current Guile/Shepherd compatibility-prefix shims with a more native runtime arrangement,
- polishing residual boot noise and base-service rough edges,
- and extending `fruix` with richer deploy/vm-oriented commands.
## Conclusion
Optional Phase 10 is now complete on the current track.
Fruix no longer just has a declarative FreeBSD system implementation internally; it now has a real Fruix command surface that is used by the canonical closure/rootfs/image workflows, and that command path has been validated all the way through a successful real XCP-ng boot.

View File

@@ -0,0 +1,149 @@
# Phase 10.1: Add a real `fruix system` command for FreeBSD system artifacts
Date: 2026-04-02
## Goal
Start Optional Phase 10 by replacing one of the remaining prototype-only workflows with a real user-facing Fruix command.
The concrete target for this subphase was:
- expose the declarative FreeBSD operating-system machinery through a true `fruix` CLI entry point,
- stop relying solely on ad hoc phase-specific harness scripts for closure/image materialization,
- and validate that the new command can build the existing declarative system outputs.
## Result
A minimal user-facing `fruix` command now exists in this repository.
New entry points:
- `bin/fruix`
- `scripts/fruix.scm`
Supported commands in this first cut:
- `fruix system build OS-FILE`
- `fruix system image OS-FILE`
- `fruix system rootfs OS-FILE ROOTFS-DIR`
Supported options:
- `--system NAME`
- `--store DIR`
- `--disk-capacity SIZE`
- `--rootfs DIR`
- `--help`
The command loads a declarative operating-system definition from a Scheme file, resolves the selected operating-system binding, and then calls the existing Fruix FreeBSD system materializers directly:
- `materialize-operating-system`
- `materialize-rootfs`
- `materialize-bhyve-image`
The command prints machine-readable `key=value` metadata for the produced artifacts.
## Why this matters
Up through Phase 9, the project had already achieved a real booted Fruix guest, but the operator-facing path for producing those artifacts still lived mostly in phase-specific test harnesses such as:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase8-system-image.sh`
- `tests/system/run-phase9-xcpng-boot.sh`
Those were valuable validation tools, but they were not yet a real Fruix interface.
This subphase moves the project one step closer to a genuine OS workflow by making system artifact generation available through a Fruix CLI surface.
## Implementation details
### `bin/fruix`
Added a shell wrapper that:
- locates the repository root,
- locates the Guile and Guile-extra validation prefixes already used elsewhere in the project,
- ensures the required local Guile/Fibers/Shepherd pieces exist,
- prepares `GUILE_LOAD_PATH` for:
- `modules/`
- `~/repos/guix`
- and runs the Guile entry point.
This keeps the new command aligned with the currently validated local FreeBSD Guile toolchain rather than inventing a separate runtime path.
### `scripts/fruix.scm`
Added the actual CLI implementation.
It currently:
- parses `fruix system ...` actions,
- supports `--help`,
- loads an operating-system definition file,
- resolves the selected OS variable via `--system NAME` or a small fallback list of conventional names,
- validates the operating-system object,
- dispatches to the existing FreeBSD system materializers,
- and emits stable `key=value` metadata.
### `tests/system/run-phase10-fruix-system-command.sh`
Added a dedicated validation harness for the new command.
The test verifies that:
1. `fruix system build` materializes a closure under `/frx/store`
2. the returned closure contains the generated activation path
3. `fruix system image` materializes a disk image under `/frx/store`
4. the command emits the expected metadata fields for both operations
## Validation
Successful validation run:
- `PASS phase10-fruix-system-command`
- workdir: `/tmp/phase10-fruix-cmd-1775117490`
The new command successfully produced both:
- a Fruix system closure path under `/frx/store/*-fruix-system-fruix-freebsd`
- a Fruix image path under `/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img`
Example usage:
```sh
sudo env HOME="$HOME" ./bin/fruix system build \
tests/system/phase7-minimal-operating-system.scm \
--system phase7-operating-system
```
```sh
sudo env HOME="$HOME" ./bin/fruix system image \
tests/system/phase7-minimal-operating-system.scm \
--system phase7-operating-system \
--disk-capacity 5g
```
## Current limitations
This is intentionally a first Phase 10 step, not the final Fruix command surface.
Notable current limitations:
- system/image materialization still expects the currently validated local Guile/Shepherd build prefixes
- writing into `/frx/store` and building disk images still typically requires `sudo`
- the command currently targets only the FreeBSD system prototype path already implemented in `modules/fruix/system/freebsd.scm`
- it does not yet integrate with the larger upstream Guix command framework; it is a Fruix-native CLI wrapper in this repo
## Assessment
This subphase successfully replaces an important transitional layer with a more OS-like Fruix interface.
The project can now say not only that it can declaratively define and boot a Fruix FreeBSD system, but also that it has a direct user-facing command for materializing the resulting system artifacts.
## Recommended next step
Continue Phase 10 by reducing another transitional seam behind `fruix system`, most likely one of:
1. add a `fruix system vm` / deploy-oriented flow on top of the now-working image path
2. replace the current compatibility symlink handling for locally built Guile/Shepherd prefixes with a more native store-path-aware runtime arrangement
3. factor the phase-specific system test harnesses to call `bin/fruix` directly where practical, so the command becomes the canonical operator path rather than just an additional wrapper

View File

@@ -0,0 +1,129 @@
# Phase 12.3: made the host-staged FreeBSD base boundary explicit
Date: 2026-04-02
## Goal
The current Fruix FreeBSD system still uses a transitional base layer assembled by copying selected files from the builder host. That was already true in practice, but the code needed to make this boundary explicit so the later move toward native `world`/`kernel` artifacts is easier to reason about.
The goal of this subphase was therefore not to change how the system boots, but to clarify in both code and generated metadata:
- which package sets are still host-staged FreeBSD base packages
- that this is an explicit transitional boundary
- what the first intended replacement order is for native base-build work
## Implementation
### 1. Explicit host-staged package sets in `modules/fruix/packages/freebsd.scm`
The package module now exports named transitional sets instead of only generic lists:
- `%freebsd-host-staged-all-packages`
- `%freebsd-host-staged-core-packages`
- `%freebsd-host-staged-development-profile-packages`
- `%freebsd-host-staged-system-packages`
- `%freebsd-host-staged-replacement-order`
- `freebsd-host-staged-package?`
The older names remain as compatibility aliases:
- `%freebsd-core-packages`
- `%freebsd-development-profile-packages`
- `%freebsd-system-packages`
This preserves existing callers while making the transitional nature of the current base model visible in the code.
### 2. Replacement order is now encoded directly in the package layer
The new `%freebsd-host-staged-replacement-order` documents the intended first replacement sequence:
1. `freebsd-kernel`, `freebsd-bootloader`
2. `freebsd-runtime`, `freebsd-libc`, `freebsd-userland`, `freebsd-rc-scripts`
3. `freebsd-networking`, `freebsd-openssh`
4. `freebsd-kernel-headers`, `freebsd-clang-toolchain`
5. `freebsd-gmake`, `freebsd-autotools`, `freebsd-openssl`, `freebsd-zlib`, `freebsd-sh`, `freebsd-bash`
This is not yet native base-build implementation, but it makes the intended transition path concrete.
### 3. The closure metadata now carries that boundary too
`modules/fruix/system/freebsd.scm` now records the replacement order directly in:
- `metadata/store-layout.scm`
alongside:
- host-staged base store paths
- Fruix runtime store paths
- init mode
This means the generated closure itself now explains both:
- what part of the current system still comes from the host-staged FreeBSD base, and
- what part is expected to be replaced first by future native base artifacts.
## Validation
### Package-layer validation
Confirmed directly with Guile:
```scheme
(use-modules (fruix packages freebsd))
(freebsd-host-staged-package? freebsd-runtime) => #t
%freebsd-host-staged-replacement-order
```
Observed output included:
```text
#t
((first-wave freebsd-kernel freebsd-bootloader)
(second-wave freebsd-runtime freebsd-libc freebsd-userland freebsd-rc-scripts)
...)
```
### System-closure validation
Passing run:
- `PASS phase7-system-closure`
- workdir: `/tmp/phase12-3-closure-1775162784`
The generated closure metadata file:
- `/frx/store/25ae9bb85da60b8c77971325e0e11d5390a064132a35e1bab0866cabb802a606-fruix-system-fruix-freebsd/metadata/store-layout.scm`
now records:
- `host-base-stores`
- `fruix-runtime-stores`
- `host-base-replacement-order`
with content including:
```text
(host-base-replacement-order
(first-wave freebsd-kernel freebsd-bootloader)
(second-wave freebsd-runtime freebsd-libc freebsd-userland freebsd-rc-scripts)
...)
```
## Assessment
This subphase makes the current architectural compromise explicit instead of leaving it implicit in naming and package-list accidents.
That matters because Fruix is now moving into the phase where native FreeBSD base artifacts should start displacing the host-copy model. The system now has a clearer answer to the question:
- what exactly is still transitional,
- and in what order should it be retired?
## Next recommended step
Phase 12 is now complete.
The next major step is Phase 13:
- start modeling and producing native FreeBSD `world`/`kernel` artifacts from `/usr/src`
- stage them in `/frx/store`
- and begin replacing the current host-staged base boundary with those new outputs

View File

@@ -0,0 +1,161 @@
# Phase 12.1: deployment provenance and diagnostic metadata for the current FreeBSD pipeline
Date: 2026-04-02
## Goal
Before starting native FreeBSD base builds from `/usr/src`, the current host-staged pipeline needed better provenance and easier diagnosis.
The specific targets for this subphase were:
- record which host-side FreeBSD base inputs were used for a generated closure/image
- make the closure itself carry that provenance
- distinguish host-staged FreeBSD base stores from Fruix-built runtime stores
- improve the validation harnesses so later failures are easier to investigate
## Implementation
### 1. Closure-level provenance files
`modules/fruix/system/freebsd.scm` now generates and stores two explicit metadata files in the system closure:
- `metadata/host-base-provenance.scm`
- `metadata/store-layout.scm`
These record:
- host `freebsd-version -kru`
- host `uname -a`
- `/usr/src` path
- `/usr/src` git revision/branch when available
- `newvers.sh` SHA256 as a stable source-tree identifier fallback
- exact host-staged FreeBSD base store paths used by the closure
- exact Fruix runtime store paths used by the closure
- selected init mode
This makes the generated closure self-describing instead of requiring ad hoc host inspection.
### 2. `fruix system` metadata became more explicit
`scripts/fruix.scm` now emits additional machine-readable metadata for both:
- `fruix system build`
- `fruix system image`
New fields include:
- `host_base_store_count`
- `host_base_stores`
- `fruix_runtime_store_count`
- `fruix_runtime_stores`
- `host_base_provenance_file`
- `store_layout_file`
- `host_freebsd_version`
- `host_uname`
- `usr_src_git_revision`
- `usr_src_git_branch`
- `usr_src_newvers_sha256`
This gives later harnesses and operators a clean way to identify exactly what kind of system/image was produced.
### 3. Validation harnesses now assert provenance availability
Updated harnesses:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase8-system-image.sh`
They now validate that provenance fields are present and that the generated closure contains the new metadata files.
### 4. Runtime-diagnostic capture was expanded in the VM harnesses
Updated harnesses:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
These now collect additional guest-side diagnostic tails where available, including:
- `shepherd-bootstrap.out`
- `shepherd.log`
- recent `dmesg`
This does not change the boot architecture, but it improves post-failure visibility.
### 5. Host-side validation became more stable
The host-side closure/image harnesses now force:
- `GUILE_AUTO_COMPILE=0`
when invoking `fruix` under `sudo env`.
This avoided a misleading one-time validation drift caused by mixed compiled/source host state during back-to-back reproducibility checks immediately after source edits.
## Validation
### Phase 7 system closure
Passing run:
- `PASS phase7-system-closure`
- workdir: `/tmp/phase12-1b-closure-1775157039`
Key new metadata included:
```text
host_base_store_count=8
fruix_runtime_store_count=3
host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm
store_layout_file=/frx/store/.../metadata/store-layout.scm
host_freebsd_version=15.0-STABLE
usr_src_newvers_sha256=d6f6e9ab352d3f6281e788c78a63ac311ab7a3a4bb5dfc0016ed0aadb90b5d9d
```
### Phase 8 system image
Passing run:
- `PASS phase8-system-image`
- workdir: `/tmp/phase12-1b-image-1775157039`
Key new metadata included:
```text
host_base_store_count=8
fruix_runtime_store_count=3
host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm
store_layout_file=/frx/store/.../metadata/store-layout.scm
host_freebsd_version=15.0-STABLE
host_uname=FreeBSD fruixdev 15.0-STABLE ...
```
### Harness validation
Syntax-checked successfully:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
## Assessment
This subphase does not replace the host-staged FreeBSD base model yet. Instead, it makes that transitional model much easier to reason about.
Generated systems/images now answer the key questions directly:
- which FreeBSD base inputs came from the host?
- which runtime artifacts came from Fruix-built Guile/Shepherd outputs?
- what `/usr/src` identity was available at build time?
- where should an operator look first when the VM boot/runtime path misbehaves?
That is sufficient to support the next hardening step and then the later move toward native FreeBSD world/kernel artifacts in `/frx/store`.
## Next recommended step
Proceed to Phase 12.2:
- improve runtime/operator diagnostics inside the guest itself
- reduce the remaining noisy boot rough edges where the fixes are small and local
- keep the validated deployment path stable while preparing for native base-build work

View File

@@ -0,0 +1,210 @@
# Phase 12.2: tightened guest runtime diagnostics and reduced early-boot rough edges
Date: 2026-04-02
## Goal
The current Fruix FreeBSD guest already booted, reached the network, and ran Shepherd. The next hardening step was to make the guest easier to diagnose and to remove a few distracting runtime rough edges that were still coming from the prototype-style `/etc` handling.
The main targets were:
- reduce early-boot/login-class noise around `/etc/login.conf`
- ensure the FreeBSD password/login databases are created in writable guest state, not implicitly in store-backed paths
- add a clear activation log inside the guest
- teach the validation harnesses to assert the new behavior directly
## Root cause addressed
The old rootfs layout symlinked several generated `/etc` files directly to `/run/current-system/etc/...`, including:
- `/etc/login.conf`
- `/etc/master.passwd`
- `/etc/passwd`
- `/etc/group`
That worked for static lookup, but it was a poor fit for FreeBSD tools such as:
- `cap_mkdb`
- `pwd_mkdb`
- early login-class lookups
because those tools expect to work with regular files and adjacent generated database files such as:
- `/etc/login.conf.db`
- `/etc/pwd.db`
- `/etc/spwd.db`
With symlink-backed inputs, the system could still limp along, but it produced avoidable warnings and less clear runtime behavior.
## Implementation
### 1. Selected `/etc` files now become regular files in the guest rootfs
`modules/fruix/system/freebsd.scm` now materializes these files as regular files in the rootfs instead of symlinks:
- `/etc/passwd`
- `/etc/master.passwd`
- `/etc/group`
- `/etc/login.conf`
Other generated configuration files that do not need this treatment remain symlinked to `/run/current-system/...`.
This gives FreeBSD's database tools writable, regular inputs in the guest filesystem while keeping the current system closure as the source of truth.
### 2. Activation now refreshes those files from `/run/current-system/etc`
The generated activation script now refreshes the regular guest copies from the currently selected system closure before rebuilding databases.
That means rebuild/redeploy still works coherently even though these specific files are no longer left as symlinks in the booted guest.
### 3. Activation now records a guest-visible log
Activation now writes:
- `/var/log/fruix-activate.log`
with explicit markers such as:
- `fruix-activate:start`
- `fruix-activate:cap_mkdb=ok`
- `fruix-activate:pwd_mkdb=ok`
- `fruix-activate:done`
- exit status marker from the shell trap
This gives a direct guest-side indicator of whether activation actually completed.
### 4. Closure permissions were tightened slightly
The generated closure now explicitly sets:
- `etc/master.passwd` => `0600`
before the rootfs/image path copies it into the guest.
### 5. Validation harnesses were upgraded
#### Local image validation
`tests/system/run-phase8-system-image.sh` now asserts that the mounted image contains:
- `/etc/login.conf` as a regular file
- `/etc/master.passwd` as a regular file
#### VM validation
These harnesses now check for the new runtime behavior:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
New checks include:
- `login_conf_kind=regular`
- `login_conf_db=present`
- `pwd_dbs=present`
- activation log contains `fruix-activate:done`
They also capture the activation log into metadata.
### 6. Small follow-up fix
The first activation-log implementation briefly used `touch`, which is not staged in the minimal guest userland. This was corrected by switching to shell redirection:
- `: >> "$logfile"`
so the activation path no longer depends on an extra utility for log-file creation.
## Validation
### Local image structure
Passing run:
- `PASS phase8-system-image`
- workdir: `/tmp/phase12-2-image-1775159011`
Key metadata:
```text
login_conf_kind=regular
master_passwd_kind=regular
```
### Local QEMU Shepherd PID 1
Passing run:
- `PASS phase11-shepherd-pid1-qemu`
- workdir: `/tmp/phase12-2b-qemu-1775161367`
Key metadata:
```text
activate_log=fruix-activate:start fruix-activate:cap_mkdb=ok fruix-activate:pwd_mkdb=ok fruix-activate:done fruix-activate:exit status=0
login_conf_kind=regular
login_conf_db=present
pwd_dbs=present
shepherd_pid=1
sshd_status=running
```
### Real VM, `freebsd-init+rc.d-shepherd`
Passing run:
- `PASS phase9-xcpng-boot`
- workdir: `/tmp/phase12-2b-phase9-1775161731`
Key metadata:
```text
activate_log=fruix-activate:start fruix-activate:cap_mkdb=ok fruix-activate:pwd_mkdb=ok fruix-activate:done fruix-activate:exit status=0
login_conf_kind=regular
login_conf_db=present
pwd_dbs=present
compat_prefix_shims=absent
guile_module_smoke=ok
shepherd_status=running
sshd_status=running
```
### Real VM, `shepherd-pid1`
Passing run:
- `PASS phase11-shepherd-pid1-xcpng`
- workdir: `/tmp/phase12-2b-phase11-1775162210`
Key metadata:
```text
activate_log=fruix-activate:start fruix-activate:cap_mkdb=ok fruix-activate:pwd_mkdb=ok fruix-activate:done fruix-activate:exit status=0
login_conf_kind=regular
login_conf_db=present
pwd_dbs=present
shepherd_pid=1
compat_prefix_shims=absent
guile_module_smoke=ok
sshd_status=running
```
## Assessment
This was a small but high-value hardening step.
The guest now behaves more like a real FreeBSD system in one of the places where a store-backed prototype can otherwise feel awkward: password/login database management and early `/etc` expectations.
The important result is not cosmetic; it is operational:
- activation success is now visible inside the guest
- the login/password database inputs live as regular guest files where FreeBSD expects them
- both validated boot modes still work locally and on the real XCP-ng VM
## Next recommended step
Proceed to Phase 12.3:
- make the host-staged FreeBSD base boundary explicit in the code/documentation model
- group the transitional host-copy base packages more clearly
- document the first intended replacement order for native world/kernel artifacts in `/frx/store`

View File

@@ -0,0 +1,191 @@
# Phase 13.3: booted Fruix using native store-built FreeBSD base artifacts
Date: 2026-04-03
## Goal
Phase 13.3 was the first end-to-end boot validation of the new native FreeBSD base path.
The target was a Fruix system/image that boots using:
- native `/usr/src`-built kernel output in `/frx/store`
- native `/usr/src`-built world output in `/frx/store`
- host-staged bootloader only, as the remaining explicit transitional boundary
Validation still followed the established strategy:
- local QEMU/UEFI/TCG
- real XCP-ng on the approved VM/VDI path
## Issues found and fixed
### 1. The old fixed 256 MiB root filesystem image was too small
The first native-base image attempt failed during `makefs` with:
```text
size of .../image-rootfs is larger than the maxsize of 268435456
```
That was expected once the image started carrying a native world instead of the earlier curated host-copy runtime slice.
To fix this cleanly, Fruix image generation gained an explicit root filesystem size parameter:
- `scripts/fruix.scm` now accepts:
- `--root-size SIZE`
- image metadata now emits:
- `root_size=...`
- `tests/system/run-phase8-system-image.sh` now accepts:
- `ROOT_SIZE`
and records the reported root size in metadata
For the native-base boot validation, the working values were:
- local QEMU:
- `ROOT_SIZE=6g`
- `DISK_CAPACITY=8g`
- real XCP-ng:
- `ROOT_SIZE=6g`
- disk capacity kept matched to the fixed 30 GiB VDI as before
### 2. `materialize-bhyve-image` needed to propagate native-base metadata
After adding the new `native_base_*` metadata path, the first native image run hit a Scheme error because `materialize-bhyve-image` still returned:
- `host-base-stores`
- `fruix-runtime-stores`
but not:
- `native-base-stores`
That was corrected so image-generation results now carry the native-base store set too.
## Harness updates
### Reused/extended existing Phase 11 boot harnesses
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
- now accepts `OS_TEMPLATE` like the XCP-ng PID1 harness already did
- `tests/system/run-phase8-system-image.sh`
- now records:
- `native_base_store_count`
- `native_base_stores`
- `root_size`
### Added native-base boot wrappers
New wrappers:
- `tests/system/run-phase13-native-base-qemu.sh`
- `tests/system/run-phase13-native-base-xcpng.sh`
These wrappers reuse the already-validated PID1 boot path but require the image metadata to confirm that the booted system really uses the intended Phase-13 base split:
- `native_base_store_count=2`
- native kernel present
- native world present
- `host_base_store_count=1`
- host base reduced to bootloader only
## Validation
### Local QEMU / UEFI / TCG
Passing run:
- `PASS phase13-native-base-qemu`
- workdir: `/tmp/phase13-3-qemu3-1775174863`
Key metadata:
```text
disk_capacity=8g
root_size=6g
native_base_store_count=2
host_base_store_count=1
shepherd_pid=1
sshd_status=running
native_base_boot=ok
```
The wrapped underlying Phase 11 PID1 harness also confirmed the full guest runtime path:
- ready marker present
- `/run/current-system` correct
- Shepherd socket present
- Shepherd PID is `1`
- `sshd` running
- activation completed successfully
- `/etc/login.conf` regular + databases present
### Real XCP-ng VM
Passing run:
- `PASS phase13-native-base-xcpng`
- workdir: `/tmp/phase13-3-xcpng-1775175086`
Key metadata:
```text
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
guest_ip=192.168.213.62
root_size=6g
native_base_store_count=2
host_base_store_count=1
shepherd_pid=1
sshd_status=running
compat_prefix_shims=absent
guile_module_smoke=ok
native_base_boot=ok
```
The dynamic VHD upload remained workable even with the larger native-world image payload, though naturally much larger than the earlier minimal image path:
- upload size: `4740784128` bytes (~4.42 GiB)
## What booted
The validated native-base system closure used:
- native kernel store:
- `/frx/store/93f35ddcb9a03f63f83c9e8ae29788685d339789da664f881822b4a1914f5ff6-freebsd-native-kernel-15.0-STABLE`
- native world store:
- `/frx/store/3f6f7f8c06ed8dad4cae21a1e8ac8ba4823bdb7cf54328c9bbcccaeb858beb77-freebsd-native-world-15.0-STABLE`
- remaining host base store:
- `/frx/store/8ffcfe0356fea815726b610514a1280a11266851c2acb870047d559795569f0e-freebsd-bootloader-15.0-STABLE`
That means Phase 13 has now crossed its main success boundary:
- the kernel is native
- the core world runtime is native
- the system actually boots from those outputs
## Assessment
Phase 13 is complete.
Fruix has now moved from:
- building a FreeBSD system from curated host copies
into:
- building kernel/world artifacts from `/usr/src` into `/frx/store`, then booting a declarative Fruix system from those outputs.
The remaining major transitional boundary is now much narrower and explicit:
- host-staged bootloader/boot assets remain
- native kernel + native core world runtime are already in place
That is a real architectural shift, not just more documentation.
## Next recommended step
Proceed to Phase 14:
1. replace the remaining host-copy boot assets first
2. then keep reducing the older host-staged FreeBSD base slice around the now-working native world/kernel path
3. revisit cleaner runtime vs. development splits once the boot asset transition is done

View File

@@ -0,0 +1,144 @@
# Phase 13.1: model FreeBSD world and kernel as native Fruix build artifacts
Date: 2026-04-02
## Goal
Phase 13 begins the Option B pivot from host-copy FreeBSD base packages toward builder-produced FreeBSD base artifacts in `/frx/store`.
This first subphase focused on the modeling and package/materialization layer:
- define native Fruix package objects for FreeBSD kernel and world outputs
- make their output identity depend on `/usr/src` and explicit build parameters
- teach the system metadata to distinguish host-staged base stores from native base stores
The purpose of this step was not yet to prove a full boot from those new outputs. It was to land the description and materialization model that Phase 13.2 can exercise concretely.
## Implementation
### 1. Added native package definitions in `modules/fruix/packages/freebsd.scm`
New package objects:
- `freebsd-native-kernel`
- `freebsd-native-world`
New predicate:
- `freebsd-native-build-package?`
The native packages are intentionally FreeBSD-specific and currently parameterized with:
- `source-root` => `/usr/src`
- `target` => `amd64`
- `target-arch` => `amd64`
- `kernconf` => `GENERIC`
- `make-flags` =>
- `__MAKE_CONF=/dev/null`
- `SRCCONF=/dev/null`
- `SRC_ENV_CONF=/dev/null`
- `MK_DEBUG_FILES=no`
- `MK_TESTS=no`
The first native world artifact also carries an explicit runtime-oriented prune list:
- `usr/share/doc`
- `usr/share/examples`
- `usr/share/info`
- `usr/share/man`
- `usr/tests`
That keeps the first native world target narrower and aligned with the current boot/runtime goal.
### 2. Added native build-system support in `modules/fruix/system/freebsd.scm`
`materialize-freebsd-package` now understands three classes of package build system:
- `copy-build-system`
- `freebsd-world-build-system`
- `freebsd-kernel-build-system`
For the native FreeBSD path, the materializer now records identity from:
- `/usr/src`
- explicit build parameters
- the selected kernel configuration
The `/usr/src` source identity is currently represented by an `mtree`-based tree digest using:
- `type`
- `link`
- `size`
- `mode`
- `sha256digest`
The kernel configuration is also hashed explicitly via its resolved `KERNCONF` path.
The code now has dedicated helpers for:
- native source identity
- native build-root identity
- buildworld/buildkernel stamp handling
- staged installworld/installkernel materialization
- writing native build metadata beside the resulting store outputs
### 3. Store-layout metadata now separates native and host-staged base stores
The closure metadata and CLI metadata now distinguish:
- `host_base_stores`
- `native_base_stores`
- `fruix_runtime_stores`
This is important for the architecture transition: a system can now say explicitly which parts of its FreeBSD base still come from transitional host-copy packages and which parts come from native `/usr/src`-driven artifacts.
### 4. Merged output trees now skip private dotfile metadata
The profile/tree merge logic now ignores private dotfile metadata in store outputs. This keeps internal build metadata files from leaking into the merged runtime tree when native base outputs start being consumed by real system closures.
## Validation
### Package/model load
Verified that the package/system modules still load after the native build-model additions, and that the new native package objects are present.
Observed output included:
```text
freebsd-kernel-build-system
#t
```
for:
- `freebsd-native-kernel` build-system lookup
- `freebsd-native-build-package? freebsd-native-world`
### Existing closure path regression check
To make sure the modeling work did not break the already validated host-copy path, re-ran:
- `tests/system/run-phase7-system-closure.sh`
Passing run:
- `PASS phase7-system-closure`
- workdir: `/tmp/phase13-1-closure-1775164392`
This confirmed that the existing FreeBSD system closure path still works while the new native base model is being introduced.
## Assessment
Phase 13.1 is complete.
Fruix now has a real representation for native FreeBSD base artifacts in its package/materialization model rather than only the older host-copy package set. Just as importantly, the output identity story is no longer purely implicit: `/usr/src`, `KERNCONF`, and the selected build parameters are now part of the native artifact definition.
This is the necessary bridge into Phase 13.2, where those new package definitions need to be exercised to produce concrete kernel/world outputs in `/frx/store`.
## Next recommended step
Proceed to Phase 13.2:
- build the first concrete `freebsd-native-kernel` and `freebsd-native-world` outputs from `/usr/src`
- inspect their staged contents in `/frx/store`
- document the exact split that the first native runtime target provides

View File

@@ -0,0 +1,176 @@
# Phase 13.2: built the first native FreeBSD world and kernel artifacts from `/usr/src`
Date: 2026-04-03
## Goal
After Phase 13.1 introduced native Fruix package/materialization support for FreeBSD base artifacts, the next step was to exercise that path for real:
- build a kernel from `/usr/src`
- build and stage a minimal runtime-oriented world from `/usr/src`
- place both outputs under `/frx/store`
- document the exact split of this first native base target
## Implementation
### 1. Fixed two important builder issues found during the first real run
#### `MAKEOBJDIRPREFIX` had to move to the environment
The first real `buildworld` attempt failed with:
```text
MAKEOBJDIRPREFIX can only be set in environment or src-env.conf(5)
```
The native build path originally passed `MAKEOBJDIRPREFIX=...` as a make command-line variable. That was corrected so the generated build command now uses:
- `env MAKEOBJDIRPREFIX=... make ...`
instead.
#### The initial `/usr/src` tree hash was accidentally unstable
The first source-tree hashing attempt hashed the raw `mtree -c` output. That was subtly wrong because `mtree` includes header comments such as:
- date
- user
- machine
- tree path banner
As a result, successive hash computations differed even when `/usr/src` had not changed.
This was corrected by stripping the leading `# ...` comment lines before hashing the `mtree` content. After that fix:
- native world and native kernel reported the same `source-tree-sha256`
- both reused the same native build root
- output identity stopped drifting on each run
## First concrete native outputs
A native Fruix system definition using:
- `freebsd-native-kernel`
- `freebsd-native-world`
- host-staged `freebsd-bootloader`
- `shepherd-pid1`
was built successfully.
Resulting store outputs:
- kernel:
- `/frx/store/93f35ddcb9a03f63f83c9e8ae29788685d339789da664f881822b4a1914f5ff6-freebsd-native-kernel-15.0-STABLE`
- world:
- `/frx/store/3f6f7f8c06ed8dad4cae21a1e8ac8ba4823bdb7cf54328c9bbcccaeb858beb77-freebsd-native-world-15.0-STABLE`
Native build metadata records for both outputs show:
- `source-root=/usr/src`
- the same `source-tree-sha256`
- `kernconf=GENERIC`
- the same shared `build-root`
Shared build root:
- `/var/tmp/fruix-freebsd-native-build-c59b1b8128b305d9bad9cf3d654771c941c4e8b6a2732f6bc959df96d1d32f58`
Key log files recorded in the output metadata:
- `buildworld.log`
- `buildkernel-GENERIC.log`
- `install-freebsd-native-world.log`
- `install-freebsd-native-kernel.log`
## First native world split
The first `freebsd-native-world` output is intentionally runtime-oriented. Validation confirmed that it contains the core pieces needed by the existing Fruix guest model, including:
- `/bin/sh`
- `/sbin/init`
- `/etc/rc`
- `/usr/sbin/sshd`
- `/sbin/dhclient`
- `/usr/bin/cap_mkdb`
- `/usr/sbin/pwd_mkdb`
- `/usr/share/locale/C.UTF-8/LC_CTYPE`
The first prune list is also active. Validation confirmed the world output does **not** contain:
- `/usr/share/man`
- `/usr/tests`
This keeps the first native world output closer to the current boot/runtime target instead of a fully unpruned base install tree.
## Store-boundary result
The generated closure metadata now shows the intended transitional boundary clearly:
- `host_base_store_count=1`
- `host_base_stores=` bootloader only
- `native_base_store_count=2`
- `native_base_stores=` native kernel + native world
That means Fruix now has a mixed but explicit Phase-13 system boundary:
- bootloader still comes from the old host-staged path
- kernel and core runtime world now come from native `/usr/src`-built store outputs
## Validation
### Direct build through `fruix system build`
A real native-base system build succeeded and produced:
- native kernel store output
- native world store output
- a system closure referencing those outputs
### Dedicated Phase 13.2 harness
Added:
- `tests/system/phase13-native-base-pid1-operating-system.scm.in`
- `tests/system/run-phase13-native-base-build.sh`
Passing run:
- `PASS phase13-native-base-build`
- workdir: `/tmp/phase13-2-build-1775173551`
Key metadata from that run:
```text
kernel_store=/frx/store/93f35ddcb9a03f63f83c9e8ae29788685d339789da664f881822b4a1914f5ff6-freebsd-native-kernel-15.0-STABLE
world_store=/frx/store/3f6f7f8c06ed8dad4cae21a1e8ac8ba4823bdb7cf54328c9bbcccaeb858beb77-freebsd-native-world-15.0-STABLE
host_base_store_count=1
native_base_store_count=2
world_source_tree_sha256=72a451e2ea47c4c4777035f797dc35f8d905eaabb2a717bc1fd71019f3021f72
kernel_source_tree_sha256=72a451e2ea47c4c4777035f797dc35f8d905eaabb2a717bc1fd71019f3021f72
world_build_root=/var/tmp/fruix-freebsd-native-build-c59b1b8128b305d9bad9cf3d654771c941c4e8b6a2732f6bc959df96d1d32f58
kernel_build_root=/var/tmp/fruix-freebsd-native-build-c59b1b8128b305d9bad9cf3d654771c941c4e8b6a2732f6bc959df96d1d32f58
```
The harness also verified:
- closure rebuild path reproducibility
- existence of required kernel/world runtime files
- absence of the pruned world paths
- presence of native build metadata and logs
- presence of `native-base-stores` in closure store-layout metadata
## Assessment
Phase 13.2 is complete.
Fruix now does more than describe native FreeBSD base artifacts: it actually builds them from `/usr/src`, stores them under `/frx/store`, and records enough metadata to inspect how they were produced.
This is the first point where Fruix can honestly say that part of the FreeBSD base is no longer just a curated copy of the builder host.
## Next recommended step
Proceed to Phase 13.3:
- wire the operating-system/image path to boot using these native kernel/world outputs
- validate locally with QEMU/UEFI
- validate on the approved XCP-ng VM/VDI path

View File

@@ -0,0 +1,126 @@
# Phase 14.1: validated native FreeBSD boot assets without host-copied `/boot`
Date: 2026-04-03
## Goal
The goal of Phase 14.1 was to stop relying on the host-staged `freebsd-bootloader` package for the validated boot path.
Phase 13 had already proved that Fruix could boot with:
- native kernel
- native world
- host-staged bootloader only
Phase 14.1 moved the remaining boot assets onto the native side as well.
## Approach
Before introducing a cleaner package split, Fruix first reused the already-built native world output as the source of boot assets.
The Phase 14.1 operating-system template now uses:
- `#:kernel freebsd-native-kernel`
- `#:bootloader freebsd-native-world`
- `#:base-packages (list freebsd-native-world)`
That means the validated image now gets both:
- runtime files
- loader/boot assets
from the native `/usr/src`-built world output already staged in `/frx/store`.
This is slightly redundant at the model layer, but it is a clean way to prove the architectural point first:
- the boot path no longer needs host-copied `/boot/...` material
## Additional QEMU harness fix
During repeated local validation, the existing QEMU PID1 harness was found to boot the raw store image directly from `/frx/store`.
That meant each local boot mutated the supposedly immutable image artifact and could leave the filesystem dirty for later runs.
To avoid that, `tests/system/run-phase11-shepherd-pid1-qemu.sh` now copies the built raw image to a temporary writable workdir file before launching QEMU:
- source image remains in `/frx/store`
- QEMU now writes to `boot-disk.img` in the workdir
This keeps the store image stable across repeated local boots.
## New files
Added:
- `tests/system/phase14-native-boot-pid1-operating-system.scm.in`
- `tests/system/run-phase14-native-boot-qemu.sh`
- `tests/system/run-phase14-native-boot-xcpng.sh`
These wrappers reuse the proven PID1 boot harnesses and assert the new boundary:
- `host_base_store_count=0`
- native kernel present
- native world present
- boot assets come from native world
## Validation
### Local QEMU / UEFI / TCG
Passing run:
- `PASS phase14-native-boot-qemu`
- workdir: `/tmp/phase14-1-qemu2-1775188371`
Confirmed:
```text
native_base_store_count=2
host_base_store_count=0
shepherd_pid=1
sshd_status=running
native_boot_assets=freebsd-native-world
native_base_boot=ok
```
### Real XCP-ng VM
Passing run:
- `PASS phase14-native-boot-xcpng`
- workdir: `/tmp/phase14-1-xcpng-1775188701`
Confirmed:
```text
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
guest_ip=192.168.213.62
native_base_store_count=2
host_base_store_count=0
shepherd_pid=1
sshd_status=running
compat_prefix_shims=absent
guile_module_smoke=ok
native_boot_assets=freebsd-native-world
native_base_boot=ok
```
## Result
Phase 14.1 is complete.
For the validated boot path, Fruix no longer depends on a host-copied `freebsd-bootloader` store item.
The currently validated native boot boundary is now:
- native kernel
- native world providing boot assets
- native world providing runtime
- Fruix runtime stores for Guile/Shepherd
That is already enough to say the image no longer relies on host-copied kernel/boot material.
## Next step
Phase 14.2 should remove the remaining model redundancy by introducing a clearer native runtime slice, so the booted guest reaches ready state using an explicit native runtime output rather than reusing the broader native world output for both boot and runtime roles.

View File

@@ -0,0 +1,139 @@
# Phase 14.2: validated an explicit native FreeBSD runtime slice
Date: 2026-04-03
## Goal
Phase 14.1 proved that Fruix could boot without host-copied `/boot` material by sourcing boot assets from the existing native world output.
Phase 14.2 removed the remaining model ambiguity on the runtime side:
- the guest should now boot and reach ready state using an explicit native runtime output
- not by reusing the broader native world artifact for both boot and runtime roles
## Changes
Added a new native package:
- `freebsd-native-runtime`
This package is built from `/usr/src` via the existing native world build path and prunes at least:
- `boot`
- `usr/include`
- `usr/share/doc`
- `usr/share/examples`
- `usr/share/info`
- `usr/share/man`
- `usr/share/mk`
- `usr/tests`
The validated Phase 14.2 operating-system template now uses:
- `#:kernel freebsd-native-kernel`
- `#:bootloader freebsd-native-world`
- `#:base-packages (list freebsd-native-runtime)`
That means the model is now explicit:
- native world provides boot assets
- native runtime provides the guest runtime slice
- host base stores are no longer part of the validated path
## Practical sizing finding
This Phase 14.2 layout still duplicates some native world/runtime content in the closure because the boot assets still come from the broader native world output.
As a result, the Phase 13 image sizes were no longer large enough.
Working values were:
- local QEMU:
- `DISK_CAPACITY=12g`
- `ROOT_SIZE=10g`
- real XCP-ng:
- `ROOT_SIZE=10g`
- disk capacity still matched to the fixed 30 GiB VDI
The wrappers were updated to use those larger defaults.
This is acceptable for Phase 14.2 because the next subphase is specifically about cleaning up the runtime/development/boot boundary further.
## New files
Added:
- `tests/system/phase14-native-runtime-pid1-operating-system.scm.in`
- `tests/system/run-phase14-native-runtime-qemu.sh`
- `tests/system/run-phase14-native-runtime-xcpng.sh`
These wrappers assert:
- `host_base_store_count=0`
- native kernel present
- native world present as the current boot-source artifact
- native runtime present
- runtime store still contains the files needed for boot-to-ready
- runtime store no longer contains `/boot`
- runtime store no longer contains `/usr/include`
## Validation
### Local QEMU / UEFI / TCG
Passing run:
- `PASS phase14-native-runtime-qemu`
- workdir: `/tmp/phase14-2-qemu2-1775189802`
Confirmed:
```text
disk_capacity=12g
root_size=10g
runtime_store=/frx/store/684a82aeed2c9a353e3a09d2cbf5358274d758005e0bfa9b1025d101bc166f79-freebsd-native-runtime-15.0-STABLE
native_base_store_count=3
host_base_store_count=0
shepherd_pid=1
sshd_status=running
native_runtime_ready=ok
```
### Real XCP-ng VM
Passing run:
- `PASS phase14-native-runtime-xcpng`
- workdir: `/tmp/phase14-2-xcpng-1775190184`
Confirmed:
```text
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
guest_ip=192.168.213.62
root_size=10g
runtime_store=/frx/store/684a82aeed2c9a353e3a09d2cbf5358274d758005e0bfa9b1025d101bc166f79-freebsd-native-runtime-15.0-STABLE
native_base_store_count=3
host_base_store_count=0
shepherd_pid=1
sshd_status=running
compat_prefix_shims=absent
guile_module_smoke=ok
native_runtime_ready=ok
```
## Result
Phase 14.2 is complete.
The validated Fruix guest now reaches ready state using an explicit native runtime artifact:
- native kernel
- native world for boot assets
- native runtime for the guest runtime slice
- no host-staged FreeBSD base stores in the validated path
## Next step
Phase 14.3 should clean up the remaining redundancy by defining clearer runtime vs. development boundaries and, ideally, replacing the temporary use of the broad native world artifact as the boot-source package with a narrower native boot asset package.

View File

@@ -0,0 +1,225 @@
# Phase 14.3: split native FreeBSD boot, runtime, and development artifacts
Date: 2026-04-03
## Goal
Phase 14.3 finished the Phase 14 cleanup work by replacing the temporary broad-world reuse with narrower native artifacts and by making the runtime/development boundary explicit in code.
The target was:
- no host-staged FreeBSD base stores in the validated path
- no need to keep the broad `freebsd-native-world` artifact in the final validated system closure
- explicit native boot and runtime slices
- clearer development-profile boundaries
## Implementation
### New native packages
Added in `modules/fruix/packages/freebsd.scm`:
- `freebsd-native-bootloader`
- `freebsd-native-headers`
Also refined:
- `freebsd-native-runtime`
### Narrower native runtime slice
`freebsd-native-runtime` now prunes more obviously non-runtime content, including at least:
- `boot`
- `rescue`
- `usr/include`
- `usr/lib/debug`
- `usr/lib32`
- `usr/obj`
- `usr/src`
- `usr/share/doc`
- `usr/share/examples`
- `usr/share/info`
- `usr/share/man`
- `usr/share/mk`
- `usr/tests`
This shrank the runtime output substantially and made the runtime/development distinction much cleaner.
### Native bootloader slice
`freebsd-native-bootloader` is now a native world-derived slice that keeps only the validated boot assets Fruix currently needs:
- `boot/loader`
- `boot/loader.efi`
- `boot/device.hints`
- `boot/defaults`
- `boot/lua`
### Native headers slice
`freebsd-native-headers` now captures the development-facing header/mk boundary explicitly:
- `usr/include`
- `usr/share/mk`
### Build-system support for sliced native outputs
`modules/fruix/system/freebsd.scm` gained support for `keep-paths` on native world-derived packages.
That means a native package can now:
- install full world/distribution into a staging tree
- keep only selected subtrees/files for its final store output
- still participate in the same `/usr/src`-based identity/build-root story
Native output metadata now records both:
- `keep-paths`
- `prune-paths`
## New package-set boundaries
Added explicit package sets:
- `%freebsd-native-system-packages`
- currently centered on `freebsd-native-runtime`
- `%freebsd-native-development-profile-packages`
- `freebsd-native-runtime`
- `freebsd-native-headers`
- explicit toolchain/dev helpers that remain separate artifacts
This makes the boundary clearer:
- runtime world output
- headers artifact
- toolchain artifact(s)
- optional development profile composition
## Final validated Phase 14 system model
The final Phase 14 PID1 system template now uses:
- `#:kernel freebsd-native-kernel`
- `#:bootloader freebsd-native-bootloader`
- `#:base-packages %freebsd-native-system-packages`
That means the validated system closure now contains:
- native kernel
- native bootloader slice
- native runtime slice
- Fruix runtime stores
and no longer needs the broad `freebsd-native-world` artifact in the final system closure.
## New files
Added:
- `tests/system/phase14-native-split-pid1-operating-system.scm.in`
- `tests/system/run-phase14-native-split-qemu.sh`
- `tests/system/run-phase14-native-split-xcpng.sh`
- `tests/system/run-phase14-native-development-split.sh`
## Validation
### Development split validation
Passing run:
- `PASS phase14-native-development-split`
- workdir: `/tmp/phase14-3-dev5-1775191195`
Confirmed:
```text
bootloader_store=/frx/store/71aa3ba5dd9a02f7d2710bfc3624cbf5e3cd18f1fbff0744c82df36901b10ec0-freebsd-native-bootloader-15.0-STABLE
headers_store=/frx/store/aab09122d37962e6d479c17172ce4b8ea85e5ff33c98aa76424ada2fa1a82617-freebsd-native-headers-15.0-STABLE
native_system_packages=freebsd-native-runtime
native_development_packages=freebsd-native-runtime,freebsd-native-headers,freebsd-clang-toolchain,freebsd-gmake,freebsd-autotools,freebsd-openssl,freebsd-zlib,freebsd-sh,freebsd-bash
runtime_vs_development_split=ok
```
The harness also confirmed:
- native bootloader slice contains only expected boot assets
- native headers slice contains headers/mk files
- native headers slice does not contain `/boot` or runtime binaries
### Local QEMU / UEFI / TCG
Passing run:
- `PASS phase14-native-split-qemu`
- workdir: `/tmp/phase14-3-qemu-1775191337`
Confirmed:
```text
disk_capacity=8g
root_size=6g
bootloader_store=/frx/store/71aa3ba5dd9a02f7d2710bfc3624cbf5e3cd18f1fbff0744c82df36901b10ec0-freebsd-native-bootloader-15.0-STABLE
runtime_store=/frx/store/1b4b8774d0df36df2635fe1c35367a2c5fa7790e303f0aaa26eabfe3cce667f2-freebsd-native-runtime-15.0-STABLE
native_base_store_count=3
host_base_store_count=0
shepherd_pid=1
sshd_status=running
native_split_boot=ok
```
### Real XCP-ng VM
Passing run:
- `PASS phase14-native-split-xcpng`
- workdir: `/tmp/phase14-3-xcpng-1775191743`
Confirmed:
```text
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
guest_ip=192.168.213.62
root_size=6g
bootloader_store=/frx/store/71aa3ba5dd9a02f7d2710bfc3624cbf5e3cd18f1fbff0744c82df36901b10ec0-freebsd-native-bootloader-15.0-STABLE
runtime_store=/frx/store/1b4b8774d0df36df2635fe1c35367a2c5fa7790e303f0aaa26eabfe3cce667f2-freebsd-native-runtime-15.0-STABLE
native_base_store_count=3
host_base_store_count=0
shepherd_pid=1
sshd_status=running
compat_prefix_shims=absent
guile_module_smoke=ok
native_split_boot=ok
```
Notably, after the narrower native split, the XCP-ng upload size dropped back down dramatically:
- dynamic VHD upload: `1560725504` bytes (~1.45 GiB)
That is much better than the temporary broad-world + runtime duplication in Phase 14.2.
## Result
Phase 14 is complete.
Fruix now has a validated, host-base-free FreeBSD system path built from native artifacts in `/frx/store`:
- `freebsd-native-kernel`
- `freebsd-native-bootloader`
- `freebsd-native-runtime`
with a clearer development boundary via:
- `freebsd-native-headers`
- `%freebsd-native-development-profile-packages`
This completes the Phase 14 objective of incrementally replacing the host-copy FreeBSD base layer for the validated boot/runtime path.
## Next step
Proceed to Phase 15:
1. make the FreeBSD base version a declarative input
2. demonstrate side-by-side base versions in `/frx/store`
3. establish rebuild/redeploy/rollback across those base versions

View File

@@ -0,0 +1,179 @@
# Phase 15.2: side-by-side native base versions and rollback-friendly redeploy
Date: 2026-04-03
## Goal
Phase 15.2 demonstrated that Fruix can keep at least two distinct declarative FreeBSD base builds in `/frx/store` at the same time and switch between them through the normal system rebuild/image/boot flow.
For this first upgrade-story validation, both declared bases still point at the same local `/usr/src`, but they carry distinct declarative version labels:
- current base: `15.0-STABLE`
- candidate base: `15.0-STABLE-p1`
That is enough to prove the Fruix properties needed here:
- distinct content-addressed outputs
- side-by-side coexistence
- no in-place mutation of the older base closure
- rollback to the earlier closure using the normal deployment path
## New files
Added:
- `tests/system/run-phase15-base-coexistence.sh`
- `tests/system/run-phase15-base-rollback-qemu.sh`
- `tests/system/run-phase15-base-rollback-xcpng.sh`
## Validation model
### Current base declaration
```scheme
(freebsd-base
#:name "stable-default"
#:version-label "15.0-STABLE"
#:release "15.0-STABLE"
#:branch "stable/15"
...)
```
### Candidate base declaration
```scheme
(freebsd-base
#:name "stable-canary"
#:version-label "15.0-STABLE-p1"
#:release "15.0-STABLE"
#:branch "stable/15"
...)
```
Both declarations use the same validated native Phase 14 package composition:
- kernel from `freebsd-native-kernel-for`
- bootloader from `freebsd-native-bootloader-for`
- runtime from `freebsd-native-system-packages-for`
- `shepherd-pid1`
## Side-by-side build validation
Passing run:
- `PASS phase15-base-coexistence`
- workdir: `/tmp/phase15-2-coexist-1775202833`
Confirmed:
```text
current_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
current_base_version_label=15.0-STABLE
candidate_base_version_label=15.0-STABLE-p1
side_by_side_base_versions=ok
rollback_rebuild_path=ok
```
Current native base stores:
```text
/frx/store/d9785661ea4829d51fbf545c2607a5691af2cc33c8ef3cd44de7ad5626685098-freebsd-native-kernel-15.0-STABLE
/frx/store/b448c822302ccdfb2f06da811fb224a044c51a9935bbfcd77a71a25d02f228f1-freebsd-native-bootloader-15.0-STABLE
/frx/store/ac3ba684020e70d3c76e593fd687cef8ab5e148958baabb477b7ef3d2647c5cd-freebsd-native-runtime-15.0-STABLE
```
Candidate native base stores:
```text
/frx/store/05bee8ffbe8c43242ffd97da4dc305f2921612a660cbcb48c3a3536bfac07079-freebsd-native-kernel-15.0-STABLE-p1
/frx/store/8955f1bfe89321e6e1e628c59376f2092547523f48a773974cc259963adac184-freebsd-native-bootloader-15.0-STABLE-p1
/frx/store/30314f17fd8ff4a1a3eff31c8c5048f15f67c46d1132d5b8c45fd9768742665e-freebsd-native-runtime-15.0-STABLE-p1
```
Important result:
- the older current closure stayed in `/frx/store`
- the candidate closure appeared beside it
- rebuilding the current declaration returned the exact original current closure path again
## Local QEMU rollback validation
Passing run:
- `PASS phase15-base-rollback-qemu`
- workdir: `/tmp/phase15-2-qemu2-1775204321`
Validation sequence:
1. boot current base
2. boot candidate base
3. boot current base again
Confirmed:
```text
current_first_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
rollback_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
current_base_version_label=15.0-STABLE
candidate_base_version_label=15.0-STABLE-p1
rollback_base_version_label=15.0-STABLE
base_rollforward_and_rollback=ok
```
This showed that the booted system could move forward to the candidate base and then return to the earlier closure without mutating it in place.
## Real XCP-ng rollback validation
Passing run:
- `PASS phase15-base-rollback-xcpng`
- workdir: `/tmp/phase15-2-xcpng-1775204839`
Validation sequence:
1. boot candidate base on the approved VM/VDI
2. boot current base again on the same approved VM/VDI
Confirmed:
```text
candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
rollback_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_base_version_label=15.0-STABLE-p1
rollback_base_version_label=15.0-STABLE
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
base_rollforward_and_rollback=ok
```
Both boots also preserved the already-hardened guest properties:
- `shepherd_pid=1`
- `sshd_status=running`
- `compat_prefix_shims=absent`
- `guile_module_smoke=ok`
## Result
Phase 15.2 is complete.
Fruix now has a real declarative rebuild/redeploy/rollback story for the FreeBSD base at the store/model layer:
- two declared base versions can coexist side by side in `/frx/store`
- the candidate deployment does not overwrite the current one in place
- rebuilding the earlier declaration returns to the earlier closure path
- the same story works both locally under QEMU and on the approved XCP-ng VM/VDI path
## Scope note
This first upgrade-story validation still uses the same local `/usr/src` as the underlying source tree for both declarations. What changed is the declared base identity and therefore the store/model/deployment identity.
That is sufficient for this phase because the requirement was to establish the upgrade semantics:
- explicit base declaration
- side-by-side outputs
- rollback-friendly closures
The next improvement beyond Phase 15 would be to make acquiring or selecting distinct source trees/releases more reproducible and less tied to a single host checkout.

View File

@@ -0,0 +1,141 @@
# Phase 15.1: make the FreeBSD base version a declarative Fruix input
Date: 2026-04-03
## Goal
Phase 15.1 made the FreeBSD base an explicit declarative part of the Fruix system model instead of leaving it as an implicit property of whatever `/usr/src` happened to be present on the builder host.
## Implementation
### New declarative base record
Added in `modules/fruix/packages/freebsd.scm`:
- `freebsd-base`
- `freebsd-base?`
- accessors for:
- `name`
- `version-label`
- `release`
- `branch`
- `source-root`
- `target`
- `target-arch`
- `kernconf`
- `make-flags`
- `%default-freebsd-base`
This gives Fruix an explicit model for the base input used by native FreeBSD artifacts.
### Native package constructors now accept a declared base
Added package constructors:
- `freebsd-native-kernel-for`
- `freebsd-native-world-for`
- `freebsd-native-runtime-for`
- `freebsd-native-bootloader-for`
- `freebsd-native-headers-for`
- `freebsd-native-system-packages-for`
- `freebsd-native-development-profile-packages-for`
The existing exported package variables remain as the `%default-freebsd-base` instances:
- `freebsd-native-kernel`
- `freebsd-native-world`
- `freebsd-native-runtime`
- `freebsd-native-bootloader`
- `freebsd-native-headers`
That preserves the validated Phase 14 path while making alternative declared bases possible.
### Operating-system model now records the declared base
Added to `modules/fruix/system/freebsd.scm`:
- `operating-system-freebsd-base`
- new `#:freebsd-base` field on `operating-system`
The declared base is now recorded in:
- `operating-system-closure-spec`
- `operating-system-image-spec`
- `metadata/freebsd-base.scm`
- `metadata/store-layout.scm`
### CLI metadata now exposes the declared base
`scripts/fruix.scm` now emits, for `fruix system build` and `image`:
- `freebsd_base_name`
- `freebsd_base_version_label`
- `freebsd_base_release`
- `freebsd_base_branch`
- `freebsd_base_source_root`
- `freebsd_base_target`
- `freebsd_base_target_arch`
- `freebsd_base_kernconf`
- `freebsd_base_file`
### Native build metadata now records the declared base
Native build manifests and `.freebsd-native-build-info.scm` now carry a `declared-base` block so the native artifacts themselves record the declarative base choice.
## New files
Added:
- `tests/system/phase15-declarative-base-pid1-operating-system.scm.in`
- `tests/system/run-phase15-declarative-base-build.sh`
## Validation
Passing run:
- `PASS phase15-declarative-base-build`
- workdir: `/tmp/phase15-1-build-1775202535`
The harness used an explicit declared base:
```scheme
(freebsd-base
#:name "stable-default"
#:version-label "15.0-STABLE-declarative"
#:release "15.0-STABLE"
#:branch "stable/15"
#:source-root "/usr/src"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC")
```
Confirmed:
```text
kernel_store=/frx/store/8fcef04c7e507e86ea5e92f251fe3c6ac1aa3bcf4809fa77ddd8b92854bfcde0-freebsd-native-kernel-15.0-STABLE-declarative
bootloader_store=/frx/store/7a0ba431e487dc35a8f6318108da16a37c8426c43e77e7a7f91404ba1d980eef-freebsd-native-bootloader-15.0-STABLE-declarative
runtime_store=/frx/store/17c24ad20ddcb136c39352b68e758deae0b480258ba0128a5546f696a7eba0a6-freebsd-native-runtime-15.0-STABLE-declarative
native_base_store_count=3
host_base_store_count=0
freebsd_base_name=stable-default
freebsd_base_version_label=15.0-STABLE-declarative
freebsd_base_release=15.0-STABLE
freebsd_base_branch=stable/15
freebsd_base_source_root=/usr/src
freebsd_base_kernconf=GENERIC
declarative_base_input=ok
```
The harness also confirmed:
- `metadata/freebsd-base.scm` exists
- `parameters.scm` records the declared base
- `metadata/store-layout.scm` records the declared base
- native build info files record the declared base version/branch
## Result
Phase 15.1 is complete.
Fruix now has an explicit declarative FreeBSD base input in the system model, while still allowing the current validated `/usr/src`-based path to work unchanged by default.

View File

@@ -0,0 +1,120 @@
# Phase 15.3: decision on self-hosted FreeBSD base builds
Date: 2026-04-03
## Question
After Phases 13 through 15, should Fruix immediately pursue self-hosted FreeBSD base builds inside a Fruix-managed guest, or should it continue using the host builder while tightening source/reproducibility boundaries?
## Current evidence
What is now already true:
- Fruix builds native FreeBSD base artifacts from `/usr/src` into `/frx/store`
- Fruix has validated a host-base-free boot/runtime path composed from:
- `freebsd-native-kernel`
- `freebsd-native-bootloader`
- `freebsd-native-runtime`
- that path boots locally under QEMU/UEFI and on the approved real XCP-ng VM/VDI
- the FreeBSD base is now an explicit declarative system input through `freebsd-base`
- Fruix can keep distinct declared base versions side by side in `/frx/store`
- Fruix can roll forward to a candidate base declaration and roll back to the earlier closure without mutating it in place
So the main Guix-inspired value proposition is already present at the system-deployment layer:
- declarative system descriptions
- content-addressed outputs
- side-by-side versions
- rollback-friendly closures
## What is still not strong enough for self-hosting to be the best next step
### 1. Source acquisition is still too host-local
The native base path still depends on the host's local `/usr/src` tree as the source of truth.
That means the next reproducibility win is not "build inside the guest" first; it is "make the source tree selection/acquisition more explicit and reproducible" first.
### 2. The host-built path is already validated and useful
The current host-built native base flow is not hypothetical anymore. It has already proven:
- native artifacts in `/frx/store`
- host-base-free boot/runtime composition
- side-by-side closures
- rollback behavior
That makes it the productive path to continue refining.
### 3. Self-hosting would currently mix several problems at once
Jumping to guest self-hosting now would combine:
- source acquisition/reproducibility questions
- toolchain/profile maturity questions
- build-environment modeling questions
- large build-resource/runtime questions inside the guest
- deployment/debugging questions
That would make failure analysis worse just after the project finally obtained a clean validated deployment path.
### 4. Platform constraints still argue for caution
Current operator/environment constraints still matter:
- local bhyve remains blocked under Xen due to missing nested VT-x exposure
- real VM validation still reuses a single approved XCP-ng VM/VDI pair
- XCP-ng storage permissions still prevent creating fresh VDIs on demand
Those constraints do not block the current host-built native-base path, but they do make an early self-hosting pivot less attractive.
## Decision
**Decision:** do **not** pursue self-hosted FreeBSD base builds as the next immediate milestone.
The near-term direction should remain:
1. continue building native FreeBSD base artifacts on the host
2. keep storing them in `/frx/store`
3. improve declarative source-tree/version selection and provenance
4. tighten reproducibility around source inputs and build parameters
5. only revisit self-hosting after those pieces are stronger
## Why this is the right decision now
This preserves the important architectural win already achieved:
- Fruix, not ad hoc host copying, now defines the deployed FreeBSD base through declared store artifacts
It also follows the same spirit as Guix generation/rollback semantics: the crucial user-facing property is not where the build happened first, but that the result is declarative, content-addressed, side-by-side, and rollback-friendly.
At this point Fruix already has those properties for the FreeBSD base path.
## Conditions for revisiting self-hosting later
Self-hosted base builds should be reconsidered only after at least most of the following are true:
- Fruix can select or acquire distinct FreeBSD source trees more reproducibly than a single ambient `/usr/src`
- the native development/toolchain side is modeled cleanly enough for a Fruix-managed build environment
- operator/deployment tooling can handle larger iterative validation loops without excessive friction
- there is a concrete benefit to guest-side base builds beyond what host-built store artifacts already provide
## Recommended next focus after Phase 15
The next work should likely focus on improving the declared source/reproducibility boundary around the now-working native base path, rather than on guest self-hosting.
A good next phase would be centered on things like:
- declarative source-tree selection/acquisition for the FreeBSD base
- stronger provenance for declared source inputs
- cleaner rebuilds across multiple source trees or revisions
- continued refinement of deployment/generation management around the existing store-based base artifacts
## Result
Phase 15.3 is complete.
The project now has an evidence-based decision:
- **near-term path:** host-built native FreeBSD base artifacts in `/frx/store`
- **not yet:** self-hosted FreeBSD base builds inside the Fruix guest

View File

@@ -0,0 +1,202 @@
# Phase 16.1: model FreeBSD source inputs explicitly
Date: 2026-04-03
## Goal
Phase 16.1 introduces a first-class Fruix model for FreeBSD source inputs so the native base path is no longer described only as "whatever `/usr/src` is on the host".
This step does **not** yet fetch or materialize remote source trees. It makes the source declaration explicit and records it through the package, system, and CLI metadata layers so later phases can replace ambient `/usr/src` with fetched or materialized inputs cleanly.
## Implementation
### New declarative FreeBSD source record
Added in `modules/fruix/packages/freebsd.scm`:
- `freebsd-source`
- `freebsd-source?`
- accessors for:
- `name`
- `kind`
- `url`
- `path`
- `ref`
- `commit`
- `sha256`
- `%default-freebsd-source`
Supported source kinds are now modeled explicitly as:
- `local-tree`
- `git`
- `src-txz`
The default source remains a local-tree declaration for:
- `/usr/src`
### FreeBSD bases now carry a declared source object
Extended `freebsd-base` so it now records:
- the transitional `source-root` still used by the current native build path
- a new `source` record describing the declared FreeBSD source input
Added/exported:
- `freebsd-base-source`
This keeps the validated `/usr/src` path working while giving the base a source object that can later point at fetched Git or `src.txz` materializations.
### Native package plans and metadata now record the declared source
`modules/fruix/packages/freebsd.scm` now threads source fields into native package install plans.
`modules/fruix/system/freebsd.scm` now records a `declared-source` block in:
- native package manifests
- `.freebsd-native-build-info.scm`
This means native kernel, bootloader, and runtime outputs now remember both:
- the declared base
- the declared source input
### Operating-system validation now checks source declarations
Added source validation in `modules/fruix/system/freebsd.scm`.
Current accepted source declarations are:
- `local-tree`
- requires a path
- `git`
- requires a URL and at least a ref or commit
- `src-txz`
- requires a URL
This is intentionally enough for Phase 16.1 modeling without yet enforcing the later fetch/materialization policy.
### Closure metadata now includes a dedicated source file
System closures now generate:
- `metadata/freebsd-source.scm`
and embed source information in:
- `metadata/freebsd-base.scm`
- `metadata/store-layout.scm`
- `parameters.scm`
### CLI metadata now exposes the declared FreeBSD source
`scripts/fruix.scm` now emits the following for `fruix system build` and `image`:
- `freebsd_source_name`
- `freebsd_source_kind`
- `freebsd_source_url`
- `freebsd_source_path`
- `freebsd_source_ref`
- `freebsd_source_commit`
- `freebsd_source_sha256`
- `freebsd_source_file`
## Guix comparison
This follows the same broad idea as Guix source modeling:
- Guix uses `origin` in `guix/packages.scm`
- Git-backed sources are modeled with `git-reference` in `guix/git-download.scm`
Phase 16.1 does not copy Guix mechanically, but it adopts the same useful architectural boundary:
- source identity should be a declarative object
- builds should record that source object explicitly
For Fruix, the source kinds are FreeBSD-oriented:
- local source tree snapshots for development
- FreeBSD Git refs/commits
- official FreeBSD `src.txz` archives
## Upstream source forms verified during this step
Verified usable upstream source forms include:
- Git:
- `https://git.FreeBSD.org/src.git`
- release archive:
- `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
- snapshot archive:
- `https://download.freebsd.org/snapshots/amd64/15.0-STABLE/src.txz`
The shorter release URL form above is treated as the canonical example in this phase.
## New files
Added:
- `docs/PLAN_4.md`
- `tests/system/phase16-declarative-source-operating-system.scm.in`
- `tests/system/run-phase16-declarative-source-build.sh`
- `tests/system/validate-phase16-freebsd-source.scm`
## Validation
Passing run:
- `PASS phase16-declarative-source-build`
- workdir:
- `/tmp/fruix-phase16-declarative-source.0LRvaC`
The source model probe confirmed:
```text
local_kind=local-tree
local_path=/usr/src
git_kind=git
git_url=https://git.FreeBSD.org/src.git
git_ref=stable/15
txz_kind=src-txz
txz_url=https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz
txz_sha256=example-sha256
base_source_accessor=ok
```
The closure build confirmed:
```text
closure_path=/frx/store/ac73a2a89c3c3f794462ccde0d9f0952362dc2ae32e631a7e99e07e7363e6118-fruix-system-fruix-freebsd
kernel_store=/frx/store/c2f771a794f94cf168ae26a421ffab33e0ae4765f23882257d5bb7b2947d493d-freebsd-native-kernel-15.0-STABLE-source-model
bootloader_store=/frx/store/a146c9c2fcacdc02454fe3bfd3afb5c5dbea54b6514242d53df0783ef4ee34fd-freebsd-native-bootloader-15.0-STABLE-source-model
runtime_store=/frx/store/530e96440a518821a701db03f9437d7646c0447f8ea55c5df08417e9274dead1-freebsd-native-runtime-15.0-STABLE-source-model
native_base_store_count=3
host_base_store_count=0
freebsd_source_name=host-usr-src
freebsd_source_kind=local-tree
freebsd_source_path=/usr/src
freebsd_source_file=/frx/store/ac73a2a89c3c3f794462ccde0d9f0952362dc2ae32e631a7e99e07e7363e6118-fruix-system-fruix-freebsd/metadata/freebsd-source.scm
declarative_source_model=ok
```
The harness also verified that:
- `metadata/freebsd-source.scm` exists
- `metadata/freebsd-base.scm` contains a nested source block
- `metadata/store-layout.scm` records the declared source explicitly
- native build info files contain a `declared-source` block with:
- `kind . local-tree`
- `path . "/usr/src"`
## Result
Phase 16.1 is complete.
Fruix now has an explicit source declaration layer for FreeBSD bases while preserving the current validated `/usr/src`-driven native build path.
That means Phase 16.2 can focus narrowly on the next real boundary:
- fetching or materializing declared FreeBSD source inputs under Fruix control
- instead of just describing them

View File

@@ -0,0 +1,182 @@
# Phase 16.3: build native FreeBSD base artifacts from materialized source inputs
Date: 2026-04-03
## Goal
Phase 16.3 closes the immediate gap between:
- declarative/materialized FreeBSD source inputs, and
- the native kernel/world/runtime/bootloader build path.
Before this step, Fruix could already:
- describe FreeBSD sources explicitly, and
- materialize them into `/frx/store`
but native base builds still consumed the ambient host source tree through the package plan's `source-root`.
After this step, native FreeBSD base artifacts are driven by the declared source input's **materialized store snapshot** instead.
## Implementation
### Native package materialization now injects materialized source inputs
`modules/fruix/system/freebsd.scm` now prepares native FreeBSD packages differently from copy-based packages.
For native packages, Fruix now:
1. reconstructs the declared `freebsd-source` from the package plan
2. materializes that source under Fruix control
3. rewrites the native build plan to use the materialized source root
4. records both the declared source and the materialized/effective source in the output metadata
5. adds the materialized source store path to the package references
This means native package output identity now depends on:
- the declared source object
- the resolved/materialized source identity
- the materialized source root and tree hash
rather than on an ambient host tree path.
### Native build manifests now record both declared and materialized source identity
Native package manifests now include:
- `declared-source`
- `materialized-source`
- the `native-build-common` block derived from the materialized source root
Native `.freebsd-native-build-info.scm` files now record:
- declared source
- materialized source store path
- materialized source root
- materialized source tree hash
- effective source details such as resolved Git commit or verified archive SHA256
### Source tree hashing now reuses materialized-source metadata when available
The native build common manifest now prefers the materialized source tree hash when one is already known, rather than recomputing it unnecessarily.
### Package caching is now manifest-driven rather than name/version-only
The in-process package materialization cache for native builds now keys on the computed manifest identity rather than only:
- package name
- package version
That is important once multiple source-driven variants of the same native package version can coexist.
### System closures now record source materializations explicitly
`materialize-operating-system` now keeps a dedicated source-materialization cache while building native packages.
Closures now record:
- `metadata/freebsd-source-materializations.scm`
- `materialized-source-store-count`
- `materialized-source-stores`
and system references now include the materialized source stores explicitly.
This makes the source artifacts themselves part of the closure provenance boundary.
### `fruix system` metadata now exposes materialized source stores
`scripts/fruix.scm` now emits for `fruix system build` and `image`:
- `freebsd_source_materializations_file`
- `materialized_source_store_count`
- `materialized_source_stores`
This makes it easy for harnesses and operators to see which source store snapshot was actually used for a build.
## New files
Added:
- `tests/system/phase16-git-materialized-source-operating-system.scm.in`
- `tests/system/run-phase16-source-driven-native-build.sh`
- `docs/reports/phase16-source-driven-native-builds-freebsd.md`
## Validation
Passing runs:
- `PASS phase16-source-driven-native-build`
- `PASS phase16-source-materialization`
- `PASS phase16-declarative-source-build`
### Source-driven native build validation
Validated a native system build from a declared Git source:
- source kind:
- `git`
- source URL:
- `https://git.FreeBSD.org/src.git`
- source ref:
- `stable/15`
- resolved commit during validation:
- `332708a606f6bf0841c1d4a74c0d067f5640fe89`
The test intentionally declared an **unused transitional source-root**:
- `/var/empty/fruix-unused-source-root`
and still built successfully, proving the native build path now uses the materialized source snapshot rather than the ambient declared root.
Confirmed build metadata:
```text
closure_path=/frx/store/efed0b8ee31b4f3f97b8ae04317ccd2f3f8a88e332172e2bf0bb4295271c6023-fruix-system-fruix-freebsd
kernel_store=/frx/store/c7e094a450f017a35c2d4d66451dcda893bc4ac27d810295fa491e41fdae8551-freebsd-native-kernel-15.0-STABLE-git-materialized
bootloader_store=/frx/store/36432acf23cdff7eb7601bd314ea82d75a5e7fbcd3480d753117f4c5f9a7c7db-freebsd-native-bootloader-15.0-STABLE-git-materialized
runtime_store=/frx/store/b6f9ebb5c844aedc3d4395ad08542eb67965a19fc28f68fb0f4b55f91aef1c8f-freebsd-native-runtime-15.0-STABLE-git-materialized
materialized_source_store=/frx/store/bcbf7554ce43d40b1bdc98823eb450ce050529b34bbe2dae2a9df48688004c4b-freebsd-source-stable15-network-source
materialized_source_root=/frx/store/bcbf7554ce43d40b1bdc98823eb450ce050529b34bbe2dae2a9df48688004c4b-freebsd-source-stable15-network-source/tree
resolved_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
source_driven_native_build=ok
```
The harness also verified that:
- closure metadata records exactly one materialized source store
- `metadata/freebsd-source-materializations.scm` exists
- `metadata/store-layout.scm` records the materialized source store count/path
- native build info files for kernel, bootloader, and runtime all record:
- a `declared-source` block with `ref . "stable/15"`
- a `materialized-source` block
- the materialized source store path
- the materialized source root
- the resolved Git commit
- native build info files do **not** use the unused declared source root as their actual `source-root`
### Regression checks
Re-ran:
- `PASS phase16-source-materialization`
- `PASS phase16-declarative-source-build`
The local-tree declarative source build now also drives native base outputs from a materialized local source snapshot in `/frx/store`, while preserving the same declared source metadata interface.
## Result
Phase 16.3 is complete.
Fruix native FreeBSD base artifacts are now built from the declared source input's materialized store snapshot rather than directly from ambient `/usr/src`.
That means Fruix has crossed the key source boundary planned for Phase 16:
- source inputs can be declared
- source inputs can be fetched/materialized
- native base artifacts can now consume those materialized source inputs
The next logical phase is no longer source modeling itself, but what to do with that stronger source boundary:
- side-by-side source revisions
- boot validation from distinct source revisions
- and then installation/deployment ergonomics on top of those source-driven native builds

View File

@@ -0,0 +1,256 @@
# Phase 16.2: materialize declarative FreeBSD source inputs under Fruix control
Date: 2026-04-03
## Goal
Phase 16.2 moves from merely *describing* FreeBSD source inputs to actually *materializing* them under Fruix control.
The objective in this subphase was not yet to switch the native base build path away from ambient `/usr/src`. Instead, it was to establish the missing fetch/materialization layer that later phases can consume.
That means Fruix now knows how to:
- fetch a Git-backed FreeBSD source declaration
- download and verify a `src.txz` declaration
- materialize the resulting source tree into `/frx/store`
- cache downloaded source state under `/frx/var/cache/fruix/freebsd-source`
- record stable source metadata for later native builds
## Implementation
### New source materializer in `modules/fruix/system/freebsd.scm`
Added:
- `materialize-freebsd-source`
and exported it for use by the Fruix CLI.
This materializer now supports all currently modeled source kinds:
- `local-tree`
- `git`
- `src-txz`
### Source artifacts are now first-class store objects
Materialized source outputs are now stored in paths of the form:
- `/frx/store/<hash>-freebsd-source-<name>`
Each source output contains:
- `tree/` or an auto-detected nested source root beneath it
- `.fruix-source`
- `.freebsd-source-info.scm`
- `.references`
The source info file records at least:
- declared source
- effective/resolved source
- materialized store path
- effective source root
- source tree SHA256
- cache path used to produce it
### Cache layout added under `/frx/var/cache/fruix/freebsd-source`
The new materializer caches downloaded source state under:
- `/frx/var/cache/fruix/freebsd-source/git/...`
- `/frx/var/cache/fruix/freebsd-source/archives/...`
Current behavior:
- Git sources use a cached bare repository
- `src.txz` sources use a cached archive file
- repeated materialization of the same resolved source identity reuses the same store output path
### Git source handling
Git materialization now:
- uses `https://git.FreeBSD.org/src.git`
- supports declarations by ref and/or commit
- fetches the selected ref/commit into a cached bare repository
- resolves refs to a concrete commit
- exports that commit into a store materialization
This means Fruix can now represent moving refs declaratively while still recording the exact resolved commit used for a given materialized source tree.
### `src.txz` source handling
`src.txz` materialization now:
- downloads the declared archive URL with `fetch`
- requires and verifies SHA256 for materialization
- extracts the archive into a store materialization
- records both the declared hash and the resulting source tree hash
For release archives, the canonical shorter URL form is now used in validation and documentation, for example:
- `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
rather than the longer doubled-architecture variant.
### Auto-detect the effective source root inside materialized trees
Git exports place the source tree directly at the materialized root.
Official `src.txz` archives instead unpack as:
- `usr/src/...`
inside the extracted directory tree.
To make this usable for later native builds, the materializer now auto-detects the effective source root and records it explicitly. For example:
- Git materialization root:
- `/frx/store/...-freebsd-source-stable15-git-ref/tree`
- `src.txz` materialization root:
- `/frx/store/...-freebsd-source-release15-src-txz/tree/usr/src`
### New user-facing CLI command
Added a new user-facing command path in `scripts/fruix.scm`:
- `fruix source materialize SOURCE-FILE`
with options:
- `--source NAME`
- `--store DIR`
- `--cache DIR`
- `--help`
The command emits machine-readable metadata including:
- declared source fields
- materialized store path
- effective source root
- source tree hash
- cache path
- resolved Git commit if applicable
- verified archive hash if applicable
This gives Phase 16.2 an operator-usable entry point rather than limiting it to internal Scheme helpers.
### Validation tightened for archive-backed sources
`src-txz` source validation now requires:
- URL
- SHA256
This is the right reproducibility boundary for archive downloads.
## Guix comparison
This step continues to mirror the most useful Guix source boundary without copying it mechanically:
- Guix models source objects with `origin`
- Git-backed origins use `git-reference`
Fruix's source materializer now plays a similar role for FreeBSD-specific source inputs:
- local tree snapshots
- FreeBSD Git refs/commits
- official `src.txz` archives
The key preserved idea is the same: source identity should become an explicit, recorded, materialized input rather than ambient host state.
## New files
Added:
- `tests/system/phase16-git-freebsd-source.scm.in`
- `tests/system/phase16-txz-freebsd-source.scm.in`
- `tests/system/run-phase16-source-materialization.sh`
## Validation
Passing run:
- `PASS phase16-source-materialization`
- workdir:
- `/tmp/fruix-phase16-source-materialization.QGuXi1`
### Git validation
Validated a Git declaration:
- name:
- `stable15-git-ref`
- URL:
- `https://git.FreeBSD.org/src.git`
- ref:
- `stable/15`
Resolved/materialized result:
```text
materialized_source_store=/frx/store/dd1cc6b5ffa95b4d0c0f269522d5739da05e0f4ae81b1b314221d28b49d1981f-freebsd-source-stable15-git-ref
materialized_source_root=/frx/store/dd1cc6b5ffa95b4d0c0f269522d5739da05e0f4ae81b1b314221d28b49d1981f-freebsd-source-stable15-git-ref/tree
materialized_source_tree_sha256=d0d8e085d913a511d7fa1ba410040eb697a4cef800f354a092c65249ab3c4eb4
materialized_source_cache_path=/frx/var/cache/fruix/freebsd-source/git/9d432c47301c356bd2cede3400de40870e0b541b276888e34c68b882b9b894c7.git
materialized_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
```
The harness also confirmed:
- repeated materialization returned the same store path
- the cached Git repository exists
- the materialized tree contains:
- `Makefile`
- `sys/conf/newvers.sh`
### `src.txz` validation
Validated an archive declaration:
- name:
- `release15-src-txz`
- URL:
- `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
- SHA256:
- `83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0`
Resolved/materialized result:
```text
materialized_source_store=/frx/store/2e7857fb2c067b32acb482d048b8d1c2eeffdecd213108b3b0a4b2a87d56bc68-freebsd-source-release15-src-txz
materialized_source_root=/frx/store/2e7857fb2c067b32acb482d048b8d1c2eeffdecd213108b3b0a4b2a87d56bc68-freebsd-source-release15-src-txz/tree/usr/src
materialized_source_tree_sha256=afbe26f2213a19685fc2c3b875d26fab67e2cfcd605716cc66f669dabeaf7572
materialized_source_cache_path=/frx/var/cache/fruix/freebsd-source/archives/64ac7cc7d27435406995d63ef0b87ed0c485ce953ee8e9126127ca8f2a451d98-src.txz
materialized_source_sha256=83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0
```
The harness also confirmed:
- repeated materialization returned the same store path
- the cached archive exists
- the effective materialized source root was detected as `tree/usr/src`
- that root contains:
- `Makefile`
- `sys/conf/newvers.sh`
### Regression check
Also re-ran:
- `PASS phase16-declarative-source-build`
This confirmed the new source materialization work did not break the earlier Phase 16.1 declarative source model path.
## Result
Phase 16.2 is complete.
Fruix can now fetch or materialize declared FreeBSD source inputs into `/frx/store` with cache-backed provenance under `/frx/var/cache/fruix/freebsd-source`.
The next step is now clear and narrower:
- teach native FreeBSD kernel/world/runtime builds to consume these materialized source artifacts instead of ambient `/usr/src`
That will be the real handoff from source acquisition to source-driven native base builds.

View File

@@ -0,0 +1,141 @@
# Phase 17.1: side-by-side FreeBSD source revisions in `/frx/store`
Date: 2026-04-03
## Goal
Phase 17.1 verifies that Fruix can treat FreeBSD source revisions the same way it already treats other declarative inputs:
- as explicit inputs,
- as side-by-side store objects,
- and as drivers of distinct native base outputs.
The key question was no longer whether Fruix could materialize a source tree at all. Phase 16 already established that.
The question here was stricter:
- can two distinct FreeBSD source inputs coexist,
- can they both drive native base builds,
- and can they do so even when the user-facing base version label is the same?
That last point matters because it proves the result is driven by source identity rather than by an arbitrary version-string rename.
## Implementation
Added two Phase 17 operating-system templates:
- `tests/system/phase17-git-source-operating-system.scm.in`
- `tests/system/phase17-txz-source-operating-system.scm.in`
These model two distinct source identities:
- a Git source with both:
- ref: `stable/15`
- pinned commit: `332708a606f6bf0841c1d4a74c0d067f5640fe89`
- an official release archive source:
- `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
- sha256:
- `83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0`
Added validation harness:
- `tests/system/run-phase17-source-coexistence.sh`
## Validation design
The harness builds three times:
1. Git-backed system build
2. `src.txz`-backed system build
3. Git-backed rebuild
The Git and `src.txz` systems intentionally share the same:
- base name:
- `source-side-by-side`
- base version label:
- `15.0-source-side-by-side`
while differing only in their declared source/release metadata.
This means distinct outputs cannot be explained away by a version-label rename.
The harness verifies:
- Git rebuild stability when the Git source is pinned by commit
- distinct closure paths for Git vs `src.txz`
- distinct materialized source store paths
- distinct native kernel/bootloader/runtime store paths
- zero host-base stores in both builds
- one materialized source store in each closure
- correct closure metadata for:
- declared source
- materialized source
- materialized source store count/path
- correct native build info for:
- kernel
- runtime
- effective source roots:
- Git: `.../tree`
- `src.txz`: `.../tree/usr/src`
- the continued separation between:
- declared transitional `source-root`
- actual materialized source root used by the native build
## Results
Passing validation:
- `PASS phase17-source-coexistence`
Observed side-by-side closures:
```text
git_closure=/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd
git_closure_rebuild=/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd
txz_closure=/frx/store/02268e19930facb32e12b6ec191f2e5704d1e81033baf3637a889ad15924ff88-fruix-system-fruix-freebsd
base_version_label=15.0-source-side-by-side
same_base_version_label_distinct_sources=ok
```
Observed source identities:
```text
git_source_kind=git
git_source_ref=stable/15
git_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
git_materialized_source_store=/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a
git_materialized_source_root=/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a/tree
txz_source_kind=src-txz
txz_source_url=https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz
txz_source_sha256=83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0
txz_materialized_source_store=/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b
txz_materialized_source_root=/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b/tree/usr/src
```
Observed native base outputs for the same version label:
```text
git_native_base_stores=/frx/store/4b615431ec25c500a3bf0ed70ce39e2ebf4f584994a53756268e4383962bc86b-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3a5a0b2b88b4757cf9cb4e3040f992d8fdb5bd9a7f1b186da983854cd95392c5-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/177f78e7f2932986a380187eb09dc34cc2cd9a146c5ed1fe1f00aae15ddf78d9-freebsd-native-runtime-15.0-source-side-by-side
txz_native_base_stores=/frx/store/0c5141a86fa9c1974102f2bd8766eb3ab787b97dcccb71f17d80aefbe8ed4f3e-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3de6592f50a735d8461662cb393fc413325ce24ded45d4bb494525896f8cb5eb-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/46d256305198ee7d745b9032c71085aba97d55fdf7a0d3d2017dd4455173205d-freebsd-native-runtime-15.0-source-side-by-side
```
Those outputs differ only by content-address prefix, not by the human-readable version suffix. That is exactly the property Phase 17.1 needed.
## Key observation
A moving Git ref by itself is not a reproducibility boundary. For the reproducible half of this validation, the Git source was pinned by commit while still retaining its `stable/15` ref metadata.
This fits the intended Fruix model:
- refs are useful selectors,
- commits are the stable Git identity boundary,
- archive SHA256 values are the stable `src.txz` identity boundary.
## Result
Phase 17.1 is complete.
Fruix can now hold at least two distinct FreeBSD source revisions side by side in `/frx/store` and build distinct native FreeBSD base artifacts from them, even when the visible base version label is kept the same.

View File

@@ -0,0 +1,77 @@
# Phase 17.3: clarify FreeBSD source provenance, caching, and update policy
Date: 2026-04-03
## Goal
Phase 17.3 turns the source behavior implemented in Phases 16 through 17.2 into an explicit repo-level policy.
The main question was no longer whether Fruix *can* fetch and boot from declared FreeBSD sources. It can.
The question here is:
- how should operators think about source selectors versus stable source identity?
- what exactly lives in cache versus store?
- when should native outputs be invalidated?
- what is the intended policy for moving Git refs, pinned commits, and archive hashes?
## Added documentation
Added:
- `docs/freebsd-source-policy.md`
This document explains:
- supported source kinds:
- `local-tree`
- `git`
- `src-txz`
- declared source vs effective source
- cache locations under:
- `/frx/var/cache/fruix/freebsd-source`
- materialized source outputs under:
- `/frx/store/*-freebsd-source-*`
- effective identity rules for:
- local-tree snapshots
- Git refs/commits
- verified `src.txz` archives
- effective source root detection rules:
- `tree`
- `tree/usr/src`
- native build invalidation policy
- closure provenance policy
- update policy for moving refs vs pinned commits
- the current no-hidden-patch-layer rule
## Key policy conclusions
The repo now states clearly that:
- Git refs are selectors, not stable reproducibility boundaries
- resolved Git commits are the effective Git identity boundary
- `src.txz` URLs are not enough by themselves; `sha256` is required
- local trees are mutable selectors; the materialized snapshot and filtered tree hash are the meaningful Fruix identity
- native FreeBSD base outputs must invalidate when the materialized source identity changes, even if the visible base version label does not
- cache objects are transport optimizations, not the final identity boundary
## Relation to validated behavior
The new policy document matches the validated Phase 17 behavior:
- Phase 17.1 proved that distinct source identities can coexist side by side and produce different native outputs for the same visible base version label
- Phase 17.2 proved that systems built from those distinct source identities can boot successfully through the validated native path
## Result
Phase 17.3 is complete.
The repo now clearly explains how FreeBSD source objects are:
- fetched
- cached
- identified
- invalidated
- and consumed by native FreeBSD base builds
That completes Phase 17 and leaves Fruix in a better position to begin the installation/deployment work in Phase 18.

View File

@@ -0,0 +1,98 @@
# Phase 17.2: boot systems from distinct declared FreeBSD source revisions
Date: 2026-04-03
## Goal
Phase 17.2 extends Phase 17.1 from:
- side-by-side source-driven builds
to:
- side-by-side source-driven **boots**.
The important requirement was not visible runtime behavior differences between the guests. The requirement was that Fruix should be able to:
- build bootable systems from at least two distinct declared source revisions
- boot both systems with the validated native base path
- preserve source identity in the resulting system/image metadata
## Implementation
Added boot validation harness:
- `tests/system/run-phase17-source-revisions-qemu.sh`
This script renders the Phase 17 source templates from Phase 17.1 and then boots two systems under the already-validated QEMU/UEFI/TCG path:
- Git-backed source:
- ref: `stable/15`
- pinned commit: `332708a606f6bf0841c1d4a74c0d067f5640fe89`
- release archive source:
- `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz`
- sha256:
- `83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0`
The harness reuses:
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
and checks both guest runtime behavior and image/build provenance metadata.
## Validation
Passing run:
- `PASS phase17-source-revisions-qemu`
Confirmed booted systems from two distinct source identities:
```text
git_closure=/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd
txz_closure=/frx/store/02268e19930facb32e12b6ec191f2e5704d1e81033baf3637a889ad15924ff88-fruix-system-fruix-freebsd
```
Confirmed source metadata recorded in image/build artifacts:
```text
git_source_kind=git
git_source_ref=stable/15
git_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
git_materialized_source_store=/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a
txz_source_kind=src-txz
txz_source_url=https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz
txz_source_sha256=83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0
txz_materialized_source_store=/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b
```
Confirmed distinct native base outputs used by the two boots:
```text
git_native_base_stores=/frx/store/4b615431ec25c500a3bf0ed70ce39e2ebf4f584994a53756268e4383962bc86b-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3a5a0b2b88b4757cf9cb4e3040f992d8fdb5bd9a7f1b186da983854cd95392c5-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/177f78e7f2932986a380187eb09dc34cc2cd9a146c5ed1fe1f00aae15ddf78d9-freebsd-native-runtime-15.0-source-side-by-side
txz_native_base_stores=/frx/store/0c5141a86fa9c1974102f2bd8766eb3ab787b97dcccb71f17d80aefbe8ed4f3e-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3de6592f50a735d8461662cb393fc413325ce24ded45d4bb494525896f8cb5eb-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/46d256305198ee7d745b9032c71085aba97d55fdf7a0d3d2017dd4455173205d-freebsd-native-runtime-15.0-source-side-by-side
```
Confirmed both guests booted successfully through the validated PID 1 path:
- Shepherd ran as PID 1 in both boots
- `sshd` was running in both boots
- boot backend:
- `qemu-uefi-tcg`
Validation artifacts:
- Git serial log:
- `/tmp/fruix-phase17-source-qemu.7Za50q/git/serial.log`
- `src.txz` serial log:
- `/tmp/fruix-phase17-source-qemu.7Za50q/txz/serial.log`
## Result
Phase 17.2 is complete.
Fruix now boots systems built from at least two distinct declared FreeBSD source revisions while preserving those source identities in system/image metadata.
That means Phase 17 is no longer just about build-time coexistence. The validated native boot path now also works across distinct source identities.

View File

@@ -0,0 +1,191 @@
# Phase 18.2: minimal Fruix-managed installer environment on FreeBSD
Date: 2026-04-04
## Goal
Phase 18.2 builds on the Phase 18.1 host-driven install primitive.
The goal here is not a polished live installer. The goal is a small Fruix-managed environment that can:
- boot as its own Fruix system,
- carry a selected target Fruix system closure and rootfs payload,
- install that target system onto a second disk from inside the booted environment,
- and leave the installed target bootable.
## Implementation
### New installer-environment API
Added in `modules/fruix/system/freebsd.scm`:
- `installer-operating-system`
- `operating-system-installer-image-spec`
- `materialize-installer-image`
The installer environment is derived from the selected target operating system, but with installer-specific behavior:
- host name defaults to:
- `<target-host-name>-installer`
- init mode is kept on the currently most stable installer path:
- `freebsd-init+rc.d-shepherd`
- the installer image root label is distinct:
- `fruix-installer-root`
- `sshd` is enabled for operator/debug access
- installer accounts needed for SSH/DHCP are ensured if absent:
- `sshd`
- `_dhcp`
### Bootable installer image contents
`materialize-installer-image` now produces a bootable image that contains:
- the installer system closure and its runtime store closure
- the selected target system closure
- the selected target system's referenced store items
- a prebuilt target rootfs tree staged under:
- `/var/lib/fruix/installer/target-rootfs`
- installer plan/state files under:
- `/var/lib/fruix/installer`
- installer helper scripts:
- `/usr/local/libexec/fruix-installer-run`
- `/usr/local/etc/rc.d/fruix-installer`
The booted installer environment runs a background rc.d job that:
- partitions the selected target disk
- creates EFI + UFS filesystems
- copies the staged target rootfs onto the target
- copies only the target system's required store items into the target `/frx/store`
- installs the target's `loader.efi`
- writes `/var/lib/fruix/install.scm` on the target
- records installer state in:
- `/var/lib/fruix/installer/state`
- logs to:
- `/var/log/fruix-installer.log`
### New CLI action
Added in `scripts/fruix.scm`:
- `fruix system installer`
Added option:
- `--install-target-device DEVICE`
This action materializes a bootable installer image in `/frx/store` and emits metadata for:
- installer image paths
- installer closure path
- target closure path
- target install device
- installer state/log paths
- declared/materialized FreeBSD source metadata
- target/native/runtime store metadata
### FreeBSD virtio target-device detail
A practical detail surfaced during validation:
- the correct FreeBSD virtio block device node for the second QEMU disk is:
- `/dev/vtbd1`
The earlier Linux-flavored guess:
- `/dev/vtblk1`
was wrong for the actual FreeBSD device node namespace in this environment.
The installer defaults were updated accordingly.
### Small image-builder correctness fix
While doing this work I also fixed `materialize-bhyve-image` so its generated UFS filesystem label respects the requested:
- `root-partition-label`
instead of always hardcoding:
- `fruix-root`
This matters for the installer image because it needs a distinct root label while the target disk still uses the normal target label.
## Validation
Added validation artifacts:
- `tests/system/phase18-installer-target-operating-system.scm.in`
- `tests/system/run-phase18-installer-environment.sh`
Passing validations:
- `PASS phase18-installer-environment`
- regression re-check:
- `PASS phase18-system-install`
- regression re-check:
- `PASS phase17-source-revisions-qemu`
Validated installer-environment result:
```text
installer_image_store_path=/frx/store/fb038dbf5dac2ad1bb767a264d3a268915f489b936dc5dd32425645102d3da48-fruix-installer-image-fruix-freebsd-installer
installer_disk_image=/frx/store/fb038dbf5dac2ad1bb767a264d3a268915f489b936dc5dd32425645102d3da48-fruix-installer-image-fruix-freebsd-installer/disk.img
installer_disk_capacity=16g
installer_root_size=14g
target_disk_capacity=12g
install_target_device=/dev/vtbd1
installer_closure_path=/frx/store/ea821f20b579684877fdc86a2a1e80485cf2b12d9d32f74f42e368d738c2ad4d-fruix-system-fruix-freebsd-installer
target_closure_path=/frx/store/7ee225db532b6973e385f8507d2d61aec3cd3aeb0864f983c2ae4b6e149ef3b0-fruix-system-fruix-freebsd
freebsd_source_kind=git
freebsd_source_ref=stable/15
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
materialized_source_store=/frx/store/7563df2714ae7fa9bd40b83c74512ffe2cb2ad91b297915591b55c76edbb2fcb-freebsd-source-stable15-installer-target-source
installer_state=done
installer_sshd_status=running
target_esp_fstype=msdosfs
target_root_fstype=ufs
target_shepherd_status=running
target_sshd_status=running
installer_environment_boot=ok
installer_environment_install=ok
installed_target_boot=ok
```
The harness verified all of the following:
1. `fruix system installer` produces a bootable installer image in `/frx/store`
2. validation boots a workdir copy of that installer disk image so the store artifact itself is not mutated during the boot/install run
3. the installer environment boots successfully under QEMU/UEFI/TCG
4. the installer environment becomes reachable over SSH
5. `/run/current-system` inside the installer environment points at the installer closure
6. the installer rc.d job reaches:
- `state=done`
7. the installer log records:
- `fruix-installer:done`
8. the target raw disk is transformed into a valid GPT-installed Fruix target with:
- EFI filesystem: `msdosfs`
- root filesystem: `ufs`
- `EFI/BOOT/BOOTX64.EFI` present
- `/var/lib/fruix/install.scm` present
9. the installed target then boots successfully as its own Fruix system under QEMU/UEFI/TCG
10. after target boot:
- `/run/current-system` points at the target closure
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` reports running
- `sshd` is running
- activation completed successfully
## Result
Phase 18.2 is complete.
Fruix now has a real installer substrate on FreeBSD:
- a bootable Fruix-managed installer image
- a target closure bundled inside that installer environment
- in-guest non-interactive installation onto a second disk
- validated boot of the installed result
The next step is Phase 18.3:
- produce a bootable installer ISO for UEFI systems, rather than only a disk-image-style installer environment.

View File

@@ -0,0 +1,277 @@
# Phase 18.3: bootable Fruix installer ISO on FreeBSD
Date: 2026-04-04
## Goal
Phase 18.3 extends the Phase 18.2 installer-environment work from a disk-image-style installer into a UEFI-bootable ISO artifact.
The intended first ISO is deliberately narrow:
- UEFI only
- serial-console-friendly
- non-interactive install flow reused from Phase 18.1/18.2
- target disk installation still performed by the same Fruix-managed in-guest installer logic
## Implementation
### New API
Added in `modules/fruix/system/freebsd.scm`:
- `operating-system-installer-iso-spec`
- `materialize-installer-iso`
The system module split done immediately before this phase was also exercised during this work.
### New CLI action
Added in `scripts/fruix.scm`:
- `fruix system installer-iso`
This action emits metadata for:
- ISO store path
- ISO image path
- EFI boot image path
- installer root image path
- installer and target closure paths
- installer state/log paths
- declared/materialized FreeBSD source metadata
- store closure counts
### ISO boot model
The ISO does not try to run the Fruix installer directly from a read-only cd9660 root.
Instead it uses a small UEFI El Torito boot image plus an in-memory installer root image:
1. a small FAT EFI boot image contains `EFI/BOOT/BOOTX64.EFI`
2. the ISO root contains real boot assets under `/boot`
3. the ISO root also contains `/boot/root.img`
4. `loader.conf` on the ISO is augmented with:
- `mdroot_load="YES"`
- `mdroot_type="mfs_root"`
- `mdroot_name="/boot/root.img"`
- `vfs.root.mountfrom="ufs:/dev/md0"`
- `vfs.root.mountfrom.options="rw"`
A practical loader detail surfaced during validation:
- setting `rootdev` or `currdev` to `md0:` in the ISO loader path is wrong for this loader configuration and caused an early EFI-loader crash before kernel handoff
- the reliable ISO path is to let loader keep its current device on the CD media, preload `/boot/root.img`, and pass only `vfs.root.mountfrom=ufs:/dev/md0`
This preserves the existing Fruix installer environment semantics while avoiding the need to make the whole installer operate directly from a read-only ISO root.
### Installer root image contents
`materialize-installer-iso` stages the same installer payload model already validated in Phase 18.2:
- installer closure
- target closure
- target runtime store closure needed for installation/boot
- staged target rootfs under `/var/lib/fruix/installer/target-rootfs`
- installer plan and state files under `/var/lib/fruix/installer`
- installer helper scripts:
- `/usr/local/libexec/fruix-installer-run`
- `/usr/local/etc/rc.d/fruix-installer`
The ISO root image is then built as a UFS image and embedded as `/boot/root.img`.
### Split-regression fixes found during this work
While exercising the refactored split modules, two issues surfaced and were fixed:
1. `string-hash` name-clash warnings
- the old helper name collided with Guile/SRFI bindings
- it was renamed to `sha256-string`
2. missing `prefix-materializer-version`
- this constant was accidentally omitted when `modules/fruix/system/freebsd.scm` was split
- the missing definition was restored in `modules/fruix/system/freebsd/build.scm`
## Validation
### Completed smoke validation
A host-side smoke build was completed successfully for the new ISO builder using a host-staged operating-system definition:
- command pattern:
- `fruix system installer-iso ...`
- result:
- successful ISO materialization in a temporary store
- artifact checks performed:
- `etdump` reports an EFI El Torito boot entry
- the ISO contains:
- `boot/kernel/kernel`
- `boot/kernel/linker.hints`
- `boot/loader.conf`
- `boot/loader.efi`
- `boot/root.img`
- `boot/loader.conf` inside the ISO contains the expected `mdroot_*` and `vfs.root.mountfrom` entries
Example smoke-build metadata:
```text
action=installer-iso
iso_volume_label=FRUIX_INSTALLER
iso_store_path=/tmp/...-fruix-installer-iso-fruix-freebsd-installer
iso_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso
boot_efi_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img
root_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/root.img
installer_closure_path=/tmp/...-fruix-system-fruix-freebsd-installer
target_closure_path=/tmp/...-fruix-system-fruix-freebsd
```
### End-to-end harness validation
Added:
- `tests/system/run-phase18-installer-iso.sh`
This harness validates the full Phase 18.3 flow:
1. build installer ISO
2. boot it under QEMU/UEFI/TCG
3. install onto a target disk from inside the booted ISO environment
4. boot the installed target
Passing validation:
- `PASS phase18-installer-iso`
Validated result summary:
```text
installer_iso_store_path=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer
installer_iso_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso
installer_boot_efi_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img
installer_root_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/root.img
install_target_device=/dev/vtbd0
freebsd_source_kind=git
freebsd_source_ref=stable/15
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
materialized_source_store=/frx/store/459499e0eb29f4c73ad455060dd2502d21fb56f205c0a676831cf723b3a0c378-freebsd-source-stable15-installer-iso-target-source
installer_state=done
installer_sshd_status=running
target_esp_fstype=msdosfs
target_root_fstype=ufs
target_shepherd_status=running
target_sshd_status=running
installer_iso_boot=ok
installer_iso_install=ok
installed_target_boot=ok
```
Notable QEMU-specific ISO validation detail:
- unlike the disk-image-style installer environment from Phase 18.2, the ISO boots from `cd0`, so the target virtio disk appears as:
- `/dev/vtbd0`
- the earlier installer-environment default:
- `/dev/vtbd1`
remains correct for the disk-image installer, but not for the ISO path
The harness verified all of the following:
1. `fruix system installer-iso` produces a bootable ISO artifact in `/frx/store`
2. the ISO boots successfully under QEMU/UEFI/TCG
3. the booted installer ISO environment becomes reachable over SSH
4. `/run/current-system` inside the installer ISO points at the installer closure
5. the installer rc.d job reaches:
- `state=done`
6. the installer log records:
- `fruix-installer:done`
7. the installed target disk contains:
- GPT partitioning
- EFI filesystem: `msdosfs`
- root filesystem: `ufs`
- `EFI/BOOT/BOOTX64.EFI`
- `/var/lib/fruix/install.scm`
8. the installed target then boots successfully as its own Fruix system under QEMU/UEFI/TCG
9. after target boot:
- `/run/current-system` points at the target closure
- shepherd is running
- `sshd` is running
- activation completed successfully
### Real XCP-ng validation
Added:
- `tests/system/run-phase18-installer-iso-xcpng.sh`
This harness validates the same installer-iso workflow on the approved real XCP-ng path:
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- ISO SR: `537a6219-8452-7cb5-8d56-5eed6910c7a2`
- target VDIs:
- `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
- `7061d761-3639-4bec-87f7-2ba1af924eaa`
Because the current `xo-cli disk.import @=/path/to.iso` path returned an HTTP 500 error in this environment, the harness imports the ISO into the ISO SR via a temporary local HTTP URL, then inserts the resulting ISO VDI into the VM's CD drive.
Passing validation:
- `PASS phase18-installer-iso-xcpng`
Validated result summary:
```text
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
iso_id=<temporary-imported-iso-vdi>
guest_ip=192.168.213.62
installer_state=done
installer_target_device=/dev/ada0
kern_disks=cd0 ada1 ada0
installer_run_current_system=/frx/store/16969e825dbb65b5c27180030d4a7d98821893460fb3dccdc863ff6156ed61e0-fruix-system-fruix-freebsd-installer
installer_sshd_status=running
target_run_current_system=/frx/store/a98d3af6a1afbc4a927d47cea6458d5a70747b051ed994e5d9ff1ae79c4f2b42-fruix-system-fruix-freebsd
target_sshd_status=running
target_shepherd_status=running
```
Important XCP-ng-specific details:
- the installer ISO still boots from:
- `cd0`
- on this Xen HVM path, the primary target disk is exposed through Xen block front as `xbd0` and appears to FreeBSD as:
- `/dev/ada0`
- therefore the XCP-ng installer-iso path must target:
- `/dev/ada0`
rather than QEMU's:
- `/dev/vtbd0`
- the visible EFI console can appear to stop at:
- `console vidconsole is unavailable`
but boot still continues and the installer becomes reachable over SSH; that message was not the actual failure mode on XCP-ng
The harness verified all of the following on the real VM path:
1. `fruix system installer-iso` builds a bootable ISO with `--install-target-device /dev/ada0`
2. the ISO can be imported into the operator-approved ISO SR and attached to the approved VM
3. the VM boots the Fruix installer ISO successfully under UEFI
4. the installer environment becomes reachable over SSH
5. inside the installer guest:
- `kern.disks` includes `cd0` and `ada0`
- `/run/current-system` points at the installer closure
- the installer reaches `state=done`
6. the installed target on `ada0` is partitioned and formatted correctly
7. after ejecting the ISO and rebooting, the installed target boots successfully on the same XCP-ng VM
8. after target boot:
- `/run/current-system` points at the target closure
- shepherd is running
- `sshd` is running
- activation completed successfully
- `/var/lib/fruix/install.scm` still records the materialized source store provenance
## Result
Phase 18.3 is complete.
Fruix now has a validated bootable UEFI installer ISO on FreeBSD that can:
- boot into a Fruix-managed installer environment from ISO media
- perform the non-interactive installation flow onto a target disk
- boot the installed target successfully
- and do so on both:
- local `QEMU/UEFI/TCG`
- the approved real `XCP-ng` VM path

View File

@@ -0,0 +1,167 @@
# Phase 18.1: minimal non-interactive Fruix installation flow on FreeBSD
Date: 2026-04-03
## Goal
Phase 18.1 turns Fruix's existing closure/rootfs/image machinery into a real installation workflow.
The goal is not a polished installer yet. The goal is a repeatable, non-interactive install path that can:
- take a declarative Fruix system,
- partition and format a target disk or image,
- populate it with the selected system closure,
- install boot assets,
- and leave the target bootable.
## Implementation
### New install spec and installer entry point
Added in `modules/fruix/system/freebsd.scm`:
- `operating-system-install-spec`
- `install-operating-system`
The installer currently supports:
- raw image-file targets
- `/dev/...` block-device targets
For raw image-file targets, Fruix now:
- creates/truncates the target image
- attaches it with `mdconfig`
- creates a GPT layout
- adds:
- an EFI partition
- a FreeBSD UFS root partition
- formats them with:
- `newfs_msdos`
- `newfs`
- mounts them
- stages the declarative Fruix rootfs
- copies the closure and referenced `/frx/store` items into the installed root
- installs `loader.efi` to `EFI/BOOT/BOOTX64.EFI`
- writes install metadata to:
- `/var/lib/fruix/install.scm`
### Rootfs staging was factored for reuse
Added internal helper:
- `populate-rootfs-from-closure`
This lets image generation and installation reuse the same rootfs staging logic while differing in how the final target is created.
### New CLI action
Added user-facing command support in `scripts/fruix.scm`:
- `fruix system install`
New system option:
- `--target PATH`
Install metadata now emits machine-readable fields including:
- `target`
- `target_kind`
- `target_device`
- `esp_device`
- `root_device`
- `install_metadata_path`
- `disk_capacity`
- `root_size`
- declared/materialized FreeBSD source metadata
- closure/native/runtime store metadata
### Validation harnesses
Added:
- `tests/system/phase18-install-operating-system.scm.in`
- `tests/system/run-phase18-system-install.sh`
The Phase 18 install validation uses the already-validated boot mode:
- `freebsd-init+rc.d-shepherd`
This keeps the install-flow validation focused on installation mechanics rather than on the separate Shepherd-as-PID-1 boot path.
## Validation
Passing validation:
- `PASS phase18-system-install`
- regression re-check:
- `PASS phase17-source-revisions-qemu`
Validated install result:
```text
target_image=/tmp/fruix-phase18-install.CyrgKc/installed.img
target_kind=raw-file
disk_capacity=12g
root_size=10g
closure_path=/frx/store/ee486985797103aa5d3eeeef7f2cf066bcbd6839cd81083dbe626a594e71a703-fruix-system-fruix-freebsd
freebsd_source_kind=git
freebsd_source_ref=stable/15
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
materialized_source_store=/frx/store/a892afb425235de71c9da38884e2ebdba5dafd3a1993f432fe7c446f5af2151f-freebsd-source-stable15-install-source
native_base_store_count=3
install_metadata_path=/var/lib/fruix/install.scm
esp_fstype=msdosfs
root_fstype=ufs
shepherd_status=running
sshd_status=running
install_flow=non_interactive
init_mode=freebsd-init+rc.d-shepherd
install_target_boot=ok
```
The harness verified all of the following:
- GPT partitioning is created on the target image
- the installed ESP is a valid `msdosfs` filesystem
- the installed root partition is `ufs`
- `EFI/BOOT/BOOTX64.EFI` exists on the target
- `/run/current-system` points at the installed closure in `/frx/store`
- the installed closure exists under the target's `/frx/store`
- `/var/lib/fruix/install.scm` exists and records:
- closure path
- store items
- install spec
- materialized source provenance via the referenced closure/store items
- the installed system boots under local QEMU/UEFI/TCG
- after boot:
- `sshd` is running
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` reports running
- activation completed successfully
## QEMU SMP note
I tested the idea of defaulting local QEMU validation to 8 vCPUs.
Result:
- for local `qemu-system-x86_64` under **TCG**, higher SMP did not help this validation path and in practice regressed boot responsiveness
- the harnesses therefore keep `QEMU_SMP` configurable but retain a conservative default of `2` for TCG-based local validation
This keeps the validated path reliable while still allowing manual override when useful.
## Result
Phase 18.1 is complete.
Fruix now has a real, repeatable, non-interactive installation workflow for FreeBSD systems:
- declarative system input
- native/source-driven closure output
- partition/format/install to a target image or disk
- bootable installed result
The next step is Phase 18.2:
- a minimal Fruix-managed installer environment that can boot into an install context and run this workflow from within that environment.

View File

@@ -0,0 +1,143 @@
# Phase 19.1: canonical Fruix deployment workflow on FreeBSD
Date: 2026-04-04
## Goal
Phase 19.1 is about turning Fruix's already-validated closure/image/install behavior into a clear operator-facing deployment story.
The verification target here is documentation clarity rather than a new low-level boot primitive.
The repo needed a single coherent explanation of how Fruix expects operators to:
- build a system closure
- materialize a rootfs or image
- install directly to an image or block device
- use the installer image and installer ISO paths
- roll forward to a candidate declaration
- roll back to an earlier declaration
## Result
Phase 19.1 is complete.
The repository now documents a first-class Fruix deployment workflow in:
- `docs/system-deployment-workflow.md`
That document defines the current canonical command surface and explains how the already-existing validated paths fit together operationally.
## What was documented
### Canonical frontend
The documented user-facing frontend is now explicitly:
- `./bin/fruix system ...`
This includes the currently supported deployment-oriented actions:
- `build`
- `rootfs`
- `image`
- `install`
- `installer`
- `installer-iso`
### Canonical deployment model
The workflow document now defines Fruix's current deployment model as:
1. declare a system in Scheme
2. build the system closure in `/frx/store`
3. materialize the artifact appropriate to the target environment
4. boot or install that artifact
5. treat the resulting closure path and emitted provenance metadata as the deployment identity
### Roll-forward and rollback semantics
The document makes explicit an important current design point:
- Fruix rollback is already real at the declaration/closure/deployment layer
- but it is not yet a first-class installed-system generation switch operation
So the documented rollback workflow today is:
- retain the earlier declaration
- rebuild or rematerialize it
- redeploy or reboot that earlier closure again
That matches what Fruix has already validated in earlier phases.
### Platform-specific installer target-device detail
The workflow document also records the now-important target-device distinctions between validated environments:
- installer disk-image path under QEMU:
- `/dev/vtbd1`
- installer ISO path under QEMU:
- `/dev/vtbd0`
- installer ISO path under XCP-ng:
- `/dev/ada0`
That makes the deployment story less harness-specific and more operator-explicit.
## Why this satisfies Phase 19.1
Before this phase, Fruix already had the machinery for:
- building declarative system closures
- generating bootable images
- performing direct non-interactive installation
- booting a Fruix installer environment
- booting and installing from a Fruix installer ISO
- rollback-friendly redeploy of earlier declarations
What was missing was a repo-level explanation that unified those into a single operator workflow.
The new document closes that gap by connecting:
- Phase 10 command-surface work
- Phase 15 redeploy/rollback validation
- Phase 18 install and installer-media validation
- and the recent QEMU + XCP-ng installer ISO validation
## Current boundaries now made explicit
The documentation intentionally records what Fruix has **not** solved yet:
- installed-system generation links
- explicit rollback targets and generation metadata
- a first-class `switch` or `reconfigure` command
- installed-system rollback as an in-place operator workflow
- GC-root management for installed systems
Those are left for later Phase 19 steps rather than being blurred into the current deployment story.
## References to existing validation
The documented workflow rests on already-passing validation paths, including:
- `PASS phase18-system-install`
- `PASS phase18-installer-environment`
- `PASS phase18-installer-iso`
- `PASS phase18-installer-iso-xcpng`
- `PASS phase15-base-rollback-qemu`
- `PASS phase15-base-rollback-xcpng`
## Conclusion
Phase 19.1 is now complete.
Fruix has a documented canonical deployment workflow for FreeBSD covering:
- build
- image generation
- direct install
- installer-media install
- roll-forward
- rollback by redeploying an earlier declaration
The next step is Phase 19.2:
- model installed-system generations, rollback targets, and deployment roots more explicitly.

View File

@@ -0,0 +1,192 @@
# Phase 19.2: explicit installed-system generation layout on FreeBSD
Date: 2026-04-04
## Goal
Phase 19.2 is about making Fruix's installed-system generation model more explicit.
The target here is not yet a full Guix-equivalent in-place `switch-generation` workflow.
The immediate goal is to stop relying mainly on harness knowledge and implicit symlink expectations by recording installed deployment state more explicitly on disk.
## Decision
Fruix now follows this design direction:
- keep Guix-like **semantics**
- do not mirror Guix's installed-system/profile layout **mechanically 1:1**
What Fruix preserves from Guix:
- immutable closure identity
- rollback-friendly deployment semantics
- explicit current deployment pointer
- GC-root-style retention links
- `/run/current-system` as the active runtime boundary
What Fruix intentionally changes:
- installed-system generation state is represented as a small metadata-bearing directory
- the generation model is recorded under a Fruix-native path
- deployment metadata and provenance are easier to inspect directly without reconstructing intent from symlink layout alone
## Implemented layout
Installed systems now record an explicit generation root under:
- `/var/lib/fruix/system`
Current validated initial layout:
```text
/var/lib/fruix/system/
current -> generations/1
current-generation
generations/
1/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm
```
Installed systems now also create explicit retention roots under:
- `/frx/var/fruix/gcroots`
Current validated initial layout:
```text
/frx/var/fruix/gcroots/
current-system -> /frx/store/...-fruix-system-...
system-1 -> /frx/store/...-fruix-system-...
```
Important compatibility point:
- `/run/current-system` still points directly at the active closure in `/frx/store`
That means the new explicit generation model strengthens deployment metadata without changing the already-validated runtime contract used by activation, rc.d integration, and service startup.
## Code changes
### `modules/fruix/system/freebsd/media.scm`
Added explicit generation-layout helpers:
- generation metadata object writer
- generation provenance object writer
- generation layout population for staged rootfs trees
The system rootfs staging path now creates explicit generation state during rootfs population.
That affects:
- direct rootfs materialization
- direct image materialization
- direct installation targets
- target rootfs payloads staged inside installer images
- target rootfs payloads staged inside installer ISOs
The direct install path now also refreshes the generation layout after writing:
- `/var/lib/fruix/install.scm`
so the generation directory carries the same install metadata.
### Installer runtime path
The generated installer runtime script now also copies install metadata into:
- `/var/lib/fruix/system/generations/1/install.scm`
on the installed target.
This keeps direct-install and installer-mediated installs aligned.
## New validation harness
Added:
- `tests/system/run-phase19-generation-layout-qemu.sh`
This harness builds on the already-passing direct install validation from Phase 18.1 and then verifies the new explicit generation layout on the installed target image.
Passing validation:
- `PASS phase19-generation-layout-qemu`
Validated result summary:
```text
closure_path=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
current_generation=1
current_link=generations/1
generation_closure=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
gcroot_current=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
gcroot_generation=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
run_current_system_target=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
generation_layout=explicit
generation_layout_validation=ok
```
The harness verified all of the following:
1. the installed target contains:
- `/var/lib/fruix/system`
2. the current generation pointer exists and resolves to:
- `generations/1`
3. the generation directory contains:
- `closure`
- `metadata.scm`
- `provenance.scm`
- `install.scm`
4. the generation closure link points at the installed closure in `/frx/store`
5. the generation metadata records:
- closure path
- install metadata path
6. the generation install metadata records:
- closure path
- materialized source provenance
7. explicit retention roots exist under:
- `/frx/var/fruix/gcroots/current-system`
- `/frx/var/fruix/gcroots/system-1`
8. those GC-root-style links point at the same active closure
9. `/run/current-system` still points directly at the active closure path
10. existing install boot validation remains intact
## Relationship to Guix
Fruix now has an explicit installed-system generation model, but it is still intentionally not a byte-for-byte clone of Guix's on-disk conventions.
The design choice is:
- preserve Guix's deployment semantics
- use a Fruix-native metadata-oriented representation where that improves clarity for operators and debugging
That decision is documented separately in:
- `docs/GUIX_DIFFERENCES.md`
## Current limitations
This phase does **not** yet add:
- multi-generation switching in place
- a `switch`/`reconfigure` command
- an operator-facing rollback command that flips from current to a previous installed generation without redeploy
- explicit `rollback` link management beyond the initial current-generation layout
Those belong to later Phase 19 work.
## Conclusion
Phase 19.2 is complete.
Fruix now has a clearer, explicit installed-system generation and retention-root model on FreeBSD:
- generation metadata is recorded under `/var/lib/fruix/system`
- retention links are recorded under `/frx/var/fruix/gcroots`
- `/run/current-system` remains stable as the runtime boundary
- and the model is documented in Fruix-native terms for Guix-familiar operators

View File

@@ -0,0 +1,226 @@
# Phase 19.3: installed-system rollback workflow on FreeBSD
Date: 2026-04-04
## Goal
Phase 19.3 is about validating installed-system rollback through the intended operator-facing workflow, not only through host-side build/image redeploy harnesses.
The key question was:
- can an already-installed Fruix system move between recorded generations coherently, using an operator-facing command surface on the target itself?
## Decision
The current Fruix solution is intentionally modest.
Fruix now provides a small installed-system helper on the target itself:
- `/usr/local/bin/fruix`
Validated in-guest commands:
- `fruix system status`
- `fruix system switch /frx/store/...-fruix-system-...`
- `fruix system rollback`
Important scope choice:
- `switch` assumes the candidate closure is already present on the target's `/frx/store`
- Fruix does **not** yet fetch or transfer that closure onto the target automatically
That keeps Phase 19.3 focused on generation-state correctness rather than introducing a larger store-transfer story prematurely.
## Implemented model
Installed systems now support the following validated operator pattern:
1. build a candidate closure with the host-side Fruix frontend
2. stage that closure into the installed system's `/frx/store`
3. run:
- `fruix system switch /frx/store/...candidate...`
4. reboot into the candidate generation
5. if needed, run:
- `fruix system rollback`
6. reboot into the recorded rollback generation
The installed system now records explicit rollback state under:
```text
/var/lib/fruix/system/
current -> generations/N
current-generation
rollback -> generations/M
rollback-generation
generations/
...
```
and explicit rollback reachability under:
```text
/frx/var/fruix/gcroots/
current-system -> /frx/store/...current...
rollback-system -> /frx/store/...rollback...
system-1 -> ...
system-2 -> ...
```
## Code changes
### `modules/fruix/system/freebsd/render.scm`
Added a generated in-guest Fruix deployment helper script under:
- `usr/local/bin/fruix`
That helper now:
- reports installed-system state with `fruix system status`
- stages a new current generation with `fruix system switch`
- stages the recorded rollback generation with `fruix system rollback`
- updates:
- `/var/lib/fruix/system/current`
- `/var/lib/fruix/system/current-generation`
- `/var/lib/fruix/system/rollback`
- `/var/lib/fruix/system/rollback-generation`
- `/frx/var/fruix/gcroots/current-system`
- `/frx/var/fruix/gcroots/rollback-system`
- `/frx/var/fruix/gcroots/system-N`
- refreshes the ESP bootloader file from the selected closure's `boot/loader.efi`
A practical implementation detail mattered here:
- replacing `/run/current-system` with a remove-then-recreate strategy caused the live shell environment to break while the link was absent
- switching that update to an atomic symlink replacement path for `/run/current-system` avoided that gap and made the in-guest operator command reliable
### `modules/fruix/system/freebsd/media.scm`
Updated installed rootfs staging so that installed targets expose:
- `/usr/local/bin/fruix -> /run/current-system/usr/local/bin/fruix`
Also bumped the explicit generation-layout version from:
- `1` to `2`
because the installed-system model now includes operator-driven switch/rollback state as part of the validated layout story.
### `modules/fruix/system/freebsd/model.scm`
Updated generated-file metadata so the system closure records:
- `usr/local/bin/fruix`
as part of the generated operating-system file set.
## New validation harness
Added:
- `tests/system/run-phase19-installed-system-rollback-qemu.sh`
This harness validates the actual installed-system operator flow on local `QEMU/UEFI/TCG`.
## Validation flow
The harness now performs all of the following:
1. installs a current system image directly to a target disk image
2. builds a distinct candidate closure
- in the validated harness this differs by host name so the closure identity changes cleanly without needing a heavier base-version rebuild
3. stages the candidate closure and its referenced store items into the installed target's `/frx/store`
4. boots the installed current system
5. validates initial state:
- current generation = `1`
- current closure = current installed closure
- no rollback generation yet recorded
6. runs:
- `fruix system switch /frx/store/...candidate...`
7. validates staged switch state:
- current generation = `2`
- rollback generation = `1`
- current closure = candidate closure
- rollback closure = original current closure
- generation 2 metadata/install files were written
8. reboots and validates boot into the candidate closure
9. runs:
- `fruix system rollback`
10. validates staged rollback state:
- current generation = `1`
- rollback generation = `2`
- current closure = original closure
- rollback closure = candidate closure
11. reboots and validates boot back into the original current system
12. confirms post-rollback service state:
- `fruix-shepherd` running
- `sshd` running
- activation log still shows success
## Passing validation
Passing result:
- `PASS phase19-installed-system-rollback-qemu`
Validated metadata summary:
```text
current_closure_path=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
candidate_closure_path=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
current_host_name=fruix-freebsd-current
candidate_host_name=fruix-freebsd-canary
final_current_generation=1
final_current_closure=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
final_rollback_generation=2
final_rollback_closure=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
installed_system_switch=ok
installed_system_rollback=ok
```
## Regression checks
After landing the installed-system switch/rollback workflow, the following regression checks still pass:
- `PASS phase19-generation-layout-qemu`
- `PASS phase18-installer-iso`
That means the new in-guest generation-management path did not regress:
- the previously validated explicit generation layout
- or the UEFI installer ISO boot/install path
## Relationship to Guix
This phase does **not** claim that Fruix now matches Guix's full installed-system UX.
What Fruix now has is:
- explicit generation state on disk
- explicit current/rollback pointers
- a minimal installed-system operator command surface
- validated switching and rollback between already-staged closures
What still remains compared with Guix:
- building/staging the candidate closure from inside the target system itself
- automatic closure transfer/fetch as part of `switch`
- a richer long-term generation lifecycle policy
## Conclusion
Phase 19.3 is complete.
Fruix now validates an actual installed-system rollback workflow on FreeBSD:
- the target system itself can report current/rollback state
- it can switch to a staged candidate generation
- it can reboot into that candidate generation
- it can roll back to the recorded prior generation
- and it can reboot into the restored current system
That closes the Phase 19 deployment story from:
- documented deployment workflow
- to explicit generation layout
- to validated installed-system operator rollback behavior

View File

@@ -0,0 +1,160 @@
# Phase 20.1: Fruix-managed development environment for native FreeBSD base work
Date: 2026-04-05
## Goal
Validate that a booted Fruix-managed FreeBSD system can expose a usable development environment for deeper native base work without collapsing the runtime/development boundary back into one broad profile.
This step explicitly builds on the Phase 14 split between:
- native runtime/boot artifacts for the running system
- separate development-facing artifacts such as headers and toolchain pieces
The goal is **not** full self-hosting yet.
It is to prove that a running Fruix system can expose the tools and paths needed for native FreeBSD build work in a controlled way.
## Implementation
### New operating-system field
`modules/fruix/system/freebsd/model.scm` now supports:
- `#:development-packages`
- `operating-system-development-packages`
The default remains empty, so existing systems do not change unless they opt in.
### Separate development profile inside the system closure
`modules/fruix/system/freebsd/media.scm` now materializes an additional profile tree when `development-packages` is non-empty:
- `/frx/store/...-fruix-system-.../development-profile`
The main runtime tree remains:
- `/frx/store/...-fruix-system-.../profile`
This preserves the runtime/development split:
- runtime stays under `profile`
- development tooling stays under `development-profile`
The development stores are also now part of the closure references and recorded in `metadata/store-layout.scm`.
### In-guest development environment helper
Opt-in systems with development packages now ship:
- `/usr/local/bin/fruix-development-environment`
and expose a stable runtime link:
- `/run/current-development -> /run/current-system/development-profile`
The helper emits shell exports for the active development profile, including at least:
- `FRUIX_DEVELOPMENT_PROFILE`
- `FRUIX_DEVELOPMENT_INCLUDE`
- `FRUIX_DEVELOPMENT_SHARE_MK`
- `FRUIX_CC`
- `FRUIX_CXX`
- `FRUIX_AR`
- `FRUIX_RANLIB`
- `FRUIX_NM`
- `FRUIX_BMAKE`
- `CPPFLAGS`
- `MAKEFLAGS`
- `PATH`
Intended use:
```sh
eval "$(/usr/local/bin/fruix-development-environment)"
```
### Chosen development overlay for this phase
For the validated Phase 20.1 guest path, the development overlay is intentionally narrow:
- `freebsd-native-headers`
- `freebsd-clang-toolchain`
This was chosen deliberately.
The earlier standalone Phase 14 development-profile package set was designed for broad profile composition, but for a running Fruix system it unnecessarily duplicated runtime pieces already present in the system profile.
For native base work, the important additions here are:
- `usr/include`
- `usr/share/mk`
- Clang/binutils-style frontend tools
while the running system already supplies:
- base runtime
- `/usr/bin/make`
- system libraries and boot/runtime state
That keeps the Phase 20.1 environment focused on native base development rather than reintroducing a broad mixed profile.
## New files
Added:
- `tests/system/phase20-development-operating-system.scm.in`
- `tests/system/run-phase20-development-environment-xcpng.sh`
## Validation
Passing run:
- `PASS phase20-development-environment-xcpng`
- workdir: `/tmp/fruix-phase20-development-xcpng`
Validated on the approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Representative result:
```text
closure_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd
development_profile_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd/development-profile
development_profile_guest=/run/current-system/development-profile
cc_version=FreeBSD clang version 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd708029e0b2)
hello_direct=hello-from-direct-dev-env
hello_make=hello-from-make-dev-env
development_environment=ok
```
The harness verified all of the following on the real booted Fruix guest:
- runtime boot/regression still succeeds on XCP-ng
- the closure contains a separate `development-profile`
- runtime `profile` does **not** regain headers or `usr/share/mk`
- `/usr/local/bin/fruix-development-environment` exists and emits the expected exports
- `/run/current-development` points at `/run/current-system/development-profile`
- direct compilation works with the exported Clang toolchain and headers
- a simple native FreeBSD `make` build using `.include <bsd.prog.mk>` also succeeds
## Result
Phase 20.1 is complete.
Fruix now validates a real booted FreeBSD system path where:
- the running system remains lean and runtime-focused
- native development artifacts are exposed separately and explicitly
- the guest can compile code directly with the Fruix-provided toolchain
- the guest can also drive a simple `bsd.prog.mk` build using the exported development environment
This is enough to say that Fruix can host a controlled native FreeBSD base-development environment, without yet claiming full self-hosting.
## Next step
Per `docs/PLAN_4.md`, the next planned step is:
- **Phase 20.2** — run host-initiated native base builds inside a Fruix-managed environment

View File

@@ -0,0 +1,169 @@
# Phase 20.2: host-initiated native base builds inside a Fruix-managed environment
Date: 2026-04-05
## Goal
Validate the next step after Phase 20.1:
- the host still orchestrates the outer loop
- the actual FreeBSD native base build work runs inside a booted Fruix-managed system
This is the intermediate path between:
- purely host-side native base builds
- any future claim of guest self-hosting
The target here was not a new self-hosted package manager story.
It was narrower:
- boot a Fruix-managed FreeBSD system on the approved real XCP-ng path
- expose the development environment required for native base work
- run real `buildworld` / `buildkernel` / staged install steps inside that Fruix guest
- confirm that the resulting staged artifacts are the expected FreeBSD base slices
## Implementation
### Canonical development-path compatibility links
Phase 20.1 proved that Fruix could keep development content separate in:
- `/run/current-system/development-profile`
- `/run/current-development`
Phase 20.2 exposed an additional practical requirement:
- FreeBSD native base builds still expect canonical system paths such as:
- `/usr/include`
- `/usr/share/mk`
For development-enabled systems, `populate-rootfs-from-closure` now also exposes:
- `/usr/include -> /run/current-system/development-profile/usr/include`
- `/usr/share/mk -> /run/current-system/development-profile/usr/share/mk`
This keeps the development profile separate while still satisfying the buildworld/buildkernel assumptions of the native FreeBSD build system.
### Media builder invalidation
Because this changed the visible rootfs layout of booted systems, the media builder versions were bumped in `modules/fruix/system/freebsd/media.scm`:
- `image-builder-version`
- `install-builder-version`
- `installer-image-builder-version`
- `installer-iso-builder-version`
That ensured booted images and future installed targets actually pick up the new compatibility links.
### New validation harness
Added:
- `tests/system/run-phase20-host-initiated-native-build-xcpng.sh`
This harness reuses the validated Phase 20.1 XCP-ng path first, then performs the 20.2-native-build step over SSH from the host.
The guest build flow is:
1. boot the development-enabled Fruix guest on XCP-ng
2. recover the materialized source store from `/run/current-system/metadata/store-layout.scm`
3. run real FreeBSD native build commands inside the guest:
- `make -j8 buildworld`
- `make -j8 buildkernel`
- `make DESTDIR=... installworld`
- `make DESTDIR=... distribution`
- `make DESTDIR=... installkernel`
4. stage narrower artifact slices from the staged output:
- headers slice
- bootloader slice
- kernel stage
### Why `DB_FROM_SRC=yes` is used for staged install steps
The development-enabled Fruix guest is intentionally lean and does not carry the full ambient host account database.
`installworld` on modern FreeBSD checks for required users/groups unless `DB_FROM_SRC` is defined. For staged installs into `DESTDIR`, the appropriate controlled input is the source tree's own account database under `etc/`, not the minimal running guest's `/etc/master.passwd`.
So the validated Phase 20.2 staged install path uses:
- `DB_FROM_SRC=yes`
for:
- `installworld`
- `distribution`
- `installkernel`
That keeps the staged install driven by the declared source input rather than by accidental guest-local account state.
## Validation
Passing run:
- `PASS phase20-host-initiated-native-build-xcpng`
- workdir: `/tmp/fruix-phase20-host-initiated-native-build-xcpng`
Validated on the approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Representative result:
```text
build_jobs=8
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
source_root=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default/tree
build_root=/var/tmp/fruix-phase20-native-build
world_stage=/var/tmp/fruix-phase20-native-build/stage-world
kernel_stage=/var/tmp/fruix-phase20-native-build/stage-kernel
headers_stage=/var/tmp/fruix-phase20-native-build/artifact-headers
bootloader_stage=/var/tmp/fruix-phase20-native-build/artifact-bootloader
build_root_size=7.6G
world_stage_size=672M
kernel_stage_size=739M
headers_stage_size=32M
bootloader_stage_size=1.3M
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
host_initiated_native_build=ok
```
The harness verified all of the following:
- the guest still boots and passes the Phase 20.1 development-environment checks first
- development-enabled systems expose canonical native-build compatibility links at:
- `/usr/include`
- `/usr/share/mk`
- the guest can recover the declared materialized FreeBSD source store from system metadata
- real FreeBSD `buildworld` succeeds inside the booted Fruix guest
- real FreeBSD `buildkernel` succeeds inside the booted Fruix guest
- staged `installworld`, `distribution`, and `installkernel` also succeed inside the guest
- the staged outputs contain the expected artifact shapes:
- `boot/kernel/kernel`
- `usr/include/sys/param.h`
- `usr/share/mk/bsd.prog.mk`
- `boot/loader.efi`
- `boot/defaults/loader.conf`
- `boot/lua/loader.lua`
## Result
Phase 20.2 is complete.
Fruix now validates a real host-orchestrated path where:
- the host boots and reaches a Fruix-managed development-enabled guest
- the guest uses its own Fruix-exposed development paths and declared source store
- the native FreeBSD base build work runs inside that Fruix-managed environment
- the host remains the outer orchestrator and result collector
This materially narrows the gap to any future self-hosting experiment while still avoiding the complexity jump to a full guest-driven package/deployment loop.
## Next step
Per `docs/PLAN_4.md`, the next planned step is:
- **Phase 20.3** — reassess and potentially prototype guest self-hosted base builds

View File

@@ -0,0 +1,202 @@
# Post-Phase 20: native build executor model
Date: 2026-04-06
## Goal
Turn the now-proven native base-build placement options into one Fruix abstraction instead of treating them as unrelated paths.
The desired model is:
- same declared source identity
- same expected artifact kinds
- same staged-result shape
- same promotion/provenance shape
- different executor policy
So the question becomes:
- where should the build run?
and the answer is expressed as executor policy rather than as a separate architecture each time.
## Executor model
Fruix now has an explicit native-build executor model with current executor kinds:
- `host`
- `ssh-guest`
- `self-hosted`
and intended future extension points:
- `jail`
- `remote-builder`
The new executor model is implemented in:
- `modules/fruix/system/freebsd/executor.scm`
It defines a structured executor object that records at least:
- `kind`
- `name`
- `version`
- `properties`
## What changed
### 1. Structured executor metadata
Native-build result metadata is no longer limited to a flat string such as:
- `guest-self-hosted`
Instead, result/promotion objects now carry a structured executor description.
Representative executor objects now look like:
```scheme
((kind . self-hosted)
(name . "guest-self-hosted")
(version . "4")
(properties . (...)))
```
or:
```scheme
((kind . ssh-guest)
(name . "ssh-guest")
(version . "1")
(properties . (...)))
```
Promoted store metadata also now records compatibility fields derived from that executor object:
- `executor-kind`
- `executor-name`
- `executor-version`
### 2. Shared staged-result shape across executors
Both validated executor paths now converge on the same mutable staging layout under:
- `/var/lib/fruix/native-builds/<run-id>`
with promoted artifacts staged under:
- `artifacts/world`
- `artifacts/kernel`
- `artifacts/headers`
- `artifacts/bootloader`
and promotion metadata recorded in:
- `promotion.scm`
The heavy build work remains executor-specific, but the result shape is now shared.
### 3. Shared immutable promotion path
Both validated executor paths now promote through the same command:
```sh
fruix native-build promote RESULT_ROOT
```
That promotion creates immutable store identities for:
- `world`
- `kernel`
- `headers`
- `bootloader`
- result bundle
under `/frx/store/...`.
## Validated executor policies
### `self-hosted`
The self-hosted guest helper now emits the structured executor metadata directly from:
- `/usr/local/bin/fruix-self-hosted-native-build`
Validated self-hosted promotion flow:
- `PASS phase20-self-hosted-native-build-xcpng`
- `PASS phase20-native-build-store-promotion-xcpng`
Representative promoted result:
```text
result_store=/frx/store/0423193b9bd5e652bdb9d94d077e40dfcc3e9e78-fruix-native-build-result-15.0-STABLE-guest-self-hosted
world_store=/frx/store/5f67e95058186147206ff6f5da2243a09212e358-fruix-native-world-15.0-STABLE-guest-self-hosted
kernel_store=/frx/store/3a32797cc187a90e8273f205eababae6246568d9-fruix-native-kernel-15.0-STABLE-guest-self-hosted
headers_store=/frx/store/1d54d9814461f003e91add8cd37e94ac5f3d04ce-fruix-native-headers-15.0-STABLE-guest-self-hosted
bootloader_store=/frx/store/49f4885f0f05a324ac826f2618d4c7a923ca30d2-fruix-native-bootloader-15.0-STABLE-guest-self-hosted
```
### `ssh-guest`
The host-initiated guest path now also stages a shared native-build result root under:
- `/var/lib/fruix/native-builds/<run-id>`
instead of stopping at temporary build directories alone.
Its promotion metadata records executor policy as:
- `kind = ssh-guest`
- `name = ssh-guest`
- `version = 1`
with executor properties including:
- `transport = ssh`
- `orchestrator = host`
- guest addressing / VM identity metadata
Validated host-initiated promotion flow:
- `PASS phase20-host-initiated-native-build-xcpng`
- `PASS phase20-host-initiated-native-build-store-promotion-xcpng`
Representative promoted result:
```text
result_store=/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest
world_store=/frx/store/89c7a71c3df148a1f99b13d57fd6be88243eb2cb-fruix-native-world-15.0-STABLE-ssh-guest
kernel_store=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest
headers_store=/frx/store/dd7f39f526bca4849caf1eaf96ae25d29b43493c-fruix-native-headers-15.0-STABLE-ssh-guest
bootloader_store=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest
```
## Important result
The same declared source and artifact contract now works across two different executor policies:
- `ssh-guest`
- `self-hosted`
while preserving the same Fruix-native staging/promotion split:
- mutable result roots under `/var/lib/fruix/native-builds/...`
- immutable promoted identities under `/frx/store/...`
That is the core architectural win of the executor model.
## What is not yet fully unified
The `host` executor kind now exists in the model, but the fully shared staged-result-plus-promotion workflow is not yet wired through the existing host-local native build path.
So the executor model is introduced and real-VM validated across two policies, but not yet uniformly productized across every native-build entry point.
## Result
Fruix now treats native-build placement as executor policy rather than as an architectural fork.
That means the next step is no longer to invent another build path from scratch.
It is to keep extending the same executor/result/promotion model so additional execution policies can plug into the same Fruix-native object story.

View File

@@ -0,0 +1,190 @@
# Post-Phase 20: native build result promotion into first-class Fruix store objects
Date: 2026-04-05
## Goal
Make native FreeBSD base-build results feel like real Fruix objects instead of stopping at mutable staged files under:
- `/var/lib/fruix/native-builds/...`
The desired model is:
- `/var/lib/fruix/native-builds/...` remains a staging/result area
- `/frx/store/...` remains the real immutable identity
Validated artifact identities:
- `world`
- `kernel`
- `headers`
- `bootloader`
## What changed
### Promotion metadata in guest result roots
The guest self-hosted helper now emits a promotion description file at:
- `/var/lib/fruix/native-builds/<run-id>/promotion.scm`
That metadata records at least:
- executor / executor-version
- run-id / guest-host-name
- closure path
- development profile path
- declared FreeBSD base metadata
- source store provenance
- build policy
- artifact entries for:
- `world`
- `kernel`
- `headers`
- `bootloader`
The helper also stages a promotable `world` artifact tree in addition to the already validated narrower artifacts.
### Host-side promotion API
Fruix now exports:
- `promote-native-build-result`
and the CLI now exposes:
- `fruix native-build promote RESULT_ROOT [--store DIR]`
### Promoted store object layout
Promotion now creates immutable store objects for:
- `/frx/store/...-fruix-native-world-...`
- `/frx/store/...-fruix-native-kernel-...`
- `/frx/store/...-fruix-native-headers-...`
- `/frx/store/...-fruix-native-bootloader-...`
Each promoted artifact store records:
- `.fruix-native-build-object.scm`
- `.references`
Promotion also creates a result-bundle store object:
- `/frx/store/...-fruix-native-build-result-...`
That bundle records:
- `.fruix-native-build-result.scm`
- `artifacts/world`
- `artifacts/kernel`
- `artifacts/headers`
- `artifacts/bootloader`
where the `artifacts/*` entries are symlinks to the promoted artifact stores.
### Identity policy
Artifact identity is now based on Fruix metadata plus a tree-content signature of the staged artifact tree.
That means promotion identity depends on both:
- the explicit Fruix-native build metadata
- the actual content of the promoted artifact tree
## Validation harness
Added:
- `tests/system/run-phase20-native-build-store-promotion-xcpng.sh`
This harness:
1. boots the approved real XCP-ng guest path
2. runs the validated in-guest self-hosted native build helper
3. imports the guest result root back to the host
4. runs `fruix native-build promote`
5. verifies promoted store paths, metadata, symlink structure, and representative hashes
## Validation
Passing run:
- `PASS phase20-native-build-store-promotion-xcpng`
- workdir: `/tmp/current-phase20-native-build-store-promotion-xcpng`
Approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Representative metadata:
```text
run_id=20260405T213444Z
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
guest_result_root=/var/lib/fruix/native-builds/20260405T213444Z
result_store=/frx/store/c6329a0053720b05aff3274b8b1d522c909f475d-fruix-native-build-result-15.0-STABLE-guest-self-hosted
world_store=/frx/store/dfe37b36f6537a95ceea16ea62001b2ca5617eb7-fruix-native-world-15.0-STABLE-guest-self-hosted
kernel_store=/frx/store/0ab7cbceca240ab2c3b91e83e059844ea792e49e-fruix-native-kernel-15.0-STABLE-guest-self-hosted
headers_store=/frx/store/5bbeae9266687a229f1c6d176a08886c35243ff0-fruix-native-headers-15.0-STABLE-guest-self-hosted
bootloader_store=/frx/store/bd49a508bd7a3b94a2535d6774f31c993c406552-fruix-native-bootloader-15.0-STABLE-guest-self-hosted
artifact_store_count=4
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
promoted_kernel_sha=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
promoted_loader_sha=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
promoted_param_sha=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
native_build_store_promotion=ok
```
Validated facts:
- guest self-hosted native-build results remain available under `/var/lib/fruix/native-builds/...` as mutable staging/results
- those staged results are now sufficient to promote first-class Fruix identities on the host
- promotion creates four immutable artifact store objects plus one immutable result-bundle store object
- promoted metadata retains executor/source/build-policy/provenance information explicitly
- promoted kernel, bootloader, and headers hashes match the already validated staged artifacts
- the promoted `world` artifact is now also preserved as a first-class Fruix store object instead of remaining only as an unpromoted staged tree
## Important implementation note
The first full promotion attempt exposed an incorrect assumption in the validation surface:
- the promoted/staged `world` artifact should be checked at:
- `bin/sh`
- not at:
- `usr/bin/sh`
That path expectation was corrected in:
- helper validation
- emitted promotion metadata
- the end-to-end promotion harness
## Result
Fruix now has a validated native-build object model with a clear split:
- mutable native-build result roots under `/var/lib/fruix/native-builds/...`
- immutable promoted identities under `/frx/store/...`
That makes native FreeBSD base builds feel substantially more Fruix-native:
- build outputs have explicit immutable identities
- metadata is Fruix-native rather than implied only by ad hoc directory layout
- executor/source/provenance/build-policy remain attached to the promoted result
- the staged result area and the real store identity are now intentionally distinct
## Next direction
This suggests the next product step is not merely “more self-hosting”.
It is to generalize this result model so different execution modes can converge on the same promoted object story, for example:
- host-initiated in-guest builds
- guest self-hosted builds
- future executor variants
That would move Fruix closer to a shared executor model rather than treating each validation path as a one-off harness.

View File

@@ -0,0 +1,201 @@
# Phase 20.3: controlled guest self-hosted native base-build prototype
Date: 2026-04-05
## Goal
Reassess guest self-hosting now that Fruix has already completed the earlier source, installation, generation-layout, rollback, development-overlay, and host-initiated in-guest native-build steps.
Phase 20.3 asked for real evidence about:
- what self-hosting would improve
- what it would cost in complexity
- how it fits with the Fruix source/deployment model already in place
## What changed
### New in-guest helper
Development-enabled systems now also ship:
- `/usr/local/bin/fruix-self-hosted-native-build`
This helper performs a controlled in-guest native FreeBSD base build using the system's own declared materialized source store recorded in:
- `/run/current-system/metadata/store-layout.scm`
The helper:
1. verifies the development overlay is present
2. verifies the canonical compatibility links exist:
- `/usr/include`
- `/usr/share/mk`
3. recovers the materialized FreeBSD source store from current-system metadata
4. runs:
- `buildworld`
- `buildkernel`
- `installworld`
- `distribution`
- `installkernel`
5. stages narrower artifact outputs under:
- `/var/lib/fruix/native-builds/<run-id>/artifacts/`
6. records metadata and status under:
- `/var/lib/fruix/native-builds/<run-id>/`
- `/var/lib/fruix/native-builds/latest`
The heavy object/stage work stays under:
- `/var/tmp/fruix-self-hosted-native-builds/<run-id>`
so the installed-system result area remains smaller and more legible.
### Important environment fix discovered during prototyping
The first prototype attempt failed even though Phase 20.2 had already succeeded.
Cause:
- directly evaluating `fruix-development-environment` before `buildworld` exported development-oriented variables like:
- `MAKEFLAGS`
- `CPPFLAGS`
- `CFLAGS`
- `CXXFLAGS`
- `LDFLAGS`
- those are appropriate for smaller development builds, but they polluted FreeBSD's world/kernel bootstrap environment and broke the LLVM bootstrap phase
Representative failure:
- missing generated LLVM config headers during bootstrap (`llvm/Config/abi-breaking.h`)
The validated fix was to make the self-hosted helper explicitly sanitize that environment first:
- reset `PATH` to the normal base paths
- unset development-shell variables such as:
- `MAKEFLAGS`
- `CC`, `CXX`, `AR`, `RANLIB`, `NM`
- `CPPFLAGS`, `CFLAGS`, `CXXFLAGS`, `LDFLAGS`
- `FRUIX_DEVELOPMENT_*`
- `FRUIX_*` tool variables
So the final 20.3 result is not “just reuse the development shell wholesale”.
It is more precise:
- use the development overlay for canonical paths and available content
- but run the real base-build steps in a cleaner, purpose-built helper environment
### Closure invalidation
To ensure the updated helper actually affects generated system closures, the operating-system closure spec now also records helper-version markers for development-enabled systems.
That ensures guest images pick up helper changes instead of silently reusing an older cached closure path.
### Validation harness
Added:
- `tests/system/run-phase20-self-hosted-native-build-xcpng.sh`
This harness:
1. boots the validated development-enabled Fruix guest on the approved XCP-ng path
2. verifies the new helper exists in the guest
3. invokes the helper from inside the guest
4. verifies the recorded result/status/`latest` pointer
5. validates the resulting staged artifact metadata and hashes
## Validation
Passing run:
- `PASS phase20-self-hosted-native-build-xcpng`
- workdir: `/tmp/fruix-phase20-self-hosted-native-build-xcpng`
Validated on the approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Representative metadata:
```text
run_id=20260405T150359Z
helper_version=2
build_jobs=8
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
build_root=/var/tmp/fruix-self-hosted-native-builds/20260405T150359Z
result_root=/var/lib/fruix/native-builds/20260405T150359Z
latest_link=/var/lib/fruix/native-builds/latest
latest_target=/var/lib/fruix/native-builds/20260405T150359Z
status_value=ok
build_root_size=7.5G
result_root_size=343M
kernel_artifact_size=158M
headers_artifact_size=32M
bootloader_artifact_size=1.3M
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
self_hosted_native_build=ok
```
Validated facts:
- the development-enabled Fruix guest can now run a controlled self-hosted native base-build helper from inside the installed system itself
- the helper can recover the declared source store from current-system metadata without host-side parsing
- `buildworld` and `buildkernel` succeed in the guest
- staged `installworld`, `distribution`, and `installkernel` succeed in the guest
- the helper records a stable result directory and `latest` pointer under:
- `/var/lib/fruix/native-builds`
- the resulting artifact hashes match the earlier validated Phase 20.2 host-initiated in-guest path
## What self-hosting improved
The prototype demonstrates a few real improvements:
- the build recipe itself now lives inside the Fruix-managed system, not only in a host-side SSH harness
- the guest can derive its own declared source input from current-system metadata
- result/state recording now has a Fruix-native installed-system location:
- `/var/lib/fruix/native-builds`
- the host no longer needs to spell out every `make` phase just to validate the in-guest path
## What it cost in complexity
The prototype also made the extra complexity visible:
- the guest helper needs its own controlled environment contract
- a naive reuse of the development-shell exports was wrong for real `buildworld`
- helper-version invalidation had to be made explicit so closure caching would not hide helper changes
- the in-guest result/staging model now needs its own operator-facing conventions
So the experiment did not eliminate complexity.
It mostly moved some of it from the host harness into an explicit in-guest helper contract.
## Decision after the prototype
Phase 20.3 is complete because Fruix now has a **first controlled guest self-hosted native base-build prototype**.
However, the evidence does **not** suggest replacing the Phase 20.2 path as the default operator workflow yet.
The current recommendation is:
- keep the **host-initiated in-guest native-build path** as the simpler default validation and orchestration flow
- keep the new **self-hosted helper** as a controlled prototype and stepping stone toward deeper guest-driven workflows
That fits the existing Fruix model well:
- source identity still comes from declared store-backed metadata
- deployment identity still comes from immutable closures under `/frx/store`
- the guest-side prototype adds a narrower in-system build/result workflow without replacing the existing deployment story
## Result
Phase 20.3 is complete.
Fruix now has:
- a validated host-orchestrated in-guest native base-build workflow
- and a validated first controlled guest self-hosted native base-build prototype
That answers the Phase 20.3 question with real evidence instead of only prior caution.

View File

@@ -0,0 +1,86 @@
# Phase 7.1: Minimal Fruix operating-system model for FreeBSD
Date: 2026-04-01
## Summary
This step introduces the first declarative Fruix operating-system model for the FreeBSD track. The goal is not full upstream Guix System integration yet, but a coherent Fruix-owned system description that can drive later closure and rootfs generation work.
Added files:
- `modules/fruix/system/freebsd.scm`
- `tests/system/phase7-minimal-operating-system.scm`
- `tests/system/validate-phase7-operating-system.scm`
- `tests/system/run-phase7-operating-system-model.sh`
Updated file:
- `modules/fruix/packages/freebsd.scm`
## Validation command
Run command:
```sh
METADATA_OUT=/tmp/phase7-os-model-metadata.txt \
./tests/system/run-phase7-operating-system-model.sh
```
## What the model describes
The new module defines records and constructors for:
- users
- groups
- file systems
- a FreeBSD-oriented operating-system object
The minimal system description currently covers:
- host identity
- kernel package
- bootloader package
- base packages
- users/groups
- file-system declarations
- generated `/etc` files
- a generated activation script
- a generated Shepherd configuration payload
- a declared init mode of:
- `freebsd-init+rc.d-shepherd`
## Observed results
Observed metadata included:
- `host_name=fruix-freebsd`
- `kernel_package=freebsd-kernel`
- `bootloader_package=freebsd-bootloader`
- `base_packages=freebsd-runtime,freebsd-userland,freebsd-libc,freebsd-rc-scripts,freebsd-sh,freebsd-bash`
- `users=root,operator`
- `groups=wheel,operator`
- `file_system_count=3`
- `services=shepherd,ready-marker`
- `generated_files=boot/loader.conf,etc/rc.conf,etc/fstab,etc/hosts,etc/passwd,etc/group,etc/shells,etc/motd,activate,shepherd/init.scm`
- `init_mode=freebsd-init+rc.d-shepherd`
- `ready_marker=/var/lib/fruix/ready`
## Important findings
- the FreeBSD track now has a concrete declarative system object rather than only ad hoc package lists and shell prototypes
- the model is explicitly Fruix-owned and FreeBSD-oriented; it does not attempt a premature blanket rename or premature full Guix System integration
- the package layer was extended with additional system-oriented prototype packages needed by later Phase 7 work, including:
- `freebsd-bootloader`
- `freebsd-rc-scripts`
- `freebsd-runtime`
- `%freebsd-system-packages`
- the chosen first init strategy is now explicit in the model:
- FreeBSD init with declaratively generated `rc.d` launch of Shepherd
## Conclusion
Phase 7.1 is satisfied on the current FreeBSD prototype track:
- a minimal Fruix operating-system model now exists for FreeBSD
- that model evaluates into a coherent system-closure specification
- the next step is to materialize that specification into a real system closure under `/frx/store`

View File

@@ -0,0 +1,78 @@
# Phase 7.3: Installable FreeBSD rootfs tree materialized from the Fruix system closure
Date: 2026-04-01
## Summary
This step takes the Phase 7.2 system closure and materializes a root filesystem tree suitable for later image-construction work.
Added files:
- `tests/system/materialize-phase7-rootfs.scm`
- `tests/system/run-phase7-rootfs.sh`
## Validation command
Run command:
```sh
METADATA_OUT=/tmp/phase7-rootfs-metadata.txt \
./tests/system/run-phase7-rootfs.sh
```
## What the harness does
The harness:
1. ensures the local Guile/Fibers/Shepherd runtime is available
2. reuses the declarative Phase 7 operating-system model
3. materializes the referenced system closure under `/frx/store`
4. creates a root filesystem tree that points at that closure through:
- `/run/current-system`
- boot symlinks
- `/bin`, `/sbin`, `/lib`, and `/usr/*` links into the system profile
- generated `/etc` links into the system closure
- generated Shepherd `rc.d` launch integration
5. performs static validation of the generated rootfs structure
## Observed results
Observed metadata included:
- `rootfs=/tmp/.../rootfs`
- `closure_path=/frx/store/...-fruix-system-fruix-freebsd`
- `run_current_system_target=/frx/store/...-fruix-system-fruix-freebsd`
- `activate_target=/run/current-system/activate`
- `bin_target=/run/current-system/profile/bin`
- `sbin_target=/run/current-system/profile/sbin`
- `boot_kernel_target=/run/current-system/boot/kernel`
- `boot_loader_target=/run/current-system/boot/loader`
- `boot_loader_efi_target=/run/current-system/boot/loader.efi`
- `rc_conf_target=/run/current-system/etc/rc.conf`
- `rc_script_target=/run/current-system/usr/local/etc/rc.d/fruix-shepherd`
- `ready_marker=/var/lib/fruix/ready`
- `validation_mode=static-rootfs-check`
- `ready_state_mode=freebsd-init+rc.d-shepherd`
## Important findings
- the current FreeBSD Fruix track now has a concrete rootfs tree derived from the declarative system model and closure rather than only a closure directory in the store
- the rootfs uses a Guix-like `/run/current-system` anchor so that generated configuration and system profile content remain tied back to the declarative closure
- static validation confirmed:
- boot asset linkage
- generated `/etc` linkage
- activation payload presence
- Shepherd launch integration
- declared filesystem content
- declared user/group provisioning in the activation path
- the deterministic ready-marker path for the first boot target
- the chosen ready state for the first integrated FreeBSD system remains:
- FreeBSD init + `rc.d` + Shepherd-managed ready marker
## Conclusion
Phase 7.3 is satisfied on the current FreeBSD prototype track:
- a root filesystem tree can now be materialized from the declarative Fruix system closure
- the rootfs is internally coherent enough for the next image-construction phase
- Phase 7 as a whole is now complete on the active FreeBSD amd64 prototype path

View File

@@ -0,0 +1,74 @@
# Phase 7.2: Minimal FreeBSD system closure generated from the Fruix system model
Date: 2026-04-01
## Summary
This step materializes the Phase 7.1 Fruix operating-system description into a reproducible system closure under `/frx/store`.
Added files:
- `tests/system/materialize-phase7-system-closure.scm`
- `tests/system/run-phase7-system-closure.sh`
## Validation command
Run command:
```sh
METADATA_OUT=/tmp/phase7-system-closure-metadata.txt \
./tests/system/run-phase7-system-closure.sh
```
## What the harness does
The harness:
1. ensures the local Guile/Fibers/Shepherd runtime needed for the generated system is available
2. loads the declarative Phase 7 operating-system object
3. materializes the referenced FreeBSD base packages into `/frx/store`
4. materializes local Guile and Shepherd prefixes into `/frx/store`
5. generates a Fruix system closure store item containing:
- boot assets
- a merged system profile
- generated `/etc` files
- an activation script
- a Shepherd configuration
- a generated FreeBSD `rc.d` launcher for Shepherd
6. reruns the same materialization a second time and verifies that the same closure path is produced
## Observed results
Observed metadata included:
- `closure_path=/frx/store/...-fruix-system-fruix-freebsd`
- `closure_rebuild_path=/frx/store/...-fruix-system-fruix-freebsd`
- `kernel_store=/frx/store/...-freebsd-kernel-15.0-STABLE`
- `bootloader_store=/frx/store/...-freebsd-bootloader-15.0-STABLE`
- `guile_store=/frx/store/...-fruix-guile-runtime-3.0`
- `guile_extra_store=/frx/store/...-fruix-guile-extra-3.0`
- `shepherd_store=/frx/store/...-fruix-shepherd-runtime-1.0.9`
- `profile_bin_sh=/frx/store/...-fruix-system-fruix-freebsd/profile/bin/sh`
- `profile_sbin_init=/frx/store/...-fruix-system-fruix-freebsd/profile/sbin/init`
- `profile_rc=/frx/store/...-fruix-system-fruix-freebsd/profile/etc/rc`
- `init_integration=freebsd-init+rc.d-shepherd`
## Important findings
- the FreeBSD system definition now materializes into a single reproducible system closure store item rather than remaining only a logical specification
- the closure uses a Guix-like split between:
- referenced store items for package/runtime content
- generated configuration and activation payload in the system closure itself
- the first integrated init strategy is now concretely encoded in the closure contents:
- FreeBSD init and rc infrastructure from the base system profile
- a generated `fruix_shepherd` rc script
- a generated Shepherd configuration that writes a deterministic ready marker
- rerunning the same materialization produced the same closure path, which is the current prototype proof of reproducible closure generation for this phase
## Conclusion
Phase 7.2 is satisfied on the current FreeBSD prototype track:
- a declarative Fruix system description now generates a real system closure under `/frx/store`
- that closure contains boot files, `/etc` payloads, activation payload, and Shepherd launch integration
- the next step is to materialize a root filesystem tree from this closure and statically validate it for later image construction work

View File

@@ -0,0 +1,84 @@
# Phase 8.1: Reproducible bhyve-compatible disk image generation on FreeBSD
Date: 2026-04-01
## Summary
This step adds the first reproducible raw disk-image build path for the FreeBSD Fruix prototype. The target format is intentionally simple and debuggable:
- raw disk image
- GPT partition map
- UEFI boot
- FreeBSD UFS root partition
- serial-console-friendly loader configuration
Added file:
- `tests/system/run-phase8-bhyve-image.sh`
## Validation command
Run command:
```sh
METADATA_OUT=/tmp/phase8-bhyve-image-metadata.txt \
./tests/system/run-phase8-bhyve-image.sh
```
## What the harness does
The harness:
1. reuses the Phase 7 rootfs materialization path
2. discovers the generated system closure under `/frx/store`
3. creates an image-oriented staging rootfs
4. copies the closure and its recursively declared store references into:
- `rootfs/frx/store`
5. builds:
- a FAT EFI system partition image containing `EFI/BOOT/BOOTX64.EFI`
- a UFS root partition image from the staged rootfs
- a final GPT raw disk image with labeled partitions
6. rebuilds the same image a second time with fixed timestamps and filesystem parameters
7. verifies that both resulting raw images have the same SHA256
8. attaches the raw image through `mdconfig` and validates its partition/filesystem structure
## Observed results
Observed metadata included:
- `raw_sha256=08605d738021cb6fb5b87c270e1eafde57e1acb5159d3a2257aad4c560e2efc5`
- `image_size_bytes=335578624`
- `esp_fstype=msdosfs`
- `root_fstype=ufs`
- `run_current_system_target=/frx/store/...-fruix-system-fruix-freebsd`
- `boot_loader_target=/run/current-system/boot/loader`
- `boot_loader_conf_target=/run/current-system/boot/loader.conf`
- `rc_conf_target=/run/current-system/etc/rc.conf`
- `rc_script_target=/run/current-system/usr/local/etc/rc.d/fruix-shepherd`
- `store_item_count=13`
- `boot_mode=uefi`
- `image_format=raw`
- `partition_scheme=gpt`
- `root_partition_label=fruix-root`
- `efi_partition_label=efiboot`
- `serial_console=comconsole`
## Important findings
- the current Fruix FreeBSD track now has a reproducible raw disk-image build path suitable for later bhyve work
- the earlier Phase 7 rootfs tree was not sufficient by itself for an installable image because it referenced `/frx/store` content that was still only present on the host; the image builder therefore had to stage the closure and its store references inside the image rootfs under `/frx/store`
- fixed timestamps and explicit filesystem parameters were sufficient to make repeated image builds byte-for-byte reproducible on this host
- boot-structure sanity checks succeeded for:
- GPT partitioning
- EFI partition population
- UFS root partition creation
- serial-console loader configuration
- preserved `run/current-system` system-link topology
## Conclusion
Phase 8.1 is satisfied on the current FreeBSD prototype track:
- a reproducible bhyve-compatible raw disk image can now be generated from the Fruix system outputs
- the resulting image passes static boot-structure sanity checks
- the next step is to move this image builder into the Fruix system-composition layer so image generation becomes an output of the declarative system description itself

View File

@@ -0,0 +1,82 @@
# Phase 8.2: Image generation integrated with the Fruix system definition layer
Date: 2026-04-01
## Summary
This step moves bhyve-image generation out of a detached shell-only path and into the declarative Fruix FreeBSD system-composition module.
Added files:
- `tests/system/materialize-phase8-system-image.scm`
- `tests/system/run-phase8-system-image.sh`
Updated file:
- `modules/fruix/system/freebsd.scm`
## Validation command
Run command:
```sh
METADATA_OUT=/tmp/phase8-system-image-metadata.txt \
./tests/system/run-phase8-system-image.sh
```
## What changed in the system layer
The FreeBSD system module now exports image-oriented operations including:
- `operating-system-image-spec`
- `materialize-bhyve-image`
The integrated image path now:
1. starts from a declarative Fruix operating-system object
2. materializes the system closure under `/frx/store`
3. materializes a rootfs from that closure
4. stages the closure and its reference closure into `rootfs/frx/store`
5. builds:
- `esp.img`
- `root.ufs`
- `disk.img`
6. stores the resulting image artifact as a content-addressed store item under `/frx/store`
## Observed results
Observed metadata included:
- `image_store_path=/frx/store/...-fruix-bhyve-image-fruix-freebsd`
- `disk_image=/frx/store/...-fruix-bhyve-image-fruix-freebsd/disk.img`
- `closure_path=/frx/store/...-fruix-system-fruix-freebsd`
- `raw_sha256=ac57d4c694ea3cf6b1bd24be48982090a6cfcfa301d052c1f903636a46f2d56e`
- `image_size_bytes=335578624`
- `store_item_count=13`
- `esp_fstype=msdosfs`
- `root_fstype=ufs`
- `run_current_system_target=/frx/store/...-fruix-system-fruix-freebsd`
- `boot_loader_target=/run/current-system/boot/loader`
- `rc_conf_target=/run/current-system/etc/rc.conf`
- `rc_script_target=/run/current-system/usr/local/etc/rc.d/fruix-shepherd`
- `image_generation_mode=declarative-system-layer`
## Important findings
- image generation is now a direct output of the Fruix FreeBSD system-definition layer rather than an external follow-up script around Phase 7 artifacts
- the resulting image artifact is itself stored under `/frx/store`, preserving the projects store-centered composition story as the work moves from closures to VM images
- rerunning `materialize-bhyve-image` for the same operating-system description produced the same image store path, which is the current prototype proof that the declarative system object can drive image generation end-to-end
- the integrated image still passes the same static boot-structure checks used in Phase 8.1:
- GPT layout
- EFI partition contents
- UFS root partition
- serial-console loader configuration
- `run/current-system` topology
## Conclusion
Phase 8.2 is satisfied on the current FreeBSD prototype track:
- a single declarative Fruix operating-system description can now drive image generation end-to-end
- the result is a bhyve-oriented raw image artifact stored under `/frx/store`
- Phase 8 as a whole is now complete on the active FreeBSD amd64 prototype path

View File

@@ -0,0 +1,227 @@
# Phase 9 completion: Fruix FreeBSD reached the ready marker on XCP-ng
Date: 2026-04-02
## Goal
Complete the first minimal Phase 9 boot milestone on the active FreeBSD track.
Because local bhyve is unavailable in this Xen environment, the active validation target remained the operator-approved XCP-ng VM and its existing VDI:
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI: `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
The required Phase 9 outcomes for this completion step were:
- boot the generated Fruix image on the real VM,
- reach the generated ready marker,
- keep Shepherd running in the guest,
- keep SSH available for operator access,
- and validate the declared system closure from inside the guest.
## Result
This phase now succeeds on the active XCP-ng path.
`tests/system/run-phase9-xcpng-boot.sh` now passes end-to-end and verifies:
- boot on the real XCP-ng VM,
- DHCP on the guest NIC,
- root SSH access via the injected key,
- `/run/current-system` pointing at the generated Fruix closure under `/frx/store`,
- the ready marker at `/var/lib/fruix/ready`,
- `fruix-shepherd` running,
- `sshd` running,
- and a minimal operator-facing home directory for the declared `operator` account.
Successful run metadata:
- workdir: `/tmp/phase9-xcpng-pass-1775113189`
- guest IP: `192.168.213.62`
- closure path:
- `/frx/store/0fe459ea22156510e64cea794b7a001151b59625bd5f12a488d6851e1c6d2198-fruix-system-fruix-freebsd`
- image path:
- `/frx/store/73f5757f8b58cf15fd97fc9a9704664d4b1d390d547fffff68c129a85d6cc368-fruix-bhyve-image-fruix-freebsd/disk.img`
Representative successful metadata values from the passing XCP-ng run:
```text
ready_marker=ready
run_current_system_target=/frx/store/0fe459ea22156510e64cea794b7a001151b59625bd5f12a488d6851e1c6d2198-fruix-system-fruix-freebsd
shepherd_status=running
sshd_status=running
operator_home_listing=/home/operator
uname_output=FreeBSD 15.0-STABLE
logger_log=fruix-shepherd-started
```
## Root causes resolved
The remaining Phase 9 blocker turned out not to be a single Guile bug, but a chain of runtime integration gaps.
### 1. Guile needed a usable UTF-8 locale in the guest
With the minimal image as originally staged, guest Guile started in a plain `C` locale and crashed very early in locale-string conversion paths.
The fix for the current prototype track was to stage the minimal locale data needed for `C.UTF-8`:
- `/usr/share/locale/C.UTF-8/LC_CTYPE`
and to start Shepherd with:
- `LANG=C.UTF-8`
- `LC_ALL=C.UTF-8`
### 2. The copied Guile / Shepherd runtimes still contained baked-in source-prefix paths
The locally built Guile, guile-gnutls/fibers, and Shepherd artifacts were originally installed under temporary validation prefixes such as:
- `/tmp/guile-freebsd-validate-install`
- `/tmp/guile-gnutls-freebsd-validate-install`
- `/tmp/shepherd-freebsd-validate-install`
Even after those trees were copied into `/frx/store`, a number of runtime references still pointed back to the original prefixes:
- Guile system load paths compiled into `libguile`
- Shepherd launcher scripts
- Fibers and GnuTLS Scheme modules
- Shepherd configuration module paths
The immediate prototype fix was to make activation recreate compatibility symlinks at those original prefixes, but pointing at the actual store items in the guest.
This keeps the running system store-backed while unblocking the existing locally built Guile/Shepherd artifacts.
### 3. The Shepherd process needed to be detached correctly from rc startup
Starting Shepherd with a simple shell background `&` was not sufficient on the real boot path. The process could exit when the invoking shell/session disappeared, which made the ready marker appear transiently while Shepherd itself did not remain up.
The fix was to launch Shepherd through FreeBSD `daemon(8)`:
- `/usr/sbin/daemon -c -f -p "$pidfile" -o /var/log/shepherd-bootstrap.out ...`
This gave the guest a stable long-lived Shepherd daemon process and made `onestatus`/socket checks reliable.
### 4. The initial Shepherd config used helper APIs that were not actually present in the guest runtime
The generated Shepherd config originally used:
- `mkdir-p`
- `call-with-output-file` with `#:append`
Those choices were too optimistic for the minimal Scheme environment being staged.
The fix was to replace them with simpler portable logic:
- a local recursive directory-creation helper based on `mkdir`
- explicit append-mode logging via `open-file "a"`
### 5. The XCP-ng harness itself had an SSH-key bug
The first end-to-end rerun of `tests/system/run-phase9-xcpng-boot.sh` failed because it used the public key file as the SSH identity file.
The harness now distinguishes:
- `ROOT_AUTHORIZED_KEY_FILE` for guest key injection
- `ROOT_SSH_PRIVATE_KEY_FILE` for the host-side SSH login
with the private key defaulting to:
- `~/.ssh/id_ed25519`
## Code-level changes that closed the blocker
### `modules/fruix/packages/freebsd.scm`
Extended the minimal runtime again to support the final ready-state path:
- staged `/usr/sbin/daemon`
- staged `/usr/share/locale/C.UTF-8/LC_CTYPE`
### `modules/fruix/system/freebsd.scm`
Completed the Guile/Shepherd guest runtime integration by:
- generating activation that recreates compatibility symlinks from the historical build prefixes to the real `/frx/store` items
- exporting locale and Guile runtime path variables in the Shepherd rc script:
- `LANG`
- `LC_ALL`
- `GUILE_SYSTEM_PATH`
- `GUILE_SYSTEM_COMPILED_PATH`
- `GUILE_SYSTEM_EXTENSIONS_PATH`
- the existing site path variables
- starting Shepherd through `daemon(8)` instead of a fragile shell background job
- fixing the generated Shepherd config so the logger and ready-marker services work in the guest
- exposing the staged locale data into the rootfs via `/usr/share/locale`
### `tests/system/run-phase9-xcpng-boot.sh`
Improved the real XCP-ng harness by:
- separating the injected public key from the SSH private key actually used for login
- preserving the successful passing metadata for the full ready-marker path
## Validation details
### Local reproduction and verification
Before re-testing on XCP-ng, the failure was reproduced and fixed in two faster environments:
1. a host-side chroot into the generated image root partition
2. local QEMU/TCG boots with UEFI and SSH forwarding
That produced a much tighter debug loop for:
- locale staging,
- baked-prefix compatibility,
- and Shepherd daemon lifetime.
### Real XCP-ng validation
The final proof remained the real VM.
The passing XCP-ng run verified all of the following from the booted guest over SSH:
- `cat /var/lib/fruix/ready` returns `ready`
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` succeeds
- `service sshd onestatus` succeeds
- `readlink /run/current-system` matches the generated Fruix closure
- `/home/operator` exists
## Assessment against Phase 9 goals
### 9.1 deterministic ready state
Satisfied on the active XCP-ng track.
The guest now boots to a deterministic ready marker:
- `/var/lib/fruix/ready`
### 9.2 in-guest Shepherd and core-service validation
Satisfied on the active XCP-ng track.
The guest now validates:
- Shepherd active
- generated configuration in effect
- system closure mounted through `/run/current-system`
- `sshd` available for remote operator access
### 9.3 minimal operator usability
Satisfied on the active XCP-ng track.
A human operator can now:
- discover the DHCP address,
- log in over SSH with the injected root key,
- inspect `/run/current-system`,
- inspect the ready marker,
- and inspect Shepherd/log state in the guest.
## Conclusion
Phase 9 is complete for the current FreeBSD prototype track, using the active XCP-ng replacement path in place of unavailable local bhyve.
The Fruix image now boots as a real FreeBSD VM, reaches the generated ready state, runs Shepherd successfully, and supports a minimal operator workflow over SSH.

View File

@@ -0,0 +1,213 @@
# Phase 9 checkpoint: XCP-ng boot reached DHCP and SSH on FreeBSD
Date: 2026-04-02
## Goal
Advance Phase 9 from a static image-generation milestone to a real booted Fruix guest on the active FreeBSD/XCP-ng track, using the operator-approved VM:
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- existing target VDI: `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
The immediate objective for this checkpoint was narrower than full Phase 9 completion:
- boot the generated image under XCP-ng,
- obtain DHCP,
- and reach SSH access with the injected root key.
## Summary
This checkpoint succeeded.
The current Fruix FreeBSD image now:
- boots on the target XCP-ng VM,
- mounts the generated root filesystem,
- completes enough of FreeBSD `rc` startup to configure networking,
- obtains a DHCP lease on the Xen NIC,
- starts `sshd`,
- and accepts root public-key authentication over the network.
Validated guest details from the successful XCP-ng boot:
- guest IP: `192.168.213.62`
- hostname: `fruix-freebsd`
- kernel string:
- `FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64`
Representative successful SSH validation output:
```text
FreeBSD fruix-freebsd 15.0-STABLE FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64
fruix-freebsd
192.168.213.62
```
Successful XCP-ng work directory:
- `/tmp/phase9-xcpng-ssh-1775097470`
## Important boot/debugging findings
The first decisive breakthrough came from running the generated image locally under QEMU/TCG with serial capture. That made the previously opaque early-boot failure visible.
### 1. The original early boot abort was not an XCP-ng image-format problem anymore
After the earlier switch from raw uploads to dynamic VHD uploads, the remaining boot failure was inside the guest boot process, not in the XO import path.
### 2. FreeBSD `fstab` handling for pseudo-filesystems was wrong
The serial log showed that boot aborted during filesystem checks because the generated `fstab` gave non-zero fsck fields to non-UFS mounts such as `devfs`.
Representative failure:
```text
Starting file system checks:
/dev/gpt/fruix-root: FILE SYSTEM CLEAN; SKIPPING CHECKS
/dev/gpt/fruix-root: clean, ...
fsck: exec fsck_devfs for devfs in /sbin:/usr/sbin: No such file or directory
Unknown error 1; help!
ERROR: ABORTING BOOT (sending SIGTERM to parent)!
```
The fix was to generate fsck pass fields only for UFS entries and emit `0 0` for pseudo-filesystems.
### 3. The minimal image was still missing many base files and commands expected by `rc`
Once `rc` ran further, QEMU serial logs exposed a long tail of missing runtime pieces that had not been visible from the earlier static validations alone.
Examples included:
- missing base commands:
- `dd`
- `expr`
- `rmdir`
- `sort`
- `mktemp`
- `egrep`
- `fsync`
- `kldload`
- `kldstat`
- `devfs`
- `devctl`
- `newsyslog`
- `ip6addrctl`
- missing base config files:
- `/etc/network.subr`
- `/etc/devd.conf`
- `/etc/newsyslog.conf`
- `/etc/syslog.conf`
- missing runtime directories:
- `/var/db`
- `/var/cron`
- missing libraries needed by later boot helpers:
- `libgeom.so.5`
- `libdevctl.so.5`
- `libcap_net.so.1`
- C++ runtime pieces used by `devd`
These were staged into the current FreeBSD package layer and linked into the generated rootfs.
### 4. SSH auth initially failed because the image relied on PAM without a complete PAM runtime/configuration
`sshd` would start, but root public-key authentication still failed. A direct in-guest debug run showed:
```text
PAM: initialisation failed
```
For the minimal Phase 9 guest, the practical fix was to make the generated `sshd_config` use:
- `UsePAM no`
while still keeping key-only login enabled.
That was sufficient to unlock real SSH access on both the local QEMU debug guest and the XCP-ng guest.
## Current code-level outcomes
The current checkpoint work materially expanded the minimal FreeBSD runtime staged into Fruix images.
Highlights:
- `modules/fruix/packages/freebsd.scm`
- added dedicated runtime packages for:
- `freebsd-networking`
- `freebsd-openssh`
- expanded staged base runtime coverage substantially for `rc`, networking, and SSH
- added required config files and shared libraries used during real boot
- `modules/fruix/system/freebsd.scm`
- added root authorized-key support to the operating-system model
- generated static account databases and supporting files:
- `/etc/passwd`
- `/etc/master.passwd`
- `/etc/group`
- `/etc/login.conf`
- `/etc/ttys`
- activation now runs:
- `cap_mkdb`
- `pwd_mkdb`
- activation creates required directories and SSH host keys
- generated `sshd_config` now disables PAM for the current minimal key-only Phase 9 path
- `fstab` generation now avoids fsck pass numbers for pseudo-filesystems
- rootfs generation now links the additional `/etc` files needed by real boot
- `tests/system/phase9-minimal-operating-system.scm.in`
- enables DHCP on the relevant NIC names for the current tracks:
- `xn0`
- `em0`
- `vtnet0`
- injects the root authorized key
- includes the SSH/network runtime packages and required system users/groups
- `tests/system/run-phase8-system-image.sh`
- now accepts `OS_FILE`
- now accepts/passes `DISK_CAPACITY`
- serial-console validation was relaxed from an exact loader string to a `comconsole` presence check
## Verified current state
The current validated Phase 9 state is:
- XCP-ng VHD upload path works against the existing VDI
- the guest boots far enough for normal `rc` networking and `sshd`
- DHCP works on the Xen NIC
- SSH key injection works
- root login over SSH works
This means the project has crossed an important Phase 9 boundary:
- the first boot validation no longer depends on local bhyve serial automation,
- and the real XCP-ng target can now be exercised over the network.
## Remaining blocker
Phase 9 is not complete yet because the Fruix-specific readiness path still fails.
Current remaining blocker:
- Guile still crashes in the guest
- therefore `fruix-shepherd` does not start
- therefore `/var/lib/fruix/ready` is still absent
Representative guest evidence:
```text
pid 262 (guile), jid 0, uid 0: exited on signal 11 (core dumped)
```
Over SSH on the real XCP-ng guest:
- `sshd` is running
- DHCP is active
- `fruix-shepherd` is stopped
- `/var/lib/fruix/ready` is missing
A retrieved core dump and local `lldb` analysis show the Guile crash occurs extremely early during initialization, in the locale/string conversion path while building Guile load/build info. This remains the next debugging target.
## Assessment
This checkpoint satisfies a meaningful Phase 9 intermediate milestone on the active FreeBSD/XCP-ng track:
- the generated Fruix image now boots as a network-reachable FreeBSD guest,
- and minimal operator access via SSH is working.
However, the full Fruix boot milestone is still blocked by in-guest Guile/Shepherd failure, so the overall Phase 9 milestone remains open.

View File

@@ -0,0 +1,160 @@
# Post-Phase-10: removed runtime dependence on `/tmp` Guile/Shepherd compatibility-prefix shims
Date: 2026-04-02
## Goal
After validating both the `rc.d` bridge path and the new Shepherd-as-PID-1 path, the next priority was the remaining runtime-artifact problem:
- the guest still carried activation-time `/tmp/...` compatibility symlinks for locally built Guile, guile-extra, and Shepherd prefixes
- those shims were compensating for baked-in historical paths in staged runtime artifacts
The goal of this subphase was narrower than fully rebuilding Guile/Shepherd in a store-native way. The immediate target was:
- remove the guest's *runtime dependence* on those `/tmp` compatibility-prefix symlinks,
- keep both validated boot modes working,
- and prove that Guile can still load the relevant modules from the store-backed runtime layout without those `/tmp` aliases.
## Result
This subphase succeeded.
The generated guest no longer needs activation to create:
- `/tmp/guile-freebsd-validate-install`
- `/tmp/guile-gnutls-freebsd-validate-install`
- `/tmp/shepherd-freebsd-validate-install`
Those shims are now absent in the booted guest, while both of these boot paths still pass on the real XCP-ng VM:
1. `freebsd-init+rc.d-shepherd`
2. `shepherd-pid1`
In both cases, the guest now also passes an explicit in-guest Guile module smoke test using the staged store paths and *without* the `/tmp` prefix aliases.
## What changed
### 1. Activation no longer creates `/tmp` compatibility-prefix symlinks
`modules/fruix/system/freebsd.scm` no longer emits activation logic that recreates the old `/tmp/...` prefix trees.
That means the guest is no longer depending on those aliases as part of ordinary first boot.
### 2. The staged runtime prefixes are sanitized as they are materialized into `/frx/store`
The prefix materializer now post-processes the copied runtime trees so the most relevant runtime-facing Scheme modules no longer require those `/tmp` prefix aliases.
This required a new prefix-materializer revision and deterministic post-copy sanitation.
### 3. Guile-extra runtime fixes
The staged guile-extra runtime now:
- patches `fibers/config.scm` so it can use `GUILE_EXTENSIONS_PATH` instead of falling back to the old `/tmp/guile-gnutls-freebsd-validate-install/...` path
- patches `gnutls.scm` so it can fall back to `GUILE_EXTENSIONS_PATH`
- removes stale compiled cache files that still embedded the old prefix values and would otherwise override the corrected source modules:
- `fibers/config.go`
- `gnutls.go`
### 4. Shepherd runtime fixes
The staged Shepherd runtime now:
- patches `share/guile/site/3.0/shepherd/config.scm` to use real runtime-facing paths instead of the old temporary install prefix:
- `Prefix-dir` => `/frx`
- `%localstatedir` => `/var`
- `%runstatedir` => `/var/run`
- `%sysconfdir` => `/etc`
- `%localedir` => `/usr/share/locale`
- `%pkglibdir` => `/usr/local/lib/shepherd`
- removes the stale compiled cache file that still embedded the old prefix values:
- `shepherd/config.go`
### 5. Real-VM validation harnesses now assert shim absence directly
The main real-VM validation scripts now check that the old `/tmp` compatibility-prefix trees are absent in the guest:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
They also run an in-guest Guile module smoke test using the store-backed runtime environment and require:
- `guile_module_smoke=ok`
## Validation
### Real VM, `freebsd-init+rc.d-shepherd`
Passing run:
- `PASS phase9-xcpng-boot`
- workdir: `/tmp/noshim-phase9-smoke-1775143001`
Key metadata:
```text
compat_prefix_shims=absent
guile_module_smoke=ok
ready_marker=ready
shepherd_status=running
sshd_status=running
```
### Real VM, `shepherd-pid1`
Passing run:
- `PASS phase11-shepherd-pid1-xcpng`
- workdir: `/tmp/noshim-phase11-smoke-1775142712`
Key metadata:
```text
compat_prefix_shims=absent
guile_module_smoke=ok
ready_marker=ready
shepherd_pid=1
shepherd_status=running
sshd_status=running
```
### Manual guest confirmation
A direct SSH probe on the real guest confirmed that all three historical `/tmp` compatibility prefixes are absent while Guile can still load:
- `(fibers config)`
- `(gnutls)`
- `(shepherd config)`
with output including:
```text
ABSENT:/tmp/guile-freebsd-validate-install
ABSENT:/tmp/guile-gnutls-freebsd-validate-install
ABSENT:/tmp/shepherd-freebsd-validate-install
1.4.2
/var/run
```
## Assessment
This removes an important transitional runtime crutch.
Fruix is no longer merely *booting despite* historical `/tmp` prefix aliases; it now boots and runs Guile/Shepherd services from the staged store-backed runtime arrangement without those guest-side compatibility symlinks.
## Remaining limitation
This does **not** mean every historical prefix string has disappeared from every staged artifact.
Some compiled binaries and libraries still contain old build/install strings internally, especially in places such as:
- `libguile`
- libtool metadata
- pkg-config metadata
- extension shared objects built upstream with the original prefixes
However, the important practical milestone is that the guest no longer depends on `/tmp` compatibility-prefix symlinks at runtime.
## Recommended next step
The next, deeper cleanup would be to eliminate the remaining baked prefix strings from the runtime artifacts themselves by moving the local Guile/guile-extra/Shepherd build/install flow closer to a genuinely store-native prefix from the start, rather than relying on post-copy sanitation.

View File

@@ -0,0 +1,178 @@
# Post-Phase-10: local Shepherd-as-PID-1 boot prototype on FreeBSD
Date: 2026-04-02
## Goal
Begin the next post-Phase-10 runtime-integration pass by exploring two related directions:
- reduce reliance on the `freebsd-init + rc.d + shepherd` bridge, and
- compare Fruix's boot path with how Guix boots Shepherd as PID 1.
The concrete goal for this subphase was not yet a full real-VM migration of the main boot track, but a first validated local prototype where the generated FreeBSD image boots with Shepherd as PID 1 under QEMU/UEFI.
## Comparison with Guix
Guix's system model treats Shepherd as a first-class root service.
In `~/repos/guix/gnu/services/shepherd.scm`, the key pattern is:
- `shepherd-root-service-type` extends the boot service graph
- `shepherd-boot-gexp` ultimately does an `execl` of Shepherd as PID 1
- higher-level system services extend that root Shepherd instance declaratively
In other words, Guix does not merely start Shepherd from a late init script; it composes the system boot graph around Shepherd directly.
Fruix is not at that level of native service composition yet, but this subphase adopts the same basic architectural direction:
- boot into a generated Shepherd config directly,
- let Shepherd own the service graph from PID 1,
- and keep the imperative compatibility/bootstrap logic as small as possible.
## Implementation
### 1. Added an explicit init-mode to the FreeBSD operating-system model
`modules/fruix/system/freebsd.scm` now has an `init-mode` field on the declarative operating-system record.
Supported values are currently:
- `freebsd-init+rc.d-shepherd`
- `shepherd-pid1`
The existing boot path remains the default.
### 2. Added a generated PID 1 launcher for the `shepherd-pid1` mode
For `shepherd-pid1`, the generated system now contains:
- `boot/fruix-pid1`
and the generated loader configuration adds:
- `init_exec="/run/current-system/boot/fruix-pid1"`
That means FreeBSD `init(8)` directly `exec`s the generated Fruix launcher as its very first action, replacing itself as PID 1.
The launcher currently performs the minimum bootstrap steps needed before turning control over to Shepherd:
- remount `/` read-write on this very-early path
- mount declared non-root filesystems such as:
- `devfs` on `/dev`
- `tmpfs` on `/tmp`
- set the hostname
- run `/run/current-system/activate`
- export the Guile/Shepherd runtime environment
- `exec` Shepherd directly
Because Shepherd is a Guile script, the actual PID 1 process image is the Guile interpreter running Shepherd. The important validation point is that Shepherd's own pidfile records PID 1 and the service socket is owned by that process.
### 3. Generated a different Shepherd config for PID 1 mode
For `shepherd-pid1`, the generated `shepherd/init.scm` now includes the minimal helper procedures it needs inline, rather than importing the repo-side `(fruix shepherd freebsd)` module at runtime.
This avoids depending on checkout-only Scheme modules being present in the guest.
The PID 1 config currently starts a minimal service graph:
- `fruix-logger`
- `netif` through FreeBSD `service(8)`
- `sshd` through FreeBSD `service(8)`
- `fruix-ready`
So this prototype still uses some FreeBSD rc scripts as service implementations, but now under Shepherd control rather than under `/etc/rc` as the primary boot manager.
### 4. Made activation more store-friendly for this early-boot path
The generated activation script now treats:
- `cap_mkdb /etc/login.conf`
- `pwd_mkdb -p /etc/master.passwd`
as best-effort operations.
That matters because Fruix currently symlinks these files from the immutable system closure, and on the very early PID 1 path they should not be allowed to abort the whole boot.
## Validation
### New PID 1 template
Added:
- `tests/system/phase11-shepherd-pid1-operating-system.scm.in`
This declares the same minimal FreeBSD Fruix guest shape as the current Phase 9 system, but with:
- `#:init-mode 'shepherd-pid1`
### New local QEMU validation harness
Added:
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
This harness:
- builds the image through the canonical `fruix system image` path
- boots it locally with QEMU/TCG + UEFI
- injects the root SSH key
- waits for the ready marker over forwarded SSH
- verifies that Shepherd is running and that Shepherd's pidfile says PID 1
### Successful run
Passing validation run:
- `PASS phase11-shepherd-pid1-qemu`
- workdir: `/tmp/pid1-qemu6-1775128407`
Key validated results:
```text
ready_marker=ready
run_current_system_target=/frx/store/8b44506c37da85cebf265c813ed3a9d2a42408b077ac85854e7d6209d2f910ec-fruix-system-fruix-freebsd
shepherd_pid=1
shepherd_socket=present
shepherd_status=running
sshd_status=running
pid1_command=[guile]
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
```
The important detail is:
- `shepherd_pid=1`
which shows that the running Shepherd instance in the guest is the system's PID 1 process.
## Assessment
This is a meaningful architectural step beyond the earlier `rc.d` bridge milestone.
Fruix now has a validated local boot path where:
- the generated image boots on FreeBSD,
- the generated launcher becomes PID 1 via `init_exec`,
- Shepherd itself owns PID 1,
- networking and SSH come up under Shepherd-managed service ordering,
- and the ready marker still appears.
## Remaining limitations
This is still a prototype, not yet the replacement for the main boot path.
Notable current limitations:
- the PID 1 path still relies on a small generated shell launcher before entering Shepherd
- some early boot/runtime actions are still expressed imperatively there
- the Guile/Shepherd local-runtime compatibility-prefix shims are not eliminated yet; they remain part of activation for the currently locally built runtime artifacts
- this subphase validated the path locally under QEMU/TCG, not yet on the real XCP-ng VM
## Recommended next step
Use this validated local PID 1 prototype as the base for the next subphase:
1. try the `shepherd-pid1` image on the real XCP-ng VM
2. if that succeeds, decide whether `shepherd-pid1` should become a selectable supported boot mode rather than just a prototype
3. continue reducing the remaining compatibility-prefix shims by moving the Guile/Shepherd runtime artifacts toward a more native store-aware arrangement

View File

@@ -0,0 +1,114 @@
# Post-Phase-10: Shepherd-as-PID-1 boot validated on the real XCP-ng FreeBSD VM
Date: 2026-04-02
## Goal
Take the locally validated Shepherd-as-PID-1 Fruix boot prototype and test it on the real operator-approved XCP-ng VM.
Target objects remained the same constrained deployment path used for Phase 9:
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI: `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
The concrete goal for this subphase was to confirm that the new `shepherd-pid1` init mode was not merely a local QEMU curiosity, but could also:
- boot on the real Xen guest,
- reach DHCP and SSH,
- keep Shepherd running as PID 1,
- and still reach the Fruix ready marker.
## Result
The real XCP-ng boot succeeded.
A new deployment/validation harness was added:
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
This harness reuses the existing real-VM deployment method:
- build a full-size image matching the existing VDI
- convert it to dynamic VHD
- overwrite the existing VDI
- boot the real VM
- rediscover the guest by MAC/IP
- validate the booted guest over SSH
The new Shepherd-PID-1 image passes that full path.
## Validation
Passing real-VM run:
- `PASS phase11-shepherd-pid1-xcpng`
- workdir: `/tmp/pid1-xcpng-1775129768`
Validated metadata from the real guest:
```text
ready_marker=ready
run_current_system_target=/frx/store/2940c952e9d35e47f98fe62f296be2b6ab4fceb3eee8248d6a7823decd42a305-fruix-system-fruix-freebsd
pid1_command=[guile]
shepherd_pid=1
shepherd_socket=present
shepherd_status=running
sshd_status=running
guest_ip=192.168.213.62
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
```
The key architectural confirmation is:
- `shepherd_pid=1`
That shows the running Shepherd instance in the real guest is PID 1.
As in the local QEMU prototype, the process image is Guile because Shepherd is launched as a Guile script; however, the service manager itself is the PID 1 process according to Shepherd's own pidfile and control socket state.
## What changed to make the real VM pass
The most important refinement after the first local PID 1 work was making the generated activation path more tolerant of immutable store-backed configuration files during very early boot.
Specifically, the generated activation script now treats these as best-effort:
- `cap_mkdb /etc/login.conf`
- `pwd_mkdb -p /etc/master.passwd`
That matters because on the PID 1 path they happen earlier and should not abort the system if the current `/etc` representation is not suitable for in-place database regeneration.
The Shepherd-PID-1 operating-system template was also expanded to keep the NIC configuration broad enough for both local virtio and the real Xen path:
- `ifconfig_xn0=SYNCDHCP`
- `ifconfig_em0=SYNCDHCP`
- `ifconfig_vtnet0=SYNCDHCP`
## Assessment
This is a stronger result than the earlier local-only prototype.
Fruix now has a real deployment-validated FreeBSD boot mode where:
- FreeBSD `init(8)` hands off immediately via `init_exec`
- the generated Fruix launcher performs the minimal bootstrap
- Shepherd becomes PID 1
- networking and SSH still work on the real XCP-ng VM
- and the system still reaches the Fruix ready marker
That means the project has now validated both of these boot architectures on the real VM:
1. `freebsd-init+rc.d-shepherd`
2. `shepherd-pid1`
## Remaining limitations
This does not yet eliminate the current locally built Guile/Shepherd compatibility-prefix shims.
Those shims are still needed because the locally staged runtime artifacts continue to embed historical build prefixes. The current result proves that the broader init/boot-manager dependency can be removed, but it does not yet fully solve the store-native runtime-prefix problem.
## Conclusion
The Shepherd-as-PID-1 Fruix boot mode now works not only under local QEMU/UEFI, but also on the real operator-approved XCP-ng VM.
This substantially strengthens the case that Fruix can move beyond the transitional `rc.d` bridge design and toward a more Guix-like PID-1-centered system architecture on FreeBSD.

View File

@@ -0,0 +1,223 @@
# Post-Phase 20: separate development from native base-build environment
Date: 2026-04-06
## Goal
Tighten the distinction that Phase 20.3 exposed in practice:
- an interactive development shell is not the same thing as a reliable native base-build environment
Fruix now models three layers instead of two:
- runtime profile
- development profile
- build profile
## Why this change was needed
The earlier self-hosted native-build prototype proved that simply reusing the exported development environment for `buildworld` / `buildkernel` was too loose.
Development-oriented exports like:
- `MAKEFLAGS`
- `CPPFLAGS`
- `CFLAGS`
- `CXXFLAGS`
- `LDFLAGS`
are useful for interactive compilation work, but they are the wrong contract for a real FreeBSD world/kernel bootstrap.
Phase 20.3 previously worked around that by manually sanitizing the shell before running the build.
This change makes that separation explicit in the product model instead of keeping it as an ad hoc helper detail.
## Implementation
### New operating-system layer
`modules/fruix/system/freebsd/model.scm` now supports:
- `#:build-packages`
- `operating-system-build-packages`
So a build-capable system can now carry both:
- `development-packages`
- `build-packages`
separately.
### New build profile inside the system closure
When `build-packages` is non-empty, Fruix now materializes:
- `/frx/store/...-fruix-system-.../build-profile`
alongside the existing:
- `/frx/store/...-fruix-system-.../profile`
- `/frx/store/...-fruix-system-.../development-profile`
Store-layout metadata now records both development-package stores and build-package stores explicitly.
### New in-guest build helper
Build-capable systems now ship:
- `/usr/local/bin/fruix-build-environment`
and expose a stable runtime link:
- `/run/current-build -> /run/current-system/build-profile`
That helper intentionally emits a stricter environment contract than the interactive development helper.
It clears development-shell variables such as:
- `MAKEOBJDIRPREFIX`
- `MAKEFLAGS`
- `CC`
- `CXX`
- `AR`
- `RANLIB`
- `NM`
- `CPPFLAGS`
- `CFLAGS`
- `CXXFLAGS`
- `LDFLAGS`
and also clears development-helper exports such as:
- `FRUIX_DEVELOPMENT_PROFILE`
- `FRUIX_CC`
- `FRUIX_CXX`
- `FRUIX_AR`
- `FRUIX_RANLIB`
- `FRUIX_NM`
- `FRUIX_BMAKE`
Then it exports build-specific values including at least:
- `FRUIX_BUILD_PROFILE`
- `FRUIX_BUILD_INCLUDE`
- `FRUIX_BUILD_SHARE_MK`
- `FRUIX_BUILD_BIN`
- `FRUIX_BUILD_USR_BIN`
- `FRUIX_BUILD_CC`
- `FRUIX_BUILD_CXX`
- `FRUIX_BUILD_AR`
- `FRUIX_BUILD_RANLIB`
- `FRUIX_BUILD_NM`
- `FRUIX_BMAKE`
- `PATH`
Intended use:
```sh
eval "$(/usr/local/bin/fruix-build-environment)"
```
### Canonical build compatibility links now come from the build profile
For native base-build compatibility, build-capable systems now expose:
- `/usr/include -> /run/current-system/build-profile/usr/include`
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
This means the running system can still keep development and build content separate while offering the canonical paths that FreeBSD native build machinery expects.
### Self-hosted native-build helper now uses the build helper contract
`/usr/local/bin/fruix-self-hosted-native-build` now:
- requires `build-profile`
- requires `/usr/local/bin/fruix-build-environment`
- evaluates the build helper contract before `buildworld`
- verifies the canonical compatibility links point at `build-profile`
This replaces the earlier approach where the helper had to reconstruct sanitization manually around a development-oriented environment.
### Promotion metadata now records build profile provenance
Promotion metadata emitted for self-hosted native-build results now records:
- `build-profile`
Promoted artifact/result metadata also now preserves that field.
## Validation
Validated on the approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Passing runs:
- `PASS phase20-development-environment-xcpng`
- `PASS phase20-self-hosted-native-build-xcpng`
- `PASS phase20-native-build-store-promotion-xcpng`
- `PASS phase20-host-initiated-native-build-xcpng`
- `PASS phase20-host-initiated-native-build-store-promotion-xcpng`
## Representative validated results
Development/build environment validation:
```text
development_profile_guest=/run/current-system/development-profile
build_profile_guest=/run/current-system/build-profile
development_env_script=/frx/store/...-fruix-system-.../usr/local/bin/fruix-development-environment
build_env_script=/frx/store/...-fruix-system-.../usr/local/bin/fruix-build-environment
development_environment=ok
```
Self-hosted native-build validation:
```text
helper_version=5
executor_kind=self-hosted
executor_name=guest-self-hosted
executor_version=5
build_profile=/run/current-system/build-profile
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
self_hosted_native_build=ok
```
Promotion validation:
```text
executor_kind=self-hosted
executor_name=guest-self-hosted
executor_version=5
result_store=/frx/store/3ce6aefd564bc51f2465dcbb5c261355be4c7076-fruix-native-build-result-15.0-STABLE-guest-self-hosted
native_build_store_promotion=ok
```
Host-initiated path revalidation with the new build-profile split also succeeded:
```text
executor_kind=ssh-guest
executor_name=ssh-guest
executor_version=1
result_store=/frx/store/93a4837d2588acfde2262010b296df9c7b7b367a-fruix-native-build-result-15.0-STABLE-ssh-guest
host_initiated_native_build_store_promotion=ok
```
## Result
Fruix now expresses an important product distinction more honestly:
- development is for interactive work
- build is for a stricter native base-build contract
That reduces special-case cleanup inside the self-hosted helper and gives Fruix a clearer path for future operator-facing commands such as:
- `fruix system build-base`
- `fruix system upgrade`
because the system model now has an explicit place to say:
- what is available for interactive development
- what is available for real native base builds

View File

@@ -0,0 +1,178 @@
# Post-Phase 20: installed systems as real Fruix nodes
Date: 2026-04-06
## Goal
Make a Fruix-installed machine feel like a managed Fruix node instead of only a deployed image with switch/rollback helpers.
The immediate target was not yet the full long-term command surface of:
- `fruix system upgrade`
- `fruix system build-base`
- `fruix system deploy`
but it was enough to cross an important product threshold:
- installed nodes can now remember their own declaration inputs
- installed nodes can build from those inputs locally
- installed nodes can reconfigure themselves into a newly built generation
- installed nodes can still roll back cleanly
## What changed
### Canonical declaration state now ships inside the system closure
Fruix system closures now carry explicit declaration metadata:
- `metadata/system-declaration.scm`
- `metadata/system-declaration-info.scm`
- `metadata/system-declaration-system`
That gives an installed system a canonical local answer to:
- what declaration source produced me?
- what top-level system variable should be used?
This declaration metadata is also recorded through the installed generation layout metadata.
### Bundled Fruix node CLI sources now ship inside the closure
Installed system closures now also carry a self-contained Fruix node CLI source bundle under:
- `share/fruix/node/scripts/fruix.scm`
- `share/fruix/node/modules/...`
- `share/fruix/node/guix/guix/build/utils.scm`
This gives the installed node enough local Fruix/Guix Scheme source to run Fruix system actions from the node itself.
### Installed `fruix` helper gained local build/reconfigure support
The installed helper at:
- `/usr/local/bin/fruix`
now supports:
- `fruix system build`
- `fruix system reconfigure`
- `fruix system status`
- `fruix system switch`
- `fruix system rollback`
Current behavior:
- `fruix system build` with no extra arguments uses:
- `/run/current-system/metadata/system-declaration.scm`
- `/run/current-system/metadata/system-declaration-system`
- `fruix system reconfigure` with no extra arguments builds from that same embedded declaration and then stages a switch to the resulting closure
- both commands can also take an explicit declaration file plus `--system NAME`, using the same general CLI shape as the host-side Fruix frontend
### In-node builds now reuse the installed Fruix runtime stores
A crucial implementation fix was needed here.
An installed node should not try to reconstruct the Guile/Shepherd runtime prefixes from host-side `/tmp/...` build roots or host `/usr/local/lib/...` assumptions.
Instead, in-node Fruix builds now explicitly reuse the installed runtime stores already referenced by the current system closure.
That allows the in-node build path to work on the real installed system rather than depending on build-host-only paths.
## Validation harness
Added:
- `tests/system/postphase20-installed-node-operating-system.scm.in`
- `tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh`
This harness validates the new installed-node workflow on the approved real XCP-ng path.
## Validation performed
Approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Promoted native-base result consumed by the installed node declaration:
- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest`
Validated flow:
1. boot a Fruix-installed node built from the promoted native-base result
2. confirm the installed node exposes:
- embedded declaration metadata
- bundled node CLI sources
3. run in-guest:
- `fruix system build`
using the node's own embedded declaration inputs
4. copy a candidate declaration to the guest
5. run in-guest:
- `fruix system build /root/candidate.scm --system postphase20-installed-node-operating-system`
6. run in-guest:
- `fruix system reconfigure /root/candidate.scm --system postphase20-installed-node-operating-system`
7. reboot and verify the candidate generation boots
8. run in-guest:
- `fruix system rollback`
9. reboot and verify the original generation boots again
Passing run:
- `PASS postphase20-installed-node-build-reconfigure-xcpng`
Representative metadata:
```text
closure_path=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
current_built_closure=/frx/store/18ee10925a15b48c676463a3359c45ff766e16a0-fruix-system-fruix-node-current
candidate_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
reconfigure_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
reconfigure_current_generation=2
reconfigure_current_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
reconfigure_rollback_generation=1
reconfigure_rollback_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
candidate_hostname=fruix-node-canary
rollback_current_generation=1
rollback_current_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
rollback_rollback_generation=2
rollback_rollback_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
rollback_hostname=fruix-node-current
installed_node_build_reconfigure=ok
```
## Important observation
The no-argument in-node build of the current declaration did not necessarily reproduce the exact original current closure path.
That is expected with the current model because closure metadata still records builder-local provenance such as the current build host context.
So the significant validated fact is not strict bit-for-bit closure reuse.
It is that the installed node can now:
- read its own declaration inputs locally
- build a valid local candidate closure from them
- build a different candidate declaration locally
- reconfigure itself into that candidate generation
- boot the candidate generation
- roll back and boot the prior generation again
## Result
Fruix-installed machines are now meaningfully closer to real Fruix nodes.
They no longer only support:
- `status`
- `switch`
- `rollback`
They now also support a validated local node workflow for:
- reading embedded declaration inputs
- building a local candidate closure
- reconfiguring into that locally built candidate
- rolling back through the same installed generation model
This is the first concrete step toward a fuller operator-facing Fruix node command surface.

View File

@@ -0,0 +1,165 @@
# Post-Phase 20: promoted native base result sets as declaration inputs
Date: 2026-04-06
## Goal
Move Fruix from:
- “the guest built a kernel”
to:
- “Fruix materialized a native-base result set with identity X”
and then prove that a normal Fruix system declaration can consume that promoted result set directly.
Concretely, the desired product behavior was:
1. build runs somewhere
2. result is recorded in a staging/result root
3. Fruix validates the result shape and metadata
4. Fruix promotes it into store objects
5. system declarations can refer to those promoted artifacts
Before this step, Fruix already had validated steps 1 through 4.
This step implemented and validated step 5.
## What changed
### First-class promoted result reference in system declarations
Fruix now has a first-class promoted native-build result reference object.
Current declaration-level entry points are:
- `promoted-native-build-result`
- `operating-system-from-promoted-native-build-result`
A declaration can now point at a promoted native-build result bundle in `/frx/store`, for example:
```scheme
(define promoted
(promoted-native-build-result
#:store-path "/frx/store/...-fruix-native-build-result-..."))
(define os
(operating-system-from-promoted-native-build-result
promoted
...))
```
### Promoted result bundles now drive system composition
From a promoted result bundle, Fruix now derives:
- the effective FreeBSD base metadata used by the system declaration
- kernel package identity from the promoted kernel artifact store
- bootloader package identity from the promoted bootloader artifact store
- base-package identity from the promoted world artifact store
- optional development-package identity from the promoted headers artifact store
This means a system declaration can now be based on a promoted native-build result set rather than only on source-driven base-package reconstruction.
### Closure metadata now records promoted-result provenance explicitly
When a system declaration uses a promoted native-build result set, Fruix now records that explicitly in the resulting closure metadata.
New closure metadata includes:
- `metadata/promoted-native-build-result.scm`
- `metadata/store-layout.scm` entries for:
- promoted result summary
- promoted artifact stores
The system closure also retains the promoted result bundle itself as an explicit reference in:
- `/frx/store/...-fruix-system-.../.references`
So the resulting deployed closure does not merely happen to contain files copied from a promoted artifact; it explicitly records which promoted result set it came from.
## Implementation notes
The key plumbing additions were:
- a promoted native-build result record in:
- `modules/fruix/system/freebsd/model.scm`
- promoted-result readers/helpers in:
- `modules/fruix/system/freebsd/build.scm`
- direct system-construction helper:
- `operating-system-from-promoted-native-build-result`
- support for consuming promoted artifact stores directly when materializing packages
- closure/store-layout metadata wiring in:
- `modules/fruix/system/freebsd/media.scm`
The important product-level effect is that promoted native-build results are no longer only post-build artifacts. They are now declaration inputs.
## Validation harness
Added:
- `tests/system/phase20-promoted-native-base-operating-system.scm.in`
- `tests/system/run-phase20-promoted-native-base-declaration-xcpng.sh`
The harness supports two modes:
- use an explicit `RESULT_STORE=/frx/store/...-fruix-native-build-result-...`
- or, if no result store is supplied, first run the validated host-initiated promotion path and consume that result
## Validation performed
Validated on the approved real XCP-ng path using the promoted `ssh-guest` result bundle:
- result bundle:
- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest`
- VM:
- `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI:
- `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Passing run:
- `PASS phase20-promoted-native-base-declaration-xcpng`
Representative metadata:
```text
closure_path=/frx/store/ac1e6dcfe67d3cde49d4fd5da97740f6244276b4-fruix-system-fruix-freebsd
result_store=/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest
executor_kind=ssh-guest
executor_name=ssh-guest
executor_version=1
world_store=/frx/store/89c7a71c3df148a1f99b13d57fd6be88243eb2cb-fruix-native-world-15.0-STABLE-ssh-guest
kernel_store=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest
headers_store=/frx/store/dd7f39f526bca4849caf1eaf96ae25d29b43493c-fruix-native-headers-15.0-STABLE-ssh-guest
bootloader_store=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest
kernel_link=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest/boot/kernel/kernel
bootloader_link=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest/boot/loader.efi
guest_kernel_link=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest/boot/kernel/kernel
guest_bootloader_link=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest/boot/loader.efi
guest_uname=FreeBSD 15.0-STABLE
promoted_native_base_declaration=ok
```
## Validated facts
The real-VM validation proved that Fruix can now:
- consume a promoted native-build result bundle directly from a declaration
- materialize a normal system closure from that promoted identity
- retain explicit promoted-result provenance in closure metadata
- boot the resulting system successfully on the approved XCP-ng path
- keep the closure's kernel and bootloader linked directly to the promoted artifact stores
- preserve the promoted result-bundle store itself as an explicit closure reference
So the operator-facing story is now materially better:
- not only “a build happened somewhere”
- but “this system declaration is based on promoted native-base result X”
## Result
Promoted native-build result sets are now first-class Fruix declaration inputs.
That is the point where the native FreeBSD base path stops being only a build/promotion harness and starts acting more like real product behavior.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,794 @@
# Fruix system deployment workflow
Date: 2026-04-06
## Purpose
This document defines the current canonical Fruix workflow for:
- building a declarative system closure
- materializing deployable artifacts
- installing a declarative system onto an image or disk
- booting through installer media
- rolling forward to a candidate system
- switching an installed system to a staged candidate generation
- rolling an installed system back to an earlier recorded generation
This is now the Phase 19 operator-facing view of the system model as validated through explicit installed-system generation switching and rollback.
## Core model
A Fruix system workflow starts from a Scheme file that binds an `operating-system` object.
Today, the canonical frontend is:
- `./bin/fruix system ...`
The important output objects are:
- **system closure**
- a content-addressed store item under `/frx/store/*-fruix-system-<host-name>`
- includes boot assets, activation logic, profile tree, metadata, and references
- **rootfs tree**
- a materialized runtime tree for inspection or image staging
- **disk image**
- a bootable GPT/UEFI raw disk image
- **installer image**
- a bootable Fruix installer disk image that installs a selected target system from inside the guest
- **installer ISO**
- a bootable UEFI ISO with an embedded installer mdroot payload
- **install metadata**
- `/var/lib/fruix/install.scm` on installed targets
- records the selected closure path, install spec, and referenced store items including source provenance
The current deployment story is therefore already declaration-driven and content-addressed, even before first-class installed-system generations are modeled more explicitly.
## Canonical command surface
### Build a system closure
```sh
sudo env HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
GUIX_SOURCE_DIR="$HOME/repos/guix" \
GUILE_BIN="/tmp/guile-freebsd-validate-install/bin/guile" \
GUILE_EXTRA_PREFIX="/tmp/guile-gnutls-freebsd-validate-install" \
SHEPHERD_PREFIX="/tmp/shepherd-freebsd-validate-install" \
./bin/fruix system build path/to/system.scm --system my-operating-system
```
Primary result:
- `closure_path=/frx/store/...-fruix-system-...`
Use this when you want to:
- validate the declarative system composition itself
- inspect provenance/layout metadata
- compare candidate and current closure paths
- drive later rootfs/image/install steps from the same declaration
### Materialize a rootfs tree
```sh
sudo env HOME="$HOME" ... \
./bin/fruix system rootfs path/to/system.scm ./rootfs --system my-operating-system
```
Primary result:
- `rootfs=...`
- `closure_path=/frx/store/...`
Use this when you want to:
- inspect the runtime filesystem layout directly
- stage a tree for debugging
- validate `/run/current-system`-style symlink layout without booting a full image
### Materialize a bootable disk image
```sh
sudo env HOME="$HOME" ... \
./bin/fruix system image path/to/system.scm \
--system my-operating-system \
--root-size 6g
```
Primary result:
- `disk_image=/frx/store/.../disk.img`
Use this when you want to:
- boot the system directly as a VM image
- test a candidate deployment under QEMU or XCP-ng
- validate a roll-forward or rollback candidate by image boot
### Install directly to an image file or block device
```sh
sudo env HOME="$HOME" ... \
./bin/fruix system install path/to/system.scm \
--system my-operating-system \
--target ./installed.img \
--disk-capacity 12g \
--root-size 10g
```
Primary result:
- `target=...`
- `target_kind=raw-file` or `block-device`
- `install_metadata_path=/var/lib/fruix/install.scm`
Use this when you want to:
- produce an installed target image without booting an installer guest
- validate installation mechanics directly
- populate a raw image or a real `/dev/...` target
### Materialize a bootable installer disk image
```sh
sudo env HOME="$HOME" ... \
./bin/fruix system installer path/to/system.scm \
--system my-operating-system \
--install-target-device /dev/vtbd1 \
--root-size 10g
```
Primary result:
- `installer_disk_image=/frx/store/.../disk.img`
Use this when you want to:
- boot a Fruix installer environment as a disk image
- let the in-guest installer partition and install onto a second disk
- validate non-interactive installation from inside a booted Fruix guest
### Materialize a bootable installer ISO
```sh
sudo env HOME="$HOME" ... \
./bin/fruix system installer-iso path/to/system.scm \
--system my-operating-system \
--install-target-device /dev/vtbd0
```
Primary result:
- `iso_image=/frx/store/.../installer.iso`
- `boot_efi_image=/frx/store/.../efiboot.img`
- `root_image=/frx/store/.../root.img`
Use this when you want to:
- boot through UEFI ISO media instead of a writable installer disk image
- install from an ISO-attached Fruix environment
- test the same install model on more realistic VM paths
### Installed-system generation commands
Installed Fruix systems now also ship a small in-guest deployment helper at:
- `/usr/local/bin/fruix`
Current validated in-guest commands are:
```sh
fruix system build
fruix system reconfigure
fruix system status
fruix system switch /frx/store/...-fruix-system-...
fruix system rollback
```
Installed systems now carry canonical declaration state in:
- `/run/current-system/metadata/system-declaration.scm`
- `/run/current-system/metadata/system-declaration-info.scm`
- `/run/current-system/metadata/system-declaration-system`
So the in-guest helper can now build from the node's own embedded declaration inputs.
Current validated build/reconfigure behavior is:
- `fruix system build`
- with no extra arguments, builds from the embedded current declaration
- `fruix system reconfigure`
- with no extra arguments, builds from the embedded current declaration and stages a switch to the resulting closure
- both commands can also take an explicit declaration file plus `--system NAME`
Current intended usage now has two validated patterns.
### Pattern A: build elsewhere, then switch/rollback locally
1. build a candidate closure on the operator side with `./bin/fruix system build`
2. ensure that candidate closure is present on the installed target's `/frx/store`
3. run `fruix system switch /frx/store/...` on the installed system
4. reboot into the staged candidate generation
5. if needed, run `fruix system rollback`
6. reboot back into the recorded rollback generation
Important current limitation of this lower-level pattern:
- `fruix system switch` does **not** yet fetch or copy the candidate closure onto the target for you
- it assumes the selected closure is already present in the installed system's `/frx/store`
### Pattern B: build and reconfigure from the node itself
1. inspect or edit the node declaration inputs
- embedded current declaration, or
- an explicit replacement declaration file
2. run:
```sh
fruix system build
```
or:
```sh
fruix system build /path/to/candidate.scm --system my-operating-system
```
3. stage a local generation update with:
```sh
fruix system reconfigure
```
or:
```sh
fruix system reconfigure /path/to/candidate.scm --system my-operating-system
```
4. reboot into the staged generation
5. if needed, run `fruix system rollback`
6. reboot back into the recorded prior generation
### In-guest development and build environments
Opt-in systems can now expose two separate overlays above the main runtime profile:
- development:
- `/run/current-system/development-profile`
- `/run/current-development`
- `/usr/local/bin/fruix-development-environment`
- build:
- `/run/current-system/build-profile`
- `/run/current-build`
- `/usr/local/bin/fruix-build-environment`
Intended use:
```sh
eval "$(/usr/local/bin/fruix-development-environment)"
```
for interactive development work, and:
```sh
eval "$(/usr/local/bin/fruix-build-environment)"
```
for a narrower native base-build contract.
The current split is:
- runtime profile
- development profile
- build profile
The development helper remains intentionally interactive and currently exposes at least:
- native headers under `usr/include`
- FreeBSD `share/mk` files for `bsd.*.mk`
- Clang toolchain commands such as `cc`, `c++`, `ar`, `ranlib`, and `nm`
- `MAKEFLAGS` pointing at the development profile's `usr/share/mk`
The build helper is intentionally more sanitized and less interactive. It clears development-shell variables such as:
- `MAKEFLAGS`
- `CPPFLAGS`
- `CFLAGS`
- `CXXFLAGS`
- `LDFLAGS`
and then exposes build-oriented paths such as:
- `FRUIX_BUILD_PROFILE`
- `FRUIX_BUILD_INCLUDE`
- `FRUIX_BUILD_SHARE_MK`
- `FRUIX_BUILD_CC`
- `FRUIX_BUILD_CXX`
- `FRUIX_BUILD_AR`
- `FRUIX_BUILD_RANLIB`
- `FRUIX_BUILD_NM`
- `FRUIX_BMAKE`
For native base-build compatibility, build-enabled systems now expose canonical links at:
- `/usr/include -> /run/current-system/build-profile/usr/include`
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
So Fruix now separates interactive development support from the stricter environment used for `buildworld` / `buildkernel` style work, instead of treating them as one overlay.
### Host-initiated native base builds inside a Fruix-managed guest
The currently validated intermediate path toward self-hosting is still host-orchestrated.
The host:
1. boots a development-enabled Fruix guest
2. connects over SSH
3. recovers the materialized FreeBSD source store from system metadata
4. runs native FreeBSD build commands inside the guest
5. collects and records the staged outputs
The validated build sequence inside the guest is:
- `make -jN buildworld`
- `make -jN buildkernel`
- `make DESTDIR=... installworld`
- `make DESTDIR=... distribution`
- `make DESTDIR=... installkernel`
For staged install steps, the validated path uses:
- `DB_FROM_SRC=yes`
so the staged install is driven by the declared source tree's account database rather than by accidental guest-local `/etc/master.passwd` contents.
This is the current Phase 20.2 answer to “where should native base builds run?”
- **inside** a Fruix-managed FreeBSD environment
- but still with the **host** driving the outer orchestration loop
### Controlled guest self-hosted native-build prototype
Fruix now also has a narrower in-guest prototype helper at:
- `/usr/local/bin/fruix-self-hosted-native-build`
Intended use:
```sh
FRUIX_SELF_HOSTED_NATIVE_BUILD_JOBS=8 \
/usr/local/bin/fruix-self-hosted-native-build
```
That helper:
1. evaluates the build helper and verifies the build overlay plus canonical compatibility links
2. recovers the materialized FreeBSD source store from:
- `/run/current-system/metadata/store-layout.scm`
3. runs the native FreeBSD build/install phases inside the guest
4. records staged results under:
- `/var/lib/fruix/native-builds/<run-id>`
- `/var/lib/fruix/native-builds/latest`
5. emits promotion metadata for first-class artifact identities covering:
- `world`
- `kernel`
- `headers`
- `bootloader`
6. keeps the heavier object/stage work under:
- `/var/tmp/fruix-self-hosted-native-builds/<run-id>`
Important current detail:
- the self-hosted helper now uses the separate `fruix-build-environment` contract instead of reusing the interactive development helper wholesale
- that build helper intentionally clears development-shell exports such as `MAKEFLAGS`, `CPPFLAGS`, `CFLAGS`, `CXXFLAGS`, and `LDFLAGS` before `buildworld`
- this keeps the base-build path closer to the exact contract needed for real world/kernel bootstrap work
So the validated Phase 20.3 answer is:
- a controlled guest self-hosted base-build prototype now works
- but the simpler default operator flow should still be the Phase 20.2 host-initiated in-guest path unless there is a specific reason to push the build loop farther into the guest
### Promoting native-build results into first-class Fruix store objects
The guest-side result root is now explicitly a **staging/result area**, not the final immutable identity.
Current validated flow:
1. run the in-guest helper so the guest records a result under:
- `/var/lib/fruix/native-builds/<run-id>`
2. copy that result root back to the host
3. run:
```sh
fruix native-build promote RESULT_ROOT
```
The promotion step creates immutable `/frx/store` identities for:
- `world`
- `kernel`
- `headers`
- `bootloader`
and also creates a result-bundle store object that references those promoted artifact stores.
Current metadata split:
- mutable staging/result root:
- `/var/lib/fruix/native-builds/<run-id>`
- immutable artifact stores:
- `/frx/store/...-fruix-native-world-...`
- `/frx/store/...-fruix-native-kernel-...`
- `/frx/store/...-fruix-native-headers-...`
- `/frx/store/...-fruix-native-bootloader-...`
- immutable result bundle:
- `/frx/store/...-fruix-native-build-result-...`
The promoted store objects record explicit Fruix-native metadata including at least:
- executor kind / name / version
- run-id / guest-host-name
- closure path
- source store provenance
- build policy
- artifact kind
- required-file expectations
- recorded content signatures and hashes
This is the current Fruix-native answer to the question:
- where should mutable native-build state live?
- `/var/lib/fruix/native-builds/...`
- where should immutable native-build identity live?
- `/frx/store/...`
### Using promoted native-build results in system declarations
Fruix system declarations can now refer directly to a promoted native-build result bundle.
Current declaration-level helpers are:
- `promoted-native-build-result`
- `operating-system-from-promoted-native-build-result`
Representative pattern:
```scheme
(define promoted
(promoted-native-build-result
#:store-path "/frx/store/...-fruix-native-build-result-..."))
(define os
(operating-system-from-promoted-native-build-result
promoted
#:host-name "fruix-freebsd"
...))
```
That now gives Fruix a more product-like story:
1. a build runs under some executor policy
2. Fruix records the staged mutable result
3. Fruix promotes it into immutable store identities
4. a later system declaration can point at that promoted result identity
5. Fruix materializes and boots a normal system from that promoted identity
The resulting closure now records that provenance explicitly through:
- `metadata/promoted-native-build-result.scm`
- `metadata/store-layout.scm`
- closure references that retain the selected result-bundle store path
So the operator-facing statement is now:
- “this Fruix system is based on promoted native-base result X”
not only:
- “some earlier build happened and its files were copied somewhere.”
### Native-build executor model
Fruix now has an explicit executor model for native base builds.
Current executor kinds are:
- `host`
- `ssh-guest`
- `self-hosted`
and the intended future extension points are:
- `jail`
- `remote-builder`
The important change is architectural:
- declared source identity stays the same
- expected artifact kinds stay the same
- result/promotion metadata shape stays the same
- only the executor policy changes
So “where the build runs” is now treated as executor policy rather than as a separate native-build architecture each time.
Current end-to-end validated executors for the staged-result-plus-promotion model are:
- `ssh-guest`
- `self-hosted`
Both now converge on the same Fruix-native flow:
1. run the build under a selected executor
2. stage a result root under `/var/lib/fruix/native-builds/...`
3. emit the same promotion/provenance shape
4. promote the result into immutable `/frx/store/...` objects
## Deployment patterns
### 1. Build-first workflow
The default Fruix operator workflow starts by building the closure first:
1. edit the system declaration
2. run `fruix system build`
3. inspect emitted metadata
4. if needed, produce one of:
- `rootfs`
- `image`
- `install`
- `installer`
- `installer-iso`
This keeps the declaration-to-closure boundary explicit.
### 2. VM image deployment workflow
Use this when you want to boot a system directly rather than through an installer.
1. run `fruix system image`
2. boot the image in QEMU or convert/import it for XCP-ng
3. validate:
- `/run/current-system`
- shepherd/sshd state
- activation log
4. keep the closure path from the build metadata as the deployment identity
This is the current canonical direct deployment path for already-built images.
### 3. Direct installation workflow
Use this when you want an installed target image or disk without a booted installer guest.
1. run `fruix system install --target ...`
2. let Fruix partition, format, populate, and install the target
3. boot the installed result
4. validate `/var/lib/fruix/install.scm` and target services
This is the most direct install path.
### 4. Installer-environment workflow
Use this when the install itself should happen from inside a booted Fruix environment.
1. run `fruix system installer`
2. boot the installer disk image
3. let the in-guest installer run onto the selected target device
4. boot the installed target
This is useful when the installer environment itself is part of what needs validation.
### 5. Installer-ISO workflow
Use this when the desired operator artifact is a bootable UEFI ISO.
1. run `fruix system installer-iso`
2. boot the ISO under the target virtualization path
3. let the in-guest installer run onto the selected target device
4. eject the ISO and reboot the installed target
This is now validated on both:
- local `QEMU/UEFI/TCG`
- the approved real `XCP-ng` VM path
## Install-target device conventions
The install target device is not identical across all boot styles.
Current validated defaults are:
- direct installer disk-image path under QEMU:
- `/dev/vtbd1`
- installer ISO path under QEMU:
- `/dev/vtbd0`
- installer ISO path under XCP-ng:
- `/dev/ada0`
Therefore the canonical workflow is:
- always treat `--install-target-device` as an explicit deployment parameter when moving between virtualization environments
Do not assume that a device name validated in one harness is portable to another.
## Installed-system generation layout
Installed Fruix systems now record an explicit first-generation deployment layout under:
- `/var/lib/fruix/system`
Initial installed shape:
```text
/var/lib/fruix/system/
current -> generations/1
current-generation
generations/
1/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm # present on installed targets
```
After a validated in-place switch, the layout extends to:
```text
/var/lib/fruix/system/
current -> generations/2
current-generation
rollback -> generations/1
rollback-generation
generations/
1/
...
2/
closure -> /frx/store/...-fruix-system-...
metadata.scm
provenance.scm
install.scm # deployment metadata for the switch operation
```
Installed systems also now create explicit GC-root-style deployment links under:
- `/frx/var/fruix/gcroots`
Current validated shape:
```text
/frx/var/fruix/gcroots/
current-system -> /frx/store/...-fruix-system-...
rollback-system -> /frx/store/...-fruix-system-...
system-1 -> /frx/store/...-fruix-system-...
system-2 -> /frx/store/...-fruix-system-...
```
Important detail:
- `/run/current-system` still points directly at the active closure path in `/frx/store`
- the explicit generation layout therefore adds deployment metadata and retention roots without changing the already-validated runtime contract used by activation, rc.d wiring, and tests
## Roll-forward workflow
The current Fruix roll-forward model now has two validated layers.
### Declaration/deployment roll-forward
Canonical process:
1. keep the current known-good system declaration
2. prepare a candidate declaration
- this may differ by FreeBSD base identity
- source revision
- services
- users/groups
- or other operating-system fields
3. run `fruix system build` for the candidate
4. materialize either:
- `fruix system image`
- `fruix system install`
- `fruix system installer`
- `fruix system installer-iso`
5. boot or install the candidate
6. validate the candidate closure in the booted system
### Installed-system generation roll-forward
When the candidate closure is already present on an installed target:
1. run `fruix system switch /frx/store/...candidate...`
2. confirm the staged state with `fruix system status`
3. reboot into the candidate generation
4. validate the new active closure after reboot
The important property is still that the candidate closure appears beside the earlier one in `/frx/store` rather than mutating it in place.
## Rollback workflow
The current canonical rollback workflow also now has two validated layers.
### Declaration/deployment rollback
You can still roll back by redeploying the earlier declaration:
1. retain the earlier declaration that produced the known-good closure
2. rebuild or rematerialize that earlier declaration
3. redeploy or reboot that earlier artifact again
Concretely, the usual declaration-level rollback choices are:
- rebuild the earlier declaration with `fruix system build` and confirm the old closure path reappears
- boot the earlier declaration again through `fruix system image`
- reinstall the earlier declaration through `fruix system install`, `installer`, or `installer-iso` if the deployment medium itself must change
### Installed-system generation rollback
When an installed target already has both the current and rollback generations recorded:
1. run `fruix system rollback`
2. confirm the staged state with `fruix system status`
3. reboot into the rollback generation
4. validate the restored active closure after reboot
This installed-system rollback path is now validated on local `QEMU/UEFI/TCG`.
### Important scope note
This is still not yet the same thing as Guix's full `reconfigure`/generation UX.
Current installed-system rollback is intentionally modest:
- it switches between already-recorded generations on the target
- it does not yet fetch candidate closures onto the machine for you
- it does not yet expose a richer history-management or generation-pruning policy
Still pending:
- operator-facing closure transfer or fetch onto installed systems
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
- a fuller `reconfigure`-style installed-system UX
## Provenance and deployment identity
For any serious deployment or rollback decision, the canonical identity is not merely the host name. It is the emitted metadata:
- `closure_path`
- declared FreeBSD base/source metadata
- materialized source store paths
- install metadata at `/var/lib/fruix/install.scm`
- store item counts and reference lists
Operators should retain metadata from successful candidate and current deployments because Fruix already emits enough data to answer:
- which declaration was built
- which closure booted
- which source snapshot was materialized
- which target device or image was installed
## Current limitations
The deployment workflow is now coherent, and Fruix now has a validated installed-system switch/rollback path, but it is still not the final generation-management story.
Not yet first-class:
- host-side closure transfer/fetch onto installed systems as part of `fruix system switch`
- a fuller `reconfigure` workflow that builds and stages the new closure from inside the target environment
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
- generation pruning and retention policy independent of full redeploy
Those are the next logical steps after the current explicit-generation switch/rollback model.
## Summary
The current canonical Fruix deployment model is:
- **declare** a system in Scheme
- **build** the closure with `fruix system build`
- **materialize** the artifact appropriate to the deployment target
- **boot or install** that artifact
- **identify deployments by closure path and provenance metadata**
- on installed systems, **switch** to a staged candidate with `fruix system switch`
- on installed systems, **roll back** to the recorded rollback generation with `fruix system rollback`
- still use declaration/redeploy rollback when the target does not already have the desired closure staged locally
That is the operator-facing workflow Fruix should document and use while its installed-system generation UX remains simpler than Guix's mature in-place system-generation workflow.

View File

@@ -1,7 +1,32 @@
(define-module (fruix packages freebsd)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-13)
#:export (freebsd-release
freebsd-source
freebsd-source?
freebsd-source-name
freebsd-source-kind
freebsd-source-url
freebsd-source-path
freebsd-source-ref
freebsd-source-commit
freebsd-source-sha256
%default-freebsd-source
freebsd-base
freebsd-base?
freebsd-base-name
freebsd-base-version-label
freebsd-base-release
freebsd-base-branch
freebsd-base-source-root
freebsd-base-source
freebsd-base-target
freebsd-base-target-arch
freebsd-base-kernconf
freebsd-base-make-flags
%default-freebsd-base
freebsd-package
freebsd-package?
freebsd-package-name
freebsd-package-version
@@ -15,6 +40,11 @@
freebsd-kernel
freebsd-kernel-headers
freebsd-libc
freebsd-bootloader
freebsd-rc-scripts
freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-clang-toolchain
freebsd-gmake
@@ -23,8 +53,30 @@
freebsd-zlib
freebsd-sh
freebsd-bash
freebsd-native-kernel
freebsd-native-world
freebsd-native-runtime
freebsd-native-bootloader
freebsd-native-headers
freebsd-native-kernel-for
freebsd-native-world-for
freebsd-native-runtime-for
freebsd-native-bootloader-for
freebsd-native-headers-for
freebsd-native-system-packages-for
freebsd-native-development-profile-packages-for
freebsd-native-build-package?
freebsd-host-staged-package?
%freebsd-native-system-packages
%freebsd-native-development-profile-packages
%freebsd-host-staged-all-packages
%freebsd-host-staged-core-packages
%freebsd-host-staged-development-profile-packages
%freebsd-host-staged-system-packages
%freebsd-host-staged-replacement-order
%freebsd-core-packages
%freebsd-development-profile-packages))
%freebsd-development-profile-packages
%freebsd-system-packages))
(define-record-type <freebsd-package>
(make-freebsd-package name version build-system inputs home-page synopsis
@@ -47,6 +99,83 @@
(define freebsd-release "15.0-STABLE")
(define-record-type <freebsd-source>
(make-freebsd-source name kind url path ref commit sha256)
freebsd-source?
(name freebsd-source-name)
(kind freebsd-source-kind)
(url freebsd-source-url)
(path freebsd-source-path)
(ref freebsd-source-ref)
(commit freebsd-source-commit)
(sha256 freebsd-source-sha256))
(define* (freebsd-source #:key
(name "default")
(kind 'local-tree)
(url (and (eq? kind 'git) "https://git.FreeBSD.org/src.git"))
(path (and (eq? kind 'local-tree) "/usr/src"))
(ref #f)
(commit #f)
(sha256 #f))
(make-freebsd-source name kind url path ref commit sha256))
(define %default-freebsd-source
(freebsd-source))
(define-record-type <freebsd-base>
(make-freebsd-base name version-label release branch source-root source target
target-arch kernconf make-flags)
freebsd-base?
(name freebsd-base-name)
(version-label freebsd-base-version-label)
(release freebsd-base-release)
(branch freebsd-base-branch)
(source-root freebsd-base-source-root)
(source freebsd-base-source)
(target freebsd-base-target)
(target-arch freebsd-base-target-arch)
(kernconf freebsd-base-kernconf)
(make-flags freebsd-base-make-flags))
(define default-native-make-flags
'("__MAKE_CONF=/dev/null"
"SRCCONF=/dev/null"
"SRC_ENV_CONF=/dev/null"
"MK_DEBUG_FILES=no"
"MK_TESTS=no"))
(define (default-freebsd-branch release)
(let ((major (car (string-split release #\.))))
(cond
((string-contains release "STABLE")
(string-append "stable/" major))
((string-contains release "RELEASE")
(string-append "releng/" major))
(else
"unknown"))))
(define* (freebsd-base #:key
(name "default")
(version-label freebsd-release)
(release freebsd-release)
(branch (default-freebsd-branch release))
(source #f)
(source-root #f)
(target "amd64")
(target-arch "amd64")
(kernconf "GENERIC")
(make-flags default-native-make-flags))
(let* ((source (or source %default-freebsd-source))
(source-root (or source-root
(freebsd-source-path source)
"/usr/src")))
(make-freebsd-base name version-label release branch source-root source target
target-arch kernconf make-flags)))
(define %default-freebsd-base
(freebsd-base))
(define freebsd-kernel
(freebsd-package
#:name "freebsd-kernel"
@@ -91,8 +220,69 @@ and the userland C headers needed for development profiles."
#:install-plan
'((file "/lib/libc.so.7" "lib/libc.so.7")
(file "/lib/libsys.so.7" "lib/libsys.so.7")
(file "/lib/libthr.so.3" "lib/libthr.so.3")
(file "/lib/libutil.so.10" "lib/libutil.so.10")
(file "/lib/libxo.so.0" "lib/libxo.so.0")
(file "/lib/libgeom.so.5" "lib/libgeom.so.5")
(file "/lib/libc++.so.1" "lib/libc++.so.1")
(file "/lib/libcxxrt.so.1" "lib/libcxxrt.so.1")
(file "/lib/libgcc_s.so.1" "lib/libgcc_s.so.1")
(file "/lib/libm.so.5" "lib/libm.so.5")
(file "/lib/libelf.so.2" "lib/libelf.so.2")
(file "/lib/libkvm.so.7" "lib/libkvm.so.7")
(file "/lib/lib80211.so.1" "lib/lib80211.so.1")
(file "/lib/libjail.so.1" "lib/libjail.so.1")
(file "/lib/libnv.so.1" "lib/libnv.so.1")
(file "/lib/libsbuf.so.6" "lib/libsbuf.so.6")
(file "/lib/libbsdxml.so.4" "lib/libbsdxml.so.4")
(file "/lib/libcrypt.so.5" "lib/libcrypt.so.5")
(file "/lib/libmd.so.7" "lib/libmd.so.7")
(file "/lib/libedit.so.8" "lib/libedit.so.8")
(file "/lib/libtinfow.so.9" "lib/libtinfow.so.9")
(file "/lib/libcasper.so.1" "lib/libcasper.so.1")
(file "/lib/libcap_syslog.so.1" "lib/libcap_syslog.so.1")
(file "/lib/libcap_fileargs.so.1" "lib/libcap_fileargs.so.1")
(file "/lib/libcap_net.so.1" "lib/libcap_net.so.1")
(file "/lib/libufs.so.8" "lib/libufs.so.8")
(file "/usr/lib/libdevinfo.so.7" "usr/lib/libdevinfo.so.7")
(file "/usr/lib/libdevctl.so.5" "usr/lib/libdevctl.so.5")
(file "/lib/libz.so.6" "lib/libz.so.6")
(file "/lib/libcrypto.so.35" "lib/libcrypto.so.35")
(file "/usr/lib/libssl.so.35" "usr/lib/libssl.so.35")
(file "/usr/lib/libdl.so.1" "usr/lib/libdl.so.1")
(file "/usr/lib/libpam.so.6" "usr/lib/libpam.so.6")
(file "/usr/lib/libbsm.so.3" "usr/lib/libbsm.so.3")
(file "/usr/lib/libblocklist.so.0" "usr/lib/libblocklist.so.0")
(file "/usr/lib/libregex.so.1" "usr/lib/libregex.so.1")
(file "/usr/lib/libprivatessh.so.5" "usr/lib/libprivatessh.so.5")
(file "/usr/lib/libprivateldns.so.5" "usr/lib/libprivateldns.so.5")
(file "/usr/lib/libwrap.so.6" "usr/lib/libwrap.so.6")
(file "/usr/lib/libgssapi_krb5.so.122" "usr/lib/libgssapi_krb5.so.122")
(file "/usr/lib/libkrb5.so.122" "usr/lib/libkrb5.so.122")
(file "/usr/lib/libk5crypto.so.122" "usr/lib/libk5crypto.so.122")
(file "/usr/lib/libcom_err.so.122" "usr/lib/libcom_err.so.122")
(file "/usr/lib/libkrb5support.so.122" "usr/lib/libkrb5support.so.122")
(file "/libexec/ld-elf.so.1" "libexec/ld-elf.so.1"))))
(define freebsd-bootloader
(freebsd-package
#:name "freebsd-bootloader"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for FreeBSD loader and boot assets"
#:description
"Prototype package definition that stages the FreeBSD boot loader and the
minimal loader support tree needed for declarative system-closure experiments."
#:license 'bsd-2
#:install-plan
'((file "/boot/loader" "boot/loader")
(file "/boot/loader.efi" "boot/loader.efi")
(file "/boot/device.hints" "boot/device.hints")
(directory "/boot/defaults" "boot/defaults")
(directory "/boot/lua" "boot/lua"))))
(define freebsd-sh
(freebsd-package
#:name "freebsd-sh"
@@ -122,18 +312,161 @@ userland commands needed for development and build experiments."
#:license 'bsd-2
#:install-plan
'((file "/bin/cat" "bin/cat")
(file "/bin/chflags" "bin/chflags")
(file "/bin/chmod" "bin/chmod")
(file "/bin/cp" "bin/cp")
(file "/bin/date" "bin/date")
(file "/bin/dd" "bin/dd")
(file "/bin/echo" "bin/echo")
(file "/bin/expr" "bin/expr")
(file "/bin/ln" "bin/ln")
(file "/bin/ls" "bin/ls")
(file "/bin/mkdir" "bin/mkdir")
(file "/bin/mv" "bin/mv")
(file "/bin/ps" "bin/ps")
(file "/bin/pwd" "bin/pwd")
(file "/bin/rmdir" "bin/rmdir")
(file "/bin/rm" "bin/rm")
(file "/bin/sleep" "bin/sleep")
(file "/bin/stty" "bin/stty")
(file "/bin/sync" "bin/sync")
(file "/usr/bin/awk" "usr/bin/awk")
(file "/usr/bin/basename" "usr/bin/basename")
(file "/usr/bin/cap_mkdb" "usr/bin/cap_mkdb")
(file "/usr/bin/cut" "usr/bin/cut")
(file "/usr/bin/dirname" "usr/bin/dirname")
(file "/usr/bin/egrep" "usr/bin/egrep")
(file "/usr/bin/env" "usr/bin/env")
(file "/usr/bin/find" "bin/find")
(file "/usr/bin/fsync" "usr/bin/fsync")
(file "/usr/bin/grep" "usr/bin/grep")
(file "/usr/bin/mktemp" "usr/bin/mktemp")
(file "/usr/bin/head" "usr/bin/head")
(file "/usr/bin/install" "usr/bin/install")
(file "/usr/bin/limits" "usr/bin/limits")
(file "/usr/bin/logger" "usr/bin/logger")
(file "/usr/bin/readlink" "usr/bin/readlink")
(file "/usr/bin/sed" "usr/bin/sed")
(file "/usr/bin/sort" "usr/bin/sort")
(file "/usr/bin/tar" "bin/tar")
(file "/usr/bin/tr" "usr/bin/tr")
(file "/usr/bin/uname" "usr/bin/uname")
(file "/usr/bin/xargs" "bin/xargs"))))
(define freebsd-rc-scripts
(freebsd-package
#:name "freebsd-rc-scripts"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-sh)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for FreeBSD init and rc script assets"
#:description
"Prototype package definition that stages the FreeBSD init and rc support
files needed by the first Fruix system-closure experiments."
#:license 'bsd-2
#:install-plan
'((file "/etc/rc" "etc/rc")
(file "/etc/rc.subr" "etc/rc.subr")
(file "/etc/rc.shutdown" "etc/rc.shutdown")
(file "/etc/devd.conf" "etc/devd.conf")
(file "/etc/network.subr" "etc/network.subr")
(file "/etc/newsyslog.conf" "etc/newsyslog.conf")
(file "/etc/syslog.conf" "etc/syslog.conf")
(directory "/etc/rc.d" "etc/rc.d")
(directory "/etc/defaults" "etc/defaults"))))
(define freebsd-runtime
(freebsd-package
#:name "freebsd-runtime"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc freebsd-sh freebsd-userland freebsd-rc-scripts)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for the minimal FreeBSD runtime"
#:description
"Prototype package definition that stages the minimal FreeBSD runtime
commands needed by the first declarative Fruix system and activation payload
experiments."
#:license 'bsd-2
#:install-plan
'((file "/sbin/adjkerntz" "sbin/adjkerntz")
(file "/usr/sbin/daemon" "usr/sbin/daemon")
(file "/sbin/devd" "sbin/devd")
(file "/sbin/devmatch" "sbin/devmatch")
(file "/sbin/dmesg" "sbin/dmesg")
(file "/sbin/fsck" "sbin/fsck")
(file "/sbin/fsck_ufs" "sbin/fsck_ufs")
(file "/sbin/gpart" "sbin/gpart")
(file "/sbin/init" "sbin/init")
(file "/sbin/ifconfig" "sbin/ifconfig")
(file "/sbin/md5" "sbin/md5")
(file "/sbin/mount" "sbin/mount")
(file "/sbin/rcorder" "sbin/rcorder")
(file "/sbin/reboot" "sbin/reboot")
(file "/sbin/sha256" "sbin/sha256")
(file "/sbin/shutdown" "sbin/shutdown")
(file "/sbin/swapon" "sbin/swapon")
(file "/sbin/sysctl" "sbin/sysctl")
(file "/usr/sbin/chown" "usr/sbin/chown")
(file "/usr/sbin/cron" "usr/sbin/cron")
(file "/usr/sbin/devctl" "usr/sbin/devctl")
(file "/usr/sbin/nologin" "usr/sbin/nologin")
(file "/usr/sbin/pwd_mkdb" "usr/sbin/pwd_mkdb")
(file "/usr/sbin/service" "usr/sbin/service")
(file "/usr/sbin/ip6addrctl" "usr/sbin/ip6addrctl")
(file "/usr/sbin/newsyslog" "usr/sbin/newsyslog")
(file "/usr/sbin/syslogd" "usr/sbin/syslogd")
(file "/usr/sbin/utx" "usr/sbin/utx")
(file "/usr/bin/id" "usr/bin/id")
(file "/sbin/kldload" "sbin/kldload")
(file "/sbin/kldstat" "sbin/kldstat")
(file "/sbin/devfs" "sbin/devfs")
(file "/bin/freebsd-version" "bin/freebsd-version")
(file "/bin/hostname" "bin/hostname")
(file "/bin/kenv" "bin/kenv")
(file "/usr/share/locale/C.UTF-8/LC_CTYPE" "usr/share/locale/C.UTF-8/LC_CTYPE"))))
(define freebsd-networking
(freebsd-package
#:name "freebsd-networking"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc freebsd-runtime freebsd-sh)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for FreeBSD network runtime tools"
#:description
"Prototype package definition that stages the minimal FreeBSD networking
runtime needed for DHCP-based boot validation in virtual machines."
#:license 'bsd-2
#:install-plan
'((file "/sbin/dhclient" "sbin/dhclient")
(file "/sbin/dhclient-script" "sbin/dhclient-script")
(file "/sbin/route" "sbin/route")
(file "/usr/bin/netstat" "usr/bin/netstat")
(file "/usr/sbin/arp" "usr/sbin/arp"))))
(define freebsd-openssh
(freebsd-package
#:name "freebsd-openssh"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc freebsd-runtime freebsd-sh)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for the FreeBSD OpenSSH runtime"
#:description
"Prototype package definition that stages the FreeBSD OpenSSH server and
client tools needed for first-boot operator access on the Fruix prototype
track."
#:license 'bsd-2
#:install-plan
'((file "/usr/sbin/sshd" "usr/sbin/sshd")
(file "/usr/bin/ssh" "usr/bin/ssh")
(file "/usr/bin/ssh-keygen" "usr/bin/ssh-keygen")
(file "/usr/libexec/sshd-auth" "usr/libexec/sshd-auth")
(file "/usr/libexec/sshd-session" "usr/libexec/sshd-session")
(file "/etc/ssh/moduli" "etc/ssh/moduli"))))
(define freebsd-clang-toolchain
(freebsd-package
#:name "freebsd-clang-toolchain"
@@ -245,7 +578,163 @@ library for profile experiments."
#:install-plan
'((file "/lib/libz.so.6" "lib/libz.so.6"))))
(define %freebsd-core-packages
(define default-native-world-prune-paths
'("usr/share/doc"
"usr/share/examples"
"usr/share/info"
"usr/share/man"
"usr/tests"))
(define default-native-runtime-prune-paths
'("boot"
"rescue"
"usr/include"
"usr/lib/debug"
"usr/lib32"
"usr/obj"
"usr/src"
"usr/share/doc"
"usr/share/examples"
"usr/share/info"
"usr/share/man"
"usr/share/mk"
"usr/tests"))
(define default-native-bootloader-keep-paths
'("boot/loader"
"boot/loader.efi"
"boot/device.hints"
"boot/defaults"
"boot/lua"))
(define default-native-headers-keep-paths
'("usr/include"
"usr/share/mk"))
(define (freebsd-base-native-plan base)
`((base-name . ,(freebsd-base-name base))
(base-version-label . ,(freebsd-base-version-label base))
(base-release . ,(freebsd-base-release base))
(base-branch . ,(freebsd-base-branch base))
(base-source-name . ,(freebsd-source-name (freebsd-base-source base)))
(base-source-kind . ,(freebsd-source-kind (freebsd-base-source base)))
(base-source-url . ,(freebsd-source-url (freebsd-base-source base)))
(base-source-path . ,(freebsd-source-path (freebsd-base-source base)))
(base-source-ref . ,(freebsd-source-ref (freebsd-base-source base)))
(base-source-commit . ,(freebsd-source-commit (freebsd-base-source base)))
(base-source-sha256 . ,(freebsd-source-sha256 (freebsd-base-source base)))
(source-root . ,(freebsd-base-source-root base))
(target . ,(freebsd-base-target base))
(target-arch . ,(freebsd-base-target-arch base))
(kernconf . ,(freebsd-base-kernconf base))
(make-flags . ,(freebsd-base-make-flags base))))
(define (freebsd-native-plan base extra-fields)
(append (freebsd-base-native-plan base) extra-fields))
(define (freebsd-native-kernel-for base)
(freebsd-package
#:name "freebsd-native-kernel"
#:version (freebsd-base-version-label base)
#:build-system 'freebsd-kernel-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis "Native Fruix-managed FreeBSD kernel artifact"
#:description
"FreeBSD-specific package definition that builds a kernel from a declared
FreeBSD base input and stages the resulting boot/kernel tree as a real Fruix
store artifact."
#:license 'bsd-2
#:install-plan
(freebsd-native-plan base '())))
(define (freebsd-native-world-for base)
(freebsd-package
#:name "freebsd-native-world"
#:version (freebsd-base-version-label base)
#:build-system 'freebsd-world-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis "Native Fruix-managed FreeBSD world artifact"
#:description
"FreeBSD-specific package definition that builds and installs a broad
native world from a declared FreeBSD base input into a real Fruix store
artifact."
#:license 'bsd-2
#:install-plan
(freebsd-native-plan base
`((prune-paths . ,default-native-world-prune-paths)))))
(define (freebsd-native-runtime-for base)
(freebsd-package
#:name "freebsd-native-runtime"
#:version (freebsd-base-version-label base)
#:build-system 'freebsd-world-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis "Native Fruix-managed FreeBSD runtime slice"
#:description
"FreeBSD-specific package definition that stages a runtime-focused slice of
installworld/distribution from a declared FreeBSD base input."
#:license 'bsd-2
#:install-plan
(freebsd-native-plan base
`((prune-paths . ,default-native-runtime-prune-paths)))))
(define (freebsd-native-bootloader-for base)
(freebsd-package
#:name "freebsd-native-bootloader"
#:version (freebsd-base-version-label base)
#:build-system 'freebsd-world-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis "Native Fruix-managed FreeBSD boot asset slice"
#:description
"FreeBSD-specific package definition that stages only the loader and boot
support assets needed by the validated Fruix image path from a declared
FreeBSD base input."
#:license 'bsd-2
#:install-plan
(freebsd-native-plan base
`((keep-paths . ,default-native-bootloader-keep-paths)))))
(define (freebsd-native-headers-for base)
(freebsd-package
#:name "freebsd-native-headers"
#:version (freebsd-base-version-label base)
#:build-system 'freebsd-world-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis "Native Fruix-managed FreeBSD headers slice"
#:description
"FreeBSD-specific package definition that stages the userland header set and
build-system support files from installworld/distribution for a declared
FreeBSD base input."
#:license 'bsd-2
#:install-plan
(freebsd-native-plan base
`((keep-paths . ,default-native-headers-keep-paths)))))
(define freebsd-native-kernel
(freebsd-native-kernel-for %default-freebsd-base))
(define freebsd-native-world
(freebsd-native-world-for %default-freebsd-base))
(define freebsd-native-runtime
(freebsd-native-runtime-for %default-freebsd-base))
(define freebsd-native-bootloader
(freebsd-native-bootloader-for %default-freebsd-base))
(define freebsd-native-headers
(freebsd-native-headers-for %default-freebsd-base))
(define (freebsd-native-build-package? package)
(not (not (memq (freebsd-package-build-system package)
'(freebsd-kernel-build-system freebsd-world-build-system)))))
;; Transitional boundary: the FreeBSD base layer below is still staged by
;; copying selected artifacts from the builder host. Plan 3 keeps these
;; package sets explicit so they can be replaced incrementally by native
;; FreeBSD world/kernel build outputs in /frx/store.
(define %freebsd-host-staged-core-packages
(list freebsd-kernel
freebsd-kernel-headers
freebsd-libc
@@ -258,7 +747,7 @@ library for profile experiments."
freebsd-sh
freebsd-bash))
(define %freebsd-development-profile-packages
(define %freebsd-host-staged-development-profile-packages
(list freebsd-kernel
freebsd-kernel-headers
freebsd-libc
@@ -270,3 +759,59 @@ library for profile experiments."
freebsd-zlib
freebsd-sh
freebsd-bash))
(define %freebsd-host-staged-system-packages
(list freebsd-kernel
freebsd-bootloader
freebsd-libc
freebsd-rc-scripts
freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-sh
freebsd-bash))
(define %freebsd-host-staged-all-packages
(delete-duplicates
(append %freebsd-host-staged-core-packages
%freebsd-host-staged-development-profile-packages
%freebsd-host-staged-system-packages)))
(define (freebsd-host-staged-package? package)
(and (not (freebsd-native-build-package? package))
(any (lambda (candidate)
(string=? (freebsd-package-name candidate)
(freebsd-package-name package)))
%freebsd-host-staged-all-packages)))
(define %freebsd-host-staged-replacement-order
'((first-wave . (freebsd-kernel freebsd-bootloader))
(second-wave . (freebsd-runtime freebsd-libc freebsd-userland freebsd-rc-scripts))
(third-wave . (freebsd-networking freebsd-openssh))
(fourth-wave . (freebsd-kernel-headers freebsd-clang-toolchain))
(fifth-wave . (freebsd-gmake freebsd-autotools freebsd-openssl freebsd-zlib freebsd-sh freebsd-bash))))
(define (freebsd-native-system-packages-for base)
(list (freebsd-native-runtime-for base)))
(define (freebsd-native-development-profile-packages-for base)
(list (freebsd-native-runtime-for base)
(freebsd-native-headers-for base)
freebsd-clang-toolchain
freebsd-gmake
freebsd-autotools
freebsd-openssl
freebsd-zlib
freebsd-sh
freebsd-bash))
(define %freebsd-native-system-packages
(freebsd-native-system-packages-for %default-freebsd-base))
(define %freebsd-native-development-profile-packages
(freebsd-native-development-profile-packages-for %default-freebsd-base))
(define %freebsd-core-packages %freebsd-host-staged-core-packages)
(define %freebsd-development-profile-packages %freebsd-host-staged-development-profile-packages)
(define %freebsd-system-packages %freebsd-host-staged-system-packages)

View File

@@ -0,0 +1,87 @@
(define-module (fruix system freebsd)
#:use-module (fruix system freebsd model)
#:use-module (fruix system freebsd source)
#:use-module (fruix system freebsd executor)
#:use-module (fruix system freebsd build)
#:use-module (fruix system freebsd media)
#:re-export (user-group
user-group?
user-group-name
user-group-gid
user-group-system?
user-account
user-account?
user-account-name
user-account-uid
user-account-group
user-account-supplementary-groups
user-account-comment
user-account-home
user-account-shell
user-account-system?
file-system
file-system?
file-system-device
file-system-mount-point
file-system-type
file-system-options
file-system-needed-for-boot?
promoted-native-build-result?
promoted-native-build-result-store-path
promoted-native-build-result-metadata-file
promoted-native-build-result-metadata
promoted-native-build-result-spec
operating-system
operating-system?
operating-system-host-name
operating-system-freebsd-base
operating-system-native-build-result
operating-system-kernel
operating-system-bootloader
operating-system-base-packages
operating-system-development-packages
operating-system-build-packages
operating-system-users
operating-system-groups
operating-system-file-systems
operating-system-services
operating-system-loader-entries
operating-system-rc-conf-entries
operating-system-init-mode
operating-system-ready-marker
operating-system-root-authorized-keys
validate-operating-system
materialize-freebsd-source
native-build-executor
native-build-executor?
native-build-executor-ref
native-build-executor-kind
native-build-executor-name
native-build-executor-version
native-build-executor-properties
normalize-native-build-executor
host-native-build-executor
ssh-guest-native-build-executor
self-hosted-native-build-executor
promoted-native-build-result
promoted-native-build-result->freebsd-base
promoted-native-build-result-artifact-store
promoted-native-build-result-kernel-package
promoted-native-build-result-bootloader-package
promoted-native-build-result-base-packages
promoted-native-build-result-development-packages
operating-system-from-promoted-native-build-result
promote-native-build-result
operating-system-closure-spec
operating-system-install-spec
operating-system-image-spec
operating-system-installer-image-spec
operating-system-installer-iso-spec
installer-operating-system
materialize-operating-system
materialize-rootfs
install-operating-system
materialize-bhyve-image
materialize-installer-image
materialize-installer-iso
default-minimal-operating-system))

View File

@@ -0,0 +1,933 @@
(define-module (fruix system freebsd build)
#:use-module (fruix packages freebsd)
#:use-module (fruix system freebsd model)
#:use-module (fruix system freebsd source)
#:use-module (fruix system freebsd executor)
#:use-module (fruix system freebsd utils)
#:use-module (guix build utils)
#:use-module (ice-9 format)
#:use-module (ice-9 hash-table)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-13)
#:export (host-freebsd-provenance
promoted-native-build-result
promoted-native-build-result->freebsd-base
promoted-native-build-result-artifact-store
promoted-native-build-result-kernel-package
promoted-native-build-result-bootloader-package
promoted-native-build-result-base-packages
promoted-native-build-result-development-packages
operating-system-from-promoted-native-build-result
materialize-freebsd-package
promote-native-build-result
materialize-prefix))
(define (host-freebsd-provenance)
(let ((src-git? (file-exists? "/usr/src/.git"))
(newvers "/usr/src/sys/conf/newvers.sh"))
`((freebsd-release . ,freebsd-release)
(freebsd-version-kru . ,(or (safe-command-output "freebsd-version" "-kru") "unknown"))
(uname . ,(or (safe-command-output "uname" "-a") "unknown"))
(usr-src-path . "/usr/src")
(usr-src-git-revision . ,(or (and src-git?
(safe-command-output "git" "-C" "/usr/src" "rev-parse" "HEAD"))
"absent"))
(usr-src-git-branch . ,(or (and src-git?
(safe-command-output "git" "-C" "/usr/src" "rev-parse" "--abbrev-ref" "HEAD"))
"absent"))
(usr-src-newvers-sha256 . ,(if (file-exists? newvers)
(file-hash newvers)
"absent")))))
(define native-freebsd-build-version "1")
(define (freebsd-native-build-system? build-system)
(not (not (memq build-system '(freebsd-kernel-build-system freebsd-world-build-system)))))
(define (build-plan-ref plan key default)
(match (assoc key plan)
((_ . value) value)
(#f default)))
(define (make-flag->pair flag)
(match (string-split flag #\=)
((name value ...) (cons name (string-join value "=")))
((name) (cons name "yes"))
(_ (error (format #f "invalid make flag: ~a" flag)))))
(define (native-build-kernconf-path plan)
(or (build-plan-ref plan 'kernconf-path #f)
(string-append (build-plan-ref plan 'source-root "/usr/src")
"/sys/"
(build-plan-ref plan 'target-arch "amd64")
"/conf/"
(build-plan-ref plan 'kernconf "GENERIC"))))
(define (native-build-common-manifest plan)
(let* ((source-root (build-plan-ref plan 'source-root "/usr/src"))
(target (build-plan-ref plan 'target "amd64"))
(target-arch (build-plan-ref plan 'target-arch "amd64"))
(kernconf (build-plan-ref plan 'kernconf "GENERIC"))
(make-flags (build-plan-ref plan 'make-flags '()))
(kernconf-path (native-build-kernconf-path plan)))
(unless (file-exists? source-root)
(error (format #f "native FreeBSD source root does not exist: ~a" source-root)))
(unless (file-exists? kernconf-path)
(error (format #f "native FreeBSD kernconf does not exist: ~a" kernconf-path)))
`((build-version . ,native-freebsd-build-version)
(source-root . ,source-root)
(source-tree-identity-mode . "mtree:type,link,size,mode,sha256digest")
(source-tree-sha256 . ,(or (build-plan-ref plan 'materialized-source-tree-sha256 #f)
(native-build-source-tree-sha256 source-root)))
(target . ,target)
(target-arch . ,target-arch)
(kernconf . ,kernconf)
(kernconf-path . ,kernconf-path)
(kernconf-sha256 . ,(file-hash kernconf-path))
(make-flags . ,make-flags))))
(define (native-build-declared-base plan)
`((name . ,(build-plan-ref plan 'base-name "default"))
(version-label . ,(build-plan-ref plan 'base-version-label freebsd-release))
(release . ,(build-plan-ref plan 'base-release freebsd-release))
(branch . ,(build-plan-ref plan 'base-branch "unknown"))))
(define (native-build-declared-source plan)
`((name . ,(build-plan-ref plan 'base-source-name "default"))
(kind . ,(build-plan-ref plan 'base-source-kind 'local-tree))
(url . ,(build-plan-ref plan 'base-source-url #f))
(path . ,(build-plan-ref plan 'base-source-path #f))
(ref . ,(build-plan-ref plan 'base-source-ref #f))
(commit . ,(build-plan-ref plan 'base-source-commit #f))
(sha256 . ,(build-plan-ref plan 'base-source-sha256 #f))))
(define (native-build-materialized-source plan)
`((store-path . ,(build-plan-ref plan 'materialized-source-store #f))
(source-root . ,(build-plan-ref plan 'source-root "/usr/src"))
(info-file . ,(build-plan-ref plan 'materialized-source-info-file #f))
(tree-sha256 . ,(build-plan-ref plan 'materialized-source-tree-sha256 #f))
(cache-path . ,(build-plan-ref plan 'materialized-source-cache-path #f))
(effective-source . ((kind . ,(build-plan-ref plan 'effective-source-kind #f))
(url . ,(build-plan-ref plan 'effective-source-url #f))
(path . ,(build-plan-ref plan 'effective-source-path #f))
(ref . ,(build-plan-ref plan 'effective-source-ref #f))
(commit . ,(build-plan-ref plan 'effective-source-commit #f))
(sha256 . ,(build-plan-ref plan 'effective-source-sha256 #f))))))
(define (native-build-manifest-string package input-paths)
(let* ((plan (freebsd-package-install-plan package))
(common (native-build-common-manifest plan))
(declared-base (native-build-declared-base plan))
(declared-source (native-build-declared-source plan))
(materialized-source (native-build-materialized-source plan))
(keep-paths (build-plan-ref plan 'keep-paths '()))
(prune-paths (build-plan-ref plan 'prune-paths '())))
(string-append
"name=" (freebsd-package-name package) "\n"
"version=" (freebsd-package-version package) "\n"
"build-system=" (symbol->string (freebsd-package-build-system package)) "\n"
"inputs=" (string-join input-paths ",") "\n"
"declared-base=\n"
(object->string declared-base)
"\ndeclared-source=\n"
(object->string declared-source)
"\nmaterialized-source=\n"
(object->string materialized-source)
"\nnative-build-common=\n"
(object->string common)
"\nkeep-paths=\n"
(object->string keep-paths)
"\nprune-paths=\n"
(object->string prune-paths))))
(define (copy-build-manifest-string package input-paths)
(string-append
"name=" (freebsd-package-name package) "\n"
"version=" (freebsd-package-version package) "\n"
"build-system=" (symbol->string (freebsd-package-build-system package)) "\n"
"inputs=" (string-join input-paths ",") "\n"
"install-plan-signature=\n"
(string-join (map install-plan-signature
(freebsd-package-install-plan package))
"\n")))
(define (package-manifest-string package input-paths)
(if (freebsd-native-build-system? (freebsd-package-build-system package))
(native-build-manifest-string package input-paths)
(copy-build-manifest-string package input-paths)))
(define (current-build-jobs)
(or (getenv "FRUIX_FREEBSD_BUILD_JOBS")
(safe-command-output "sysctl" "-n" "hw.ncpu")
"1"))
(define (native-build-root common)
(string-append "/var/tmp/fruix-freebsd-native-build-"
(sha256-string (object->string common))))
(define (native-make-arguments common _build-root)
(append
(list "-C" (assoc-ref common 'source-root)
(string-append "TARGET=" (assoc-ref common 'target))
(string-append "TARGET_ARCH=" (assoc-ref common 'target-arch))
(string-append "KERNCONF=" (assoc-ref common 'kernconf)))
(assoc-ref common 'make-flags)))
(define* (make-command-string common build-root target #:key (parallel? #f) (destdir #f))
(string-join
(append
(list "env" (string-append "MAKEOBJDIRPREFIX=" build-root "/obj") "make")
(if parallel?
(list (string-append "-j" (current-build-jobs)))
'())
(native-make-arguments common build-root)
(if destdir
(list (string-append "DESTDIR=" destdir))
'())
(list target))
" "))
(define (run-command/log log-file command)
(mkdir-p (dirname log-file))
(let ((status (system* "sh" "-c" (string-append command " >" log-file " 2>&1"))))
(unless (zero? status)
(error (format #f "command failed; see ~a: ~a" log-file command)))))
(define (ensure-native-build-root common build-root)
(mkdir-p build-root)
(mkdir-p (string-append build-root "/logs"))
(mkdir-p (string-append build-root "/stamps"))
(write-file (string-append build-root "/build-parameters.scm")
(object->string common)))
(define (ensure-native-buildworld common build-root)
(let ((stamp (string-append build-root "/stamps/buildworld.done")))
(ensure-native-build-root common build-root)
(unless (file-exists? stamp)
(run-command/log (string-append build-root "/logs/buildworld.log")
(make-command-string common build-root "buildworld" #:parallel? #t))
(write-file stamp "ok\n"))))
(define (ensure-native-buildkernel common build-root)
(let ((stamp (string-append build-root "/stamps/buildkernel-" (assoc-ref common 'kernconf) ".done")))
(ensure-native-buildworld common build-root)
(unless (file-exists? stamp)
(run-command/log (string-append build-root "/logs/buildkernel-" (assoc-ref common 'kernconf) ".log")
(make-command-string common build-root "buildkernel" #:parallel? #t))
(write-file stamp "ok\n"))))
(define (prune-stage-paths stage-root paths)
(for-each (lambda (path)
(delete-path-if-exists (string-append stage-root "/" path)))
paths))
(define (select-stage-paths stage-root paths)
(let ((selected-root (string-append stage-root ".selected")))
(delete-path-if-exists selected-root)
(mkdir-p selected-root)
(for-each (lambda (path)
(let ((source (string-append stage-root "/" path))
(target (string-append selected-root "/" path)))
(unless (or (file-exists? source)
(false-if-exception (readlink source)))
(error (format #f "native stage path is missing: ~a" source)))
(copy-node source target)))
paths)
selected-root))
(define (native-build-output-metadata package common build-root stage-root)
(let ((plan (freebsd-package-install-plan package)))
`((package . ,(freebsd-package-name package))
(version . ,(freebsd-package-version package))
(declared-base . ,(native-build-declared-base plan))
(declared-source . ,(native-build-declared-source plan))
(materialized-source . ,(native-build-materialized-source plan))
(build-system . ,(freebsd-package-build-system package))
(source-root . ,(assoc-ref common 'source-root))
(source-tree-sha256 . ,(assoc-ref common 'source-tree-sha256))
(target . ,(assoc-ref common 'target))
(target-arch . ,(assoc-ref common 'target-arch))
(kernconf . ,(assoc-ref common 'kernconf))
(kernconf-path . ,(assoc-ref common 'kernconf-path))
(kernconf-sha256 . ,(assoc-ref common 'kernconf-sha256))
(make-flags . ,(assoc-ref common 'make-flags))
(keep-paths . ,(build-plan-ref plan 'keep-paths '()))
(prune-paths . ,(build-plan-ref plan 'prune-paths '()))
(build-root . ,build-root)
(stage-root . ,stage-root)
(buildworld-log . ,(string-append build-root "/logs/buildworld.log"))
(buildkernel-log . ,(string-append build-root "/logs/buildkernel-" (assoc-ref common 'kernconf) ".log"))
(install-log . ,(string-append build-root "/logs/install-" (freebsd-package-name package) ".log")))))
(define (materialize-native-freebsd-package package input-paths manifest output-path)
(let* ((plan (freebsd-package-install-plan package))
(common (native-build-common-manifest plan))
(build-root (native-build-root common))
(stage-root (string-append build-root "/stage-" (freebsd-package-name package) "-" (sha256-string manifest)))
(install-log (string-append build-root "/logs/install-" (freebsd-package-name package) ".log"))
(final-stage-root
(case (freebsd-package-build-system package)
((freebsd-world-build-system)
(ensure-native-buildworld common build-root)
(delete-path-if-exists stage-root)
(mkdir-p stage-root)
(run-command/log install-log
(string-append (make-command-string common build-root "installworld" #:destdir stage-root)
" && "
(make-command-string common build-root "distribution" #:destdir stage-root)))
(let* ((keep-paths (build-plan-ref plan 'keep-paths '()))
(selected-root (if (null? keep-paths)
stage-root
(select-stage-paths stage-root keep-paths))))
(prune-stage-paths selected-root (build-plan-ref plan 'prune-paths '()))
selected-root))
((freebsd-kernel-build-system)
(ensure-native-buildkernel common build-root)
(delete-path-if-exists stage-root)
(mkdir-p stage-root)
(run-command/log install-log
(make-command-string common build-root "installkernel" #:destdir stage-root))
stage-root)
(else
(error (format #f "unsupported native FreeBSD build system: ~a"
(freebsd-package-build-system package)))))))
(mkdir-p output-path)
(stage-tree-into-output final-stage-root output-path)
(write-file (string-append output-path "/.references")
(string-join input-paths "\n"))
(write-file (string-append output-path "/.fruix-package") manifest)
(write-file (string-append output-path "/.freebsd-native-build-info.scm")
(object->string (native-build-output-metadata package common build-root final-stage-root)))))
(define (package-with-install-plan package install-plan)
(freebsd-package
#:name (freebsd-package-name package)
#:version (freebsd-package-version package)
#:build-system (freebsd-package-build-system package)
#:inputs (freebsd-package-inputs package)
#:home-page (freebsd-package-home-page package)
#:synopsis (freebsd-package-synopsis package)
#:description (freebsd-package-description package)
#:license (freebsd-package-license package)
#:install-plan install-plan))
(define (plan-freebsd-source plan)
(freebsd-source #:name (build-plan-ref plan 'base-source-name "default")
#:kind (build-plan-ref plan 'base-source-kind 'local-tree)
#:url (build-plan-ref plan 'base-source-url #f)
#:path (build-plan-ref plan 'base-source-path #f)
#:ref (build-plan-ref plan 'base-source-ref #f)
#:commit (build-plan-ref plan 'base-source-commit #f)
#:sha256 (build-plan-ref plan 'base-source-sha256 #f)))
(define (source-cache-key source)
(sha256-string (object->string (freebsd-source-spec source))))
(define (materialize-freebsd-source/cached source store-dir source-cache)
(let* ((key (source-cache-key source))
(cached (hash-ref source-cache key #f)))
(or cached
(let ((result (materialize-freebsd-source source #:store-dir store-dir)))
(hash-set! source-cache key result)
result))))
(define (plan-with-materialized-source plan source-result)
(let* ((effective (assoc-ref source-result 'effective-source))
(overrides
`((source-root . ,(assoc-ref source-result 'source-root))
(materialized-source-store . ,(assoc-ref source-result 'source-store-path))
(materialized-source-info-file . ,(assoc-ref source-result 'source-info-file))
(materialized-source-tree-sha256 . ,(assoc-ref source-result 'source-tree-sha256))
(materialized-source-cache-path . ,(assoc-ref source-result 'cache-path))
(effective-source-kind . ,(assoc-ref effective 'kind))
(effective-source-url . ,(assoc-ref effective 'url))
(effective-source-path . ,(assoc-ref effective 'path))
(effective-source-ref . ,(assoc-ref effective 'ref))
(effective-source-commit . ,(assoc-ref effective 'commit))
(effective-source-sha256 . ,(assoc-ref effective 'sha256)))))
(append overrides plan)))
(define* (materialize-freebsd-package package store-dir cache #:optional source-cache)
(if (existing-store-package? package)
(validate-existing-store-package package)
(let* ((source-cache (or source-cache (make-hash-table)))
(input-paths (map (lambda (input)
(materialize-freebsd-package input store-dir cache source-cache))
(freebsd-package-inputs package)))
(prepared-package
(if (freebsd-native-build-package? package)
(let* ((source (plan-freebsd-source (freebsd-package-install-plan package)))
(source-result (materialize-freebsd-source/cached source store-dir source-cache))
(plan (plan-with-materialized-source (freebsd-package-install-plan package)
source-result)))
(package-with-install-plan package plan))
package))
(effective-input-paths
(if (freebsd-native-build-package? package)
(cons (build-plan-ref (freebsd-package-install-plan prepared-package)
'materialized-source-store
#f)
input-paths)
input-paths))
(effective-input-paths (filter identity effective-input-paths))
(manifest (package-manifest-string prepared-package effective-input-paths))
(cache-key (sha256-string manifest))
(cached (hash-ref cache cache-key #f)))
(if cached
cached
(let* ((display-name (string-append (freebsd-package-name prepared-package)
"-"
(freebsd-package-version prepared-package)))
(output-path (make-store-path store-dir display-name manifest
#:kind 'freebsd-package)))
(unless (file-exists? output-path)
(case (freebsd-package-build-system prepared-package)
((copy-build-system)
(mkdir-p output-path)
(for-each (lambda (entry)
(materialize-plan-entry output-path entry))
(freebsd-package-install-plan prepared-package))
(write-file (string-append output-path "/.references")
(string-join effective-input-paths "\n"))
(write-file (string-append output-path "/.fruix-package") manifest))
((freebsd-world-build-system freebsd-kernel-build-system)
(materialize-native-freebsd-package prepared-package effective-input-paths manifest output-path))
(else
(error (format #f "unsupported package build system: ~a"
(freebsd-package-build-system prepared-package))))))
(hash-set! cache cache-key output-path)
output-path)))))
(define native-build-result-promotion-version "1")
(define (native-build-result-ref data key default)
(match (assoc key data)
((_ . value) value)
(#f default)))
(define (native-build-result-executor result)
(let* ((executor (native-build-result-ref result 'executor #f))
(legacy-version (native-build-result-ref result 'executor-version "legacy")))
(cond
((native-build-executor? executor)
executor)
((string? executor)
(let ((normalized (normalize-native-build-executor executor)))
`((kind . ,(native-build-executor-kind normalized))
(name . ,(native-build-executor-name normalized))
(version . ,legacy-version)
(properties . ,(native-build-executor-properties normalized)))))
(else
(native-build-executor #:kind 'unknown
#:name "unknown"
#:version legacy-version)))))
(define (native-build-result-executor-kind result)
(native-build-executor-kind (native-build-result-executor result)))
(define (native-build-result-executor-name result)
(native-build-executor-name (native-build-result-executor result)))
(define (native-build-result-executor-version result)
(native-build-executor-version (native-build-result-executor result)))
(define (read-native-build-result result-root)
(let ((promotion-file (string-append result-root "/promotion.scm")))
(unless (file-exists? promotion-file)
(error "native build result is missing promotion.scm" result-root))
(let ((result (call-with-input-file promotion-file read)))
(unless (equal? (native-build-result-ref result 'native-build-result-version #f)
native-build-result-promotion-version)
(error "unsupported native build result promotion version" promotion-file))
result)))
(define existing-store-package-build-system 'existing-store-item-build-system)
(define (existing-store-package? package)
(eq? (freebsd-package-build-system package)
existing-store-package-build-system))
(define (existing-store-package-ref package key default)
(build-plan-ref (freebsd-package-install-plan package) key default))
(define (validate-existing-store-package package)
(let* ((store-path (existing-store-package-ref package 'store-path #f))
(required-file (existing-store-package-ref package 'required-file #f))
(metadata-file (existing-store-package-ref package 'metadata-file #f)))
(unless (and (string? store-path) (file-exists? store-path))
(error "existing-store package is missing a valid store path" package store-path))
(when metadata-file
(unless (file-exists? metadata-file)
(error "existing-store package metadata file is missing" package metadata-file)))
(when required-file
(unless (file-exists? (string-append store-path "/" required-file))
(error "existing-store package is missing required file"
package
(string-append store-path "/" required-file))))
store-path))
(define (normalize-promoted-native-build-result value)
(cond
((promoted-native-build-result? value)
value)
((string? value)
(promoted-native-build-result #:store-path value))
(else
(error "expected a promoted native build result or store path" value))))
(define (read-promoted-native-build-artifact-metadata metadata-file)
(unless (file-exists? metadata-file)
(error "promoted native build artifact metadata file is missing" metadata-file))
(let ((metadata (call-with-input-file metadata-file read)))
(unless (equal? (native-build-result-ref metadata 'native-build-object-version #f)
native-build-result-promotion-version)
(error "unsupported promoted native build object version" metadata-file))
(unless (eq? (native-build-result-ref metadata 'object-kind #f) 'artifact)
(error "promoted native build object is not an artifact" metadata-file))
metadata))
(define (promoted-native-build-result-artifact-entry result artifact-kind)
(let* ((metadata (promoted-native-build-result-metadata result))
(artifacts (native-build-result-ref metadata 'artifacts '()))
(entry (find (lambda (item)
(eq? (native-build-result-ref item 'artifact-kind #f)
artifact-kind))
artifacts)))
(unless entry
(error "promoted native build result is missing artifact entry" artifact-kind))
entry))
(define (promoted-native-build-result-artifact-store result artifact-kind)
(let* ((result (normalize-promoted-native-build-result result))
(entry (promoted-native-build-result-artifact-entry result artifact-kind))
(store-path (native-build-result-ref entry 'store-path #f))
(metadata-file (native-build-result-ref entry 'metadata-file #f))
(artifact-metadata (and metadata-file
(read-promoted-native-build-artifact-metadata metadata-file)))
(required-file (and artifact-metadata
(native-build-result-ref artifact-metadata 'required-file #f))))
(unless (and (string? store-path) (file-exists? store-path))
(error "promoted native build result is missing artifact store" artifact-kind store-path))
(when artifact-metadata
(unless (eq? (native-build-result-ref artifact-metadata 'artifact-kind #f)
artifact-kind)
(error "promoted native build artifact metadata kind mismatch"
artifact-kind
metadata-file)))
(when required-file
(unless (file-exists? (string-append store-path "/" required-file))
(error "promoted native build artifact store is missing required file"
artifact-kind
(string-append store-path "/" required-file))))
store-path))
(define* (promoted-native-build-result #:key store-path)
(unless (and (string? store-path) (file-exists? store-path))
(error "promoted native build result store path does not exist" store-path))
(let* ((metadata-file (string-append store-path "/.fruix-native-build-result.scm")))
(unless (file-exists? metadata-file)
(error "promoted native build result store is missing metadata" metadata-file))
(let* ((metadata (call-with-input-file metadata-file read))
(result (make-promoted-native-build-result store-path metadata-file metadata)))
(unless (equal? (native-build-result-ref metadata 'native-build-result-version #f)
native-build-result-promotion-version)
(error "unsupported promoted native build result version" metadata-file))
(unless (eq? (native-build-result-ref metadata 'object-kind #f) 'result-bundle)
(error "promoted native build result store does not contain a result bundle" metadata-file))
(for-each (lambda (artifact-kind)
(promoted-native-build-result-artifact-store result artifact-kind))
'(world kernel headers bootloader))
result)))
(define (promoted-native-build-result->freebsd-base result)
(let* ((result (normalize-promoted-native-build-result result))
(metadata (promoted-native-build-result-metadata result))
(base (native-build-result-ref metadata 'freebsd-base '()))
(source (native-build-result-ref metadata 'source '()))
(source-root (or (native-build-result-ref source 'source-root #f)
(native-build-result-ref base 'source-root #f)
"/usr/src"))
(source-name (string-append "promoted-native-build-result-source-"
(path-basename
(promoted-native-build-result-store-path result))))
(synthetic-source (freebsd-source #:name source-name
#:kind 'local-tree
#:path source-root)))
(freebsd-base #:name (native-build-result-ref base 'name "promoted-native-build-result")
#:version-label (native-build-result-ref base 'version-label "unknown")
#:release (native-build-result-ref base 'release "unknown")
#:branch (native-build-result-ref base 'branch "unknown")
#:source synthetic-source
#:source-root (native-build-result-ref base 'source-root source-root)
#:target (native-build-result-ref base 'target "amd64")
#:target-arch (native-build-result-ref base 'target-arch "amd64")
#:kernconf (native-build-result-ref base 'kernconf "GENERIC"))))
(define (promoted-native-build-result-artifact-package result artifact-kind
package-name synopsis description)
(let* ((result (normalize-promoted-native-build-result result))
(metadata (promoted-native-build-result-metadata result))
(base (native-build-result-ref metadata 'freebsd-base '()))
(entry (promoted-native-build-result-artifact-entry result artifact-kind))
(store-path (promoted-native-build-result-artifact-store result artifact-kind))
(metadata-file (native-build-result-ref entry 'metadata-file #f))
(artifact-metadata (and metadata-file
(read-promoted-native-build-artifact-metadata metadata-file)))
(required-file (and artifact-metadata
(native-build-result-ref artifact-metadata 'required-file #f))))
(freebsd-package
#:name package-name
#:version (native-build-result-ref base 'version-label "unknown")
#:build-system existing-store-package-build-system
#:home-page "https://www.freebsd.org/"
#:synopsis synopsis
#:description description
#:license 'bsd-2
#:install-plan `((store-path . ,store-path)
(metadata-file . ,metadata-file)
(required-file . ,required-file)
(artifact-kind . ,artifact-kind)
(result-store . ,(promoted-native-build-result-store-path result))))))
(define (promoted-native-build-result-world-package result)
(promoted-native-build-result-artifact-package
result
'world
"freebsd-promoted-world"
"Promoted Fruix-native FreeBSD world artifact"
"FreeBSD world artifact imported from a promoted Fruix native-build result bundle."))
(define (promoted-native-build-result-kernel-package result)
(promoted-native-build-result-artifact-package
result
'kernel
"freebsd-promoted-kernel"
"Promoted Fruix-native FreeBSD kernel artifact"
"FreeBSD kernel artifact imported from a promoted Fruix native-build result bundle."))
(define (promoted-native-build-result-bootloader-package result)
(promoted-native-build-result-artifact-package
result
'bootloader
"freebsd-promoted-bootloader"
"Promoted Fruix-native FreeBSD bootloader artifact"
"FreeBSD bootloader artifact imported from a promoted Fruix native-build result bundle."))
(define (promoted-native-build-result-headers-package result)
(promoted-native-build-result-artifact-package
result
'headers
"freebsd-promoted-headers"
"Promoted Fruix-native FreeBSD headers artifact"
"FreeBSD headers artifact imported from a promoted Fruix native-build result bundle."))
(define (promoted-native-build-result-base-packages result)
(list (promoted-native-build-result-world-package result)))
(define (promoted-native-build-result-development-packages result)
(list (promoted-native-build-result-headers-package result)))
(define* (operating-system-from-promoted-native-build-result result
#:key
(host-name #f)
(freebsd-base #f)
(kernel #f)
(bootloader #f)
(base-packages #f)
(development-packages #f)
(users #f)
(groups #f)
(file-systems #f)
(services #f)
(loader-entries #f)
(rc-conf-entries #f)
(init-mode #f)
(ready-marker #f)
(root-authorized-keys #f))
(let* ((result (normalize-promoted-native-build-result result))
(defaults default-minimal-operating-system)
(fallback (lambda (value thunk)
(if (eq? value #f) (thunk) value))))
(operating-system
#:host-name (fallback host-name (lambda () (operating-system-host-name defaults)))
#:freebsd-base (fallback freebsd-base (lambda ()
(promoted-native-build-result->freebsd-base result)))
#:native-build-result result
#:kernel (fallback kernel (lambda ()
(promoted-native-build-result-kernel-package result)))
#:bootloader (fallback bootloader (lambda ()
(promoted-native-build-result-bootloader-package result)))
#:base-packages (fallback base-packages (lambda ()
(promoted-native-build-result-base-packages result)))
#:development-packages (fallback development-packages (lambda ()
(operating-system-development-packages defaults)))
#:users (fallback users (lambda () (operating-system-users defaults)))
#:groups (fallback groups (lambda () (operating-system-groups defaults)))
#:file-systems (fallback file-systems (lambda () (operating-system-file-systems defaults)))
#:services (fallback services (lambda () (operating-system-services defaults)))
#:loader-entries (fallback loader-entries (lambda () (operating-system-loader-entries defaults)))
#:rc-conf-entries (fallback rc-conf-entries (lambda () (operating-system-rc-conf-entries defaults)))
#:init-mode (fallback init-mode (lambda () (operating-system-init-mode defaults)))
#:ready-marker (fallback ready-marker (lambda () (operating-system-ready-marker defaults)))
#:root-authorized-keys (fallback root-authorized-keys (lambda ()
(operating-system-root-authorized-keys defaults))))))
(define (native-build-artifact-entry result artifact-kind)
(let* ((artifacts (native-build-result-ref result 'artifacts '()))
(entry (assoc artifact-kind artifacts)))
(unless entry
(error "native build result is missing artifact entry" artifact-kind))
(cdr entry)))
(define (native-build-artifact-root result-root result artifact-kind)
(let* ((entry (native-build-artifact-entry result artifact-kind))
(relative-path (native-build-result-ref entry 'path #f))
(required-file (native-build-result-ref entry 'required-file #f))
(artifact-root (and relative-path
(string-append result-root "/" relative-path))))
(unless (and artifact-root (file-exists? artifact-root))
(error "native build result is missing artifact tree" artifact-kind artifact-root))
(when required-file
(unless (file-exists? (string-append artifact-root "/" required-file))
(error "native build artifact is missing required file"
artifact-kind
(string-append artifact-root "/" required-file))))
artifact-root))
(define (native-build-existing-store-references result store-dir)
(filter identity
(map (lambda (path)
(and (string? path)
(string-prefix? (string-append store-dir "/") path)
(file-exists? path)
path))
(list (native-build-result-ref result 'closure-path #f)
(let ((source (native-build-result-ref result 'source '())))
(native-build-result-ref source 'store-path #f))))))
(define (native-build-artifact-display-name result artifact-kind)
(let* ((base (native-build-result-ref result 'freebsd-base '()))
(version-label (native-build-result-ref base 'version-label "unknown"))
(executor-name (native-build-result-executor-name result)))
(string-append "fruix-native-"
(symbol->string artifact-kind)
"-"
version-label
"-"
executor-name)))
(define (native-build-promoted-artifact-metadata result artifact-kind content-signature)
(let* ((entry (native-build-artifact-entry result artifact-kind))
(executor (native-build-result-executor result))
(build-profile (native-build-result-ref result 'build-profile
(native-build-result-ref result 'development-profile ""))))
`((native-build-object-version . ,native-build-result-promotion-version)
(object-kind . artifact)
(artifact-kind . ,artifact-kind)
(executor . ,executor)
(executor-kind . ,(native-build-result-executor-kind result))
(executor-name . ,(native-build-result-executor-name result))
(executor-version . ,(native-build-result-executor-version result))
(run-id . ,(native-build-result-ref result 'run-id "unknown"))
(guest-host-name . ,(native-build-result-ref result 'guest-host-name "unknown"))
(closure-path . ,(native-build-result-ref result 'closure-path ""))
(development-profile . ,(native-build-result-ref result 'development-profile ""))
(build-profile . ,build-profile)
(freebsd-base . ,(native-build-result-ref result 'freebsd-base '()))
(source . ,(native-build-result-ref result 'source '()))
(build-policy . ,(native-build-result-ref result 'build-policy '()))
(required-file . ,(native-build-result-ref entry 'required-file ""))
(recorded-sha256 . ,(native-build-result-ref entry 'recorded-sha256 ""))
(content-signature . ,content-signature))))
(define (promote-native-build-artifact result-root result store-dir artifact-kind)
(let* ((artifact-root (native-build-artifact-root result-root result artifact-kind))
(content-signature (tree-content-signature artifact-root))
(metadata (native-build-promoted-artifact-metadata result artifact-kind content-signature))
(payload (object->string metadata))
(display-name (native-build-artifact-display-name result artifact-kind))
(output-path (make-store-path store-dir display-name payload
#:kind 'native-build-artifact
#:output artifact-kind))
(references (native-build-existing-store-references result store-dir)))
(unless (file-exists? output-path)
(mkdir-p output-path)
(stage-tree-into-output artifact-root output-path)
(write-file (string-append output-path "/.references")
(string-join references "\n"))
(write-file (string-append output-path "/.fruix-native-build-object.scm")
payload))
`((artifact-kind . ,artifact-kind)
(artifact-root . ,artifact-root)
(store-path . ,output-path)
(content-signature . ,content-signature)
(metadata-file . ,(string-append output-path "/.fruix-native-build-object.scm")))) )
(define (native-build-result-display-name result)
(let* ((base (native-build-result-ref result 'freebsd-base '()))
(version-label (native-build-result-ref base 'version-label "unknown"))
(executor-name (native-build-result-executor-name result)))
(string-append "fruix-native-build-result-" version-label "-" executor-name)))
(define (native-build-promoted-result-object result promoted-artifacts)
(let ((executor (native-build-result-executor result))
(build-profile (native-build-result-ref result 'build-profile
(native-build-result-ref result 'development-profile ""))))
`((native-build-result-version . ,native-build-result-promotion-version)
(object-kind . result-bundle)
(executor . ,executor)
(executor-kind . ,(native-build-result-executor-kind result))
(executor-name . ,(native-build-result-executor-name result))
(executor-version . ,(native-build-result-executor-version result))
(run-id . ,(native-build-result-ref result 'run-id "unknown"))
(guest-host-name . ,(native-build-result-ref result 'guest-host-name "unknown"))
(closure-path . ,(native-build-result-ref result 'closure-path ""))
(development-profile . ,(native-build-result-ref result 'development-profile ""))
(build-profile . ,build-profile)
(freebsd-base . ,(native-build-result-ref result 'freebsd-base '()))
(source . ,(native-build-result-ref result 'source '()))
(build-policy . ,(native-build-result-ref result 'build-policy '()))
(artifact-count . ,(length promoted-artifacts))
(artifacts . ,(map (lambda (entry)
`((artifact-kind . ,(assoc-ref entry 'artifact-kind))
(store-path . ,(assoc-ref entry 'store-path))
(content-signature . ,(assoc-ref entry 'content-signature))
(metadata-file . ,(assoc-ref entry 'metadata-file))))
promoted-artifacts)))))
(define* (promote-native-build-result result-root #:key (store-dir "/frx/store"))
(let* ((result (read-native-build-result result-root))
(promoted-artifacts (map (lambda (artifact-kind)
(promote-native-build-artifact result-root result store-dir artifact-kind))
'(world kernel headers bootloader)))
(result-object (native-build-promoted-result-object result promoted-artifacts))
(payload (object->string result-object))
(display-name (native-build-result-display-name result))
(result-store (make-store-path store-dir display-name payload
#:kind 'native-build-result))
(result-references (append (map (lambda (entry)
(assoc-ref entry 'store-path))
promoted-artifacts)
(native-build-existing-store-references result store-dir))))
(unless (file-exists? result-store)
(mkdir-p (string-append result-store "/artifacts"))
(for-each (lambda (entry)
(symlink (assoc-ref entry 'store-path)
(string-append result-store
"/artifacts/"
(symbol->string (assoc-ref entry 'artifact-kind)))))
promoted-artifacts)
(write-file (string-append result-store "/.references")
(string-join result-references "\n"))
(write-file (string-append result-store "/.fruix-native-build-result.scm")
payload))
`((result-root . ,result-root)
(executor-kind . ,(native-build-result-executor-kind result))
(executor-name . ,(native-build-result-executor-name result))
(executor-version . ,(native-build-result-executor-version result))
(result-store . ,result-store)
(result-metadata-file . ,(string-append result-store "/.fruix-native-build-result.scm"))
(artifact-store-count . ,(length promoted-artifacts))
(artifact-stores . ,(map (lambda (entry) (assoc-ref entry 'store-path)) promoted-artifacts))
(world-store . ,(assoc-ref (find (lambda (entry)
(eq? (assoc-ref entry 'artifact-kind) 'world))
promoted-artifacts)
'store-path))
(kernel-store . ,(assoc-ref (find (lambda (entry)
(eq? (assoc-ref entry 'artifact-kind) 'kernel))
promoted-artifacts)
'store-path))
(headers-store . ,(assoc-ref (find (lambda (entry)
(eq? (assoc-ref entry 'artifact-kind) 'headers))
promoted-artifacts)
'store-path))
(bootloader-store . ,(assoc-ref (find (lambda (entry)
(eq? (assoc-ref entry 'artifact-kind) 'bootloader))
promoted-artifacts)
'store-path)))))
(define (sanitize-materialized-prefix name output-path)
(cond
((string=? name "fruix-guile-extra")
(rewrite-text-file
(string-append output-path "/share/guile/site/3.0/fibers/config.scm")
'(("((getenv \"FIBERS_BUILD_DIR\")\n => (lambda (builddir) (in-vicinity builddir \".libs\")))\n (else \"/tmp/guile-gnutls-freebsd-validate-install/lib/guile/3.0/extensions\"))"
. "((getenv \"FIBERS_BUILD_DIR\")\n => (lambda (builddir) (in-vicinity builddir \".libs\")))\n ((getenv \"GUILE_EXTENSIONS_PATH\"))\n (else \"/usr/local/lib/guile/3.0/extensions\"))")))
(rewrite-text-file
(string-append output-path "/share/guile/site/3.0/gnutls.scm")
'(("\"/tmp/guile-gnutls-freebsd-validate-install/lib/guile/3.0/extensions\""
. "(or (getenv \"GUILE_EXTENSIONS_PATH\") \"/usr/local/lib/guile/3.0/extensions\")")))
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/fibers/config.go"))
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/gnutls.go")))
((string=? name "fruix-shepherd-runtime")
(rewrite-text-file
(string-append output-path "/share/guile/site/3.0/shepherd/config.scm")
'(("(define Prefix-dir \"/tmp/shepherd-freebsd-validate-install\")"
. "(define Prefix-dir \"/frx\")")
("(define %localstatedir \"/tmp/shepherd-freebsd-validate-install/var\")"
. "(define %localstatedir \"/var\")")
("(define %runstatedir \"/tmp/shepherd-freebsd-validate-install/var/run\")"
. "(define %runstatedir \"/var/run\")")
("(define %sysconfdir \"/tmp/shepherd-freebsd-validate-install/etc\")"
. "(define %sysconfdir \"/etc\")")
("(define %localedir \"/tmp/shepherd-freebsd-validate-install/share/locale\")"
. "(define %localedir \"/usr/share/locale\")")
("(define %pkglibdir \"/tmp/shepherd-freebsd-validate-install/lib/shepherd\")"
. "(define %pkglibdir \"/usr/local/lib/shepherd\")")))
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/shepherd/config.go"))))
#t)
(define prefix-materializer-version "3")
(define (prefix-manifest-string source-path extra-files)
(string-append
"prefix-materializer-version=" prefix-materializer-version "\n"
"prefix-source=" source-path "\n"
(path-signature source-path)
(if (null? extra-files)
""
(string-append
"\nextra-files=\n"
(string-join
(map (lambda (entry)
(string-append (cdr entry) "\n" (path-signature (car entry))))
extra-files)
"\n")))))
(define (copy-extra-node source destination)
(let ((kind (stat:type (lstat source))))
(mkdir-p (dirname destination))
(case kind
((symlink)
(unless (or (file-exists? destination)
(false-if-exception (readlink destination)))
(let ((target (readlink source)))
(symlink target destination)
(unless (string-prefix? "/" target)
(copy-extra-node (string-append (dirname source) "/" target)
(string-append (dirname destination) "/" target))))))
(else
(unless (file-exists? destination)
(copy-node source destination))))))
(define* (materialize-prefix source-path name version store-dir #:key (extra-files '()))
(let* ((manifest (prefix-manifest-string source-path extra-files))
(display-name (string-append name "-" version))
(output-path (make-store-path store-dir display-name manifest
#:kind 'prefix)))
(unless (file-exists? output-path)
(mkdir-p output-path)
(for-each (lambda (entry)
(copy-node (string-append source-path "/" entry)
(string-append output-path "/" entry)))
(directory-entries source-path))
(for-each (lambda (entry)
(copy-extra-node (car entry)
(string-append output-path "/" (cdr entry))))
extra-files)
(sanitize-materialized-prefix name output-path)
(write-file (string-append output-path "/.fruix-package") manifest))
output-path))

View File

@@ -0,0 +1,121 @@
(define-module (fruix system freebsd executor)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:export (native-build-executor-model-version
native-build-executor
native-build-executor?
native-build-executor-ref
native-build-executor-kind
native-build-executor-name
native-build-executor-version
native-build-executor-properties
normalize-native-build-executor
host-native-build-executor
ssh-guest-native-build-executor
self-hosted-native-build-executor))
(define native-build-executor-model-version "1")
(define (association-list? value)
(and (list? value)
(every pair? value)))
(define (executor-name kind provided-name)
(or provided-name
(symbol->string kind)))
(define* (native-build-executor #:key kind name
(version native-build-executor-model-version)
(properties '()))
(unless (symbol? kind)
(error "native build executor kind must be a symbol" kind))
(unless (string? (executor-name kind name))
(error "native build executor name must be a string" name))
(unless (string? version)
(error "native build executor version must be a string" version))
(unless (association-list? properties)
(error "native build executor properties must be an association list" properties))
`((kind . ,kind)
(name . ,(executor-name kind name))
(version . ,version)
(properties . ,properties)))
(define (native-build-executor-ref executor key default)
(match (assoc key executor)
((_ . value) value)
(#f default)))
(define (native-build-executor? value)
(and (association-list? value)
(symbol? (native-build-executor-ref value 'kind #f))
(string? (native-build-executor-ref value 'name #f))
(string? (native-build-executor-ref value 'version #f))
(association-list? (native-build-executor-ref value 'properties '()))))
(define (native-build-executor-kind executor)
(native-build-executor-ref executor 'kind 'unknown))
(define (native-build-executor-name executor)
(native-build-executor-ref executor 'name "unknown"))
(define (native-build-executor-version executor)
(native-build-executor-ref executor 'version "unknown"))
(define (native-build-executor-properties executor)
(native-build-executor-ref executor 'properties '()))
(define (legacy-executor-kind name)
(cond
((member name '("host")) 'host)
((member name '("ssh-guest" "guest-ssh" "guest-host-initiated")) 'ssh-guest)
((member name '("self-hosted" "guest-self-hosted")) 'self-hosted)
((member name '("jail")) 'jail)
((member name '("remote-builder")) 'remote-builder)
(else 'legacy)))
(define (normalize-native-build-executor value)
(cond
((native-build-executor? value)
value)
((string? value)
(native-build-executor #:kind (legacy-executor-kind value)
#:name value
#:version "legacy"))
(else
(error "unsupported native build executor representation" value))))
(define* (host-native-build-executor #:key (name "host")
host-name working-directory)
(native-build-executor
#:kind 'host
#:name name
#:properties (filter-map identity
`((host-name . ,host-name)
(working-directory . ,working-directory)))))
(define* (ssh-guest-native-build-executor #:key (name "ssh-guest")
transport orchestrator
guest-host-name guest-ip vm-id vdi-id)
(native-build-executor
#:kind 'ssh-guest
#:name name
#:properties (filter-map identity
`((transport . ,(or transport "ssh"))
(orchestrator . ,(or orchestrator "host"))
(guest-host-name . ,guest-host-name)
(guest-ip . ,guest-ip)
(vm-id . ,vm-id)
(vdi-id . ,vdi-id)))))
(define* (self-hosted-native-build-executor #:key (name "self-hosted")
helper-path helper-version
guest-host-name build-root-base result-root-base)
(native-build-executor
#:kind 'self-hosted
#:name name
#:version (or helper-version native-build-executor-model-version)
#:properties (filter-map identity
`((helper-path . ,helper-path)
(guest-host-name . ,guest-host-name)
(build-root-base . ,build-root-base)
(result-root-base . ,result-root-base)))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,446 @@
(define-module (fruix system freebsd model)
#:use-module (fruix packages freebsd)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-13)
#:export (user-group
user-group?
user-group-name
user-group-gid
user-group-system?
user-account
user-account?
user-account-name
user-account-uid
user-account-group
user-account-supplementary-groups
user-account-comment
user-account-home
user-account-shell
user-account-system?
file-system
file-system?
file-system-device
file-system-mount-point
file-system-type
file-system-options
file-system-needed-for-boot?
make-promoted-native-build-result
promoted-native-build-result?
promoted-native-build-result-store-path
promoted-native-build-result-metadata-file
promoted-native-build-result-metadata
promoted-native-build-result-spec
operating-system
operating-system?
operating-system-host-name
operating-system-freebsd-base
operating-system-native-build-result
operating-system-kernel
operating-system-bootloader
operating-system-base-packages
operating-system-development-packages
operating-system-build-packages
operating-system-users
operating-system-groups
operating-system-file-systems
operating-system-services
operating-system-loader-entries
operating-system-rc-conf-entries
operating-system-init-mode
operating-system-ready-marker
operating-system-root-authorized-keys
default-minimal-operating-system
freebsd-source-spec
freebsd-base-spec
validate-freebsd-source
validate-operating-system
pid1-init-mode?
effective-loader-entries
rc-conf-entry-value
sshd-enabled?
operating-system-generated-file-names
operating-system-closure-spec))
(define-record-type <user-group>
(make-user-group name gid system?)
user-group?
(name user-group-name)
(gid user-group-gid)
(system? user-group-system?))
(define* (user-group #:key name gid (system? #t))
(make-user-group name gid system?))
(define-record-type <user-account>
(make-user-account name uid group supplementary-groups comment home shell system?)
user-account?
(name user-account-name)
(uid user-account-uid)
(group user-account-group)
(supplementary-groups user-account-supplementary-groups)
(comment user-account-comment)
(home user-account-home)
(shell user-account-shell)
(system? user-account-system?))
(define* (user-account #:key name uid group (supplementary-groups '())
(comment "Fruix user") (home "/nonexistent")
(shell "/usr/sbin/nologin") (system? #t))
(make-user-account name uid group supplementary-groups comment home shell system?))
(define-record-type <file-system>
(make-file-system device mount-point type options needed-for-boot?)
file-system?
(device file-system-device)
(mount-point file-system-mount-point)
(type file-system-type)
(options file-system-options)
(needed-for-boot? file-system-needed-for-boot?))
(define* (file-system #:key device mount-point type (options "rw")
(needed-for-boot? #f))
(make-file-system device mount-point type options needed-for-boot?))
(define-record-type <promoted-native-build-result>
(make-promoted-native-build-result store-path metadata-file metadata)
promoted-native-build-result?
(store-path promoted-native-build-result-store-path)
(metadata-file promoted-native-build-result-metadata-file)
(metadata promoted-native-build-result-metadata))
(define (promoted-native-build-result-metadata-ref metadata key default)
(match (assoc key metadata)
((_ . value) value)
(#f default)))
(define (promoted-native-build-result-artifact-spec metadata artifact-kind)
(find (lambda (entry)
(eq? (promoted-native-build-result-metadata-ref entry 'artifact-kind #f)
artifact-kind))
(promoted-native-build-result-metadata-ref metadata 'artifacts '())))
(define (promoted-native-build-result-spec result)
(let* ((metadata (promoted-native-build-result-metadata result))
(base (promoted-native-build-result-metadata-ref metadata 'freebsd-base '()))
(source (promoted-native-build-result-metadata-ref metadata 'source '())))
`((store-path . ,(promoted-native-build-result-store-path result))
(metadata-file . ,(promoted-native-build-result-metadata-file result))
(executor-kind . ,(promoted-native-build-result-metadata-ref metadata 'executor-kind #f))
(executor-name . ,(promoted-native-build-result-metadata-ref metadata 'executor-name #f))
(executor-version . ,(promoted-native-build-result-metadata-ref metadata 'executor-version #f))
(run-id . ,(promoted-native-build-result-metadata-ref metadata 'run-id #f))
(version-label . ,(promoted-native-build-result-metadata-ref base 'version-label #f))
(release . ,(promoted-native-build-result-metadata-ref base 'release #f))
(branch . ,(promoted-native-build-result-metadata-ref base 'branch #f))
(source-store . ,(promoted-native-build-result-metadata-ref source 'store-path #f))
(source-root . ,(promoted-native-build-result-metadata-ref source 'source-root #f))
(artifact-count . ,(promoted-native-build-result-metadata-ref metadata 'artifact-count 0))
(world-store . ,(promoted-native-build-result-metadata-ref
(promoted-native-build-result-artifact-spec metadata 'world)
'store-path
#f))
(kernel-store . ,(promoted-native-build-result-metadata-ref
(promoted-native-build-result-artifact-spec metadata 'kernel)
'store-path
#f))
(headers-store . ,(promoted-native-build-result-metadata-ref
(promoted-native-build-result-artifact-spec metadata 'headers)
'store-path
#f))
(bootloader-store . ,(promoted-native-build-result-metadata-ref
(promoted-native-build-result-artifact-spec metadata 'bootloader)
'store-path
#f)))))
(define-record-type <operating-system>
(make-operating-system host-name freebsd-base native-build-result kernel bootloader
base-packages development-packages build-packages users groups file-systems
services loader-entries rc-conf-entries init-mode ready-marker
root-authorized-keys)
operating-system?
(host-name operating-system-host-name)
(freebsd-base operating-system-freebsd-base)
(native-build-result operating-system-native-build-result)
(kernel operating-system-kernel)
(bootloader operating-system-bootloader)
(base-packages operating-system-base-packages)
(development-packages operating-system-development-packages)
(build-packages operating-system-build-packages)
(users operating-system-users)
(groups operating-system-groups)
(file-systems operating-system-file-systems)
(services operating-system-services)
(loader-entries operating-system-loader-entries)
(rc-conf-entries operating-system-rc-conf-entries)
(init-mode operating-system-init-mode)
(ready-marker operating-system-ready-marker)
(root-authorized-keys operating-system-root-authorized-keys))
(define* (operating-system #:key
(host-name "fruix-freebsd")
(freebsd-base %default-freebsd-base)
(native-build-result #f)
(kernel freebsd-kernel)
(bootloader freebsd-bootloader)
(base-packages %freebsd-system-packages)
(development-packages '())
(build-packages '())
(users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f)))
(groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f)))
(file-systems (list (file-system #:device "/dev/ufs/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"
#:needed-for-boot? #f)))
(services '(shepherd ready-marker))
(loader-entries '(("autoboot_delay" . "1")
("console" . "comconsole")))
(rc-conf-entries '(("clear_tmp_enable" . "YES")
("sendmail_enable" . "NONE")
("sshd_enable" . "NO")))
(init-mode 'freebsd-init+rc.d-shepherd)
(ready-marker "/var/lib/fruix/ready")
(root-authorized-keys '()))
(make-operating-system host-name freebsd-base native-build-result kernel bootloader
base-packages development-packages build-packages users groups file-systems
services loader-entries rc-conf-entries init-mode ready-marker
root-authorized-keys))
(define default-minimal-operating-system (operating-system))
(define (package-names packages)
(map freebsd-package-name packages))
(define (freebsd-source-spec source)
`((name . ,(freebsd-source-name source))
(kind . ,(freebsd-source-kind source))
(url . ,(freebsd-source-url source))
(path . ,(freebsd-source-path source))
(ref . ,(freebsd-source-ref source))
(commit . ,(freebsd-source-commit source))
(sha256 . ,(freebsd-source-sha256 source))))
(define (freebsd-base-spec base)
`((name . ,(freebsd-base-name base))
(version-label . ,(freebsd-base-version-label base))
(release . ,(freebsd-base-release base))
(branch . ,(freebsd-base-branch base))
(source-root . ,(freebsd-base-source-root base))
(source . ,(freebsd-source-spec (freebsd-base-source base)))
(target . ,(freebsd-base-target base))
(target-arch . ,(freebsd-base-target-arch base))
(kernconf . ,(freebsd-base-kernconf base))
(make-flags . ,(freebsd-base-make-flags base))))
(define (duplicate-elements values)
(let loop ((rest values) (seen '()) (duplicates '()))
(match rest
(() (reverse duplicates))
((head . tail)
(if (member head seen)
(loop tail seen (if (member head duplicates) duplicates (cons head duplicates)))
(loop tail (cons head seen) duplicates))))))
(define (non-empty-string? value)
(and (string? value)
(not (string-null? value))))
(define (validate-freebsd-source source)
(unless (freebsd-source? source)
(error "freebsd base source must be a <freebsd-source> record"))
(let ((kind (freebsd-source-kind source)))
(unless (member kind '(local-tree git src-txz))
(error "unsupported freebsd source kind" kind))
(case kind
((local-tree)
(unless (non-empty-string? (freebsd-source-path source))
(error "local-tree freebsd source must declare a path" source)))
((git)
(unless (non-empty-string? (freebsd-source-url source))
(error "git freebsd source must declare a URL" source))
(unless (or (non-empty-string? (freebsd-source-ref source))
(non-empty-string? (freebsd-source-commit source)))
(error "git freebsd source must declare a ref or commit" source)))
((src-txz)
(unless (non-empty-string? (freebsd-source-url source))
(error "src-txz freebsd source must declare a URL" source))
(unless (non-empty-string? (freebsd-source-sha256 source))
(error "src-txz freebsd source must declare a sha256" source)))))
#t)
(define (validate-operating-system os)
(let* ((host-name (operating-system-host-name os))
(base (operating-system-freebsd-base os))
(native-build-result (operating-system-native-build-result os))
(base-packages (operating-system-base-packages os))
(development-packages (operating-system-development-packages os))
(build-packages (operating-system-build-packages os))
(users (operating-system-users os))
(groups (operating-system-groups os))
(file-systems (operating-system-file-systems os))
(user-names (map user-account-name users))
(group-names (map user-group-name groups))
(mount-points (map file-system-mount-point file-systems))
(init-mode (operating-system-init-mode os)))
(when (string-null? host-name)
(error "operating-system host-name must not be empty"))
(unless (freebsd-base? base)
(error "operating-system freebsd-base must be a <freebsd-base> record"))
(when native-build-result
(unless (promoted-native-build-result? native-build-result)
(error "operating-system native-build-result must be a <promoted-native-build-result> record")))
(unless (every freebsd-package? base-packages)
(error "operating-system base-packages must be a list of <freebsd-package> records"))
(unless (every freebsd-package? development-packages)
(error "operating-system development-packages must be a list of <freebsd-package> records"))
(unless (every freebsd-package? build-packages)
(error "operating-system build-packages must be a list of <freebsd-package> records"))
(validate-freebsd-source (freebsd-base-source base))
(let ((dups (duplicate-elements user-names)))
(unless (null? dups)
(error "duplicate user names in operating-system" dups)))
(let ((dups (duplicate-elements group-names)))
(unless (null? dups)
(error "duplicate group names in operating-system" dups)))
(unless (member "/" mount-points)
(error "operating-system must declare a root file-system"))
(unless (member "root" user-names)
(error "operating-system must declare a root user"))
(unless (member "wheel" group-names)
(error "operating-system must declare a wheel group"))
(unless (member init-mode '(freebsd-init+rc.d-shepherd shepherd-pid1))
(error "unsupported operating-system init-mode" init-mode))
#t))
(define (pid1-init-mode? os)
(eq? (operating-system-init-mode os) 'shepherd-pid1))
(define (effective-loader-entries os)
(append (if (pid1-init-mode? os)
'(("init_exec" . "/run/current-system/boot/fruix-pid1"))
'())
(operating-system-loader-entries os)))
(define (rc-conf-entry-value os key)
(let ((entry (assoc key (operating-system-rc-conf-entries os))))
(and entry (cdr entry))))
(define (sshd-enabled? os)
(let ((value (rc-conf-entry-value os "sshd_enable")))
(and value
(member (string-upcase value) '("YES" "TRUE" "1")))))
(define (operating-system-generated-file-names os)
(append
'("boot/loader.conf"
"etc/rc.conf"
"etc/fstab"
"etc/hosts"
"etc/passwd"
"etc/master.passwd"
"etc/group"
"etc/login.conf"
"etc/shells"
"etc/motd"
"etc/ttys"
"metadata/freebsd-base.scm"
"metadata/host-base-provenance.scm"
"metadata/store-layout.scm"
"metadata/system-declaration.scm"
"metadata/system-declaration-info.scm"
"metadata/system-declaration-system"
"activate"
"shepherd/init.scm"
"share/fruix/node/scripts/fruix.scm"
"usr/local/bin/fruix")
(if (operating-system-native-build-result os)
'("metadata/promoted-native-build-result.scm")
'())
(if (null? (operating-system-development-packages os))
'()
'("usr/local/bin/fruix-development-environment"))
(if (null? (operating-system-build-packages os))
'()
'("usr/local/bin/fruix-build-environment"
"usr/local/bin/fruix-self-hosted-native-build"))
(if (pid1-init-mode? os)
'("boot/fruix-pid1")
'())
(if (sshd-enabled? os)
'("etc/ssh/sshd_config")
'())
(if (null? (operating-system-root-authorized-keys os))
'()
'("root/.ssh/authorized_keys"))))
(define (operating-system-closure-spec os)
(validate-operating-system os)
`((host-name . ,(operating-system-host-name os))
(freebsd-base . ,(freebsd-base-spec (operating-system-freebsd-base os)))
(promoted-native-build-result
. ,(and (operating-system-native-build-result os)
(promoted-native-build-result-spec
(operating-system-native-build-result os))))
(kernel-package . ,(freebsd-package-name (operating-system-kernel os)))
(bootloader-package . ,(freebsd-package-name (operating-system-bootloader os)))
(base-package-count . ,(length (operating-system-base-packages os)))
(base-packages . ,(package-names (operating-system-base-packages os)))
(development-package-count . ,(length (operating-system-development-packages os)))
(development-packages . ,(package-names (operating-system-development-packages os)))
(build-package-count . ,(length (operating-system-build-packages os)))
(build-packages . ,(package-names (operating-system-build-packages os)))
(installed-system-command-surface-version . "2")
(bundled-fruix-node-cli-version . "1")
(development-environment-helper-version
. ,(if (null? (operating-system-development-packages os)) #f "1"))
(build-environment-helper-version
. ,(if (null? (operating-system-build-packages os)) #f "1"))
(self-hosted-native-build-helper-version
. ,(if (null? (operating-system-build-packages os)) #f "5"))
(user-count . ,(length (operating-system-users os)))
(users . ,(map user-account-name (operating-system-users os)))
(group-count . ,(length (operating-system-groups os)))
(groups . ,(map user-group-name (operating-system-groups os)))
(file-system-count . ,(length (operating-system-file-systems os)))
(file-systems . ,(map (lambda (fs)
`((device . ,(file-system-device fs))
(mount-point . ,(file-system-mount-point fs))
(type . ,(file-system-type fs))
(options . ,(file-system-options fs))
(needed-for-boot? . ,(file-system-needed-for-boot? fs))))
(operating-system-file-systems os)))
(services . ,(operating-system-services os))
(generated-files . ,(operating-system-generated-file-names os))
(init-mode . ,(operating-system-init-mode os))
(ready-marker . ,(operating-system-ready-marker os))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,204 @@
(define-module (fruix system freebsd source)
#:use-module (fruix packages freebsd)
#:use-module (fruix system freebsd model)
#:use-module (fruix system freebsd utils)
#:use-module (guix build utils)
#:use-module (srfi srfi-13)
#:export (materialize-freebsd-source
freebsd-source-materialization-spec))
(define freebsd-source-materializer-version "2")
(define (string-downcase* value)
(list->string (map char-downcase (string->list value))))
(define (safe-name-fragment value)
(let* ((text (if (and (string? value) (not (string-null? value))) value "source"))
(chars (map (lambda (ch)
(if (or (char-alphabetic? ch)
(char-numeric? ch)
(memv ch '(#\- #\_ #\.)))
ch
#\-))
(string->list text))))
(list->string chars)))
(define (freebsd-source-manifest source effective-source identity)
(string-append
"materializer-version=" freebsd-source-materializer-version "\n"
"declared-source=\n"
(object->string (freebsd-source-spec source))
"\neffective-source=\n"
(object->string (freebsd-source-spec effective-source))
"\nidentity=\n"
(object->string identity)))
(define (ensure-git-source-cache source cache-dir)
(let* ((url (freebsd-source-url source))
(repo-dir (string-append cache-dir "/git/"
(sha256-string (string-append "git:" url))
".git")))
(mkdir-p (dirname repo-dir))
(unless (file-exists? repo-dir)
(unless (zero? (system* "git" "init" "--quiet" "--bare" repo-dir))
(error "failed to initialize git source cache" repo-dir))
(unless (zero? (system* "git" "-C" repo-dir "remote" "add" "origin" url))
(error "failed to add git source remote" url)))
(let ((current-url (safe-command-output "git" "-C" repo-dir "remote" "get-url" "origin")))
(unless (and current-url (string=? current-url url))
(unless (zero? (system* "git" "-C" repo-dir "remote" "set-url" "origin" url))
(error "failed to update git source remote" url))))
repo-dir))
(define (resolve-git-freebsd-source source cache-dir)
(let* ((selector (or (freebsd-source-commit source)
(freebsd-source-ref source)
(error "git freebsd source requires a ref or commit" source)))
(repo-dir (ensure-git-source-cache source cache-dir)))
(unless (zero? (system* "git" "-C" repo-dir "fetch" "--quiet" "--depth" "1" "origin" selector))
(error "failed to fetch git freebsd source" selector))
(let ((resolved-commit (command-output "git" "-C" repo-dir "rev-parse" "FETCH_HEAD")))
`((cache-path . ,repo-dir)
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
#:kind 'git
#:url (freebsd-source-url source)
#:ref (freebsd-source-ref source)
#:commit resolved-commit
#:sha256 #f))
(identity . ((resolved-commit . ,resolved-commit)))
(populate-tree . ,(lambda (tree-root)
(let ((archive-path (string-append (dirname tree-root) "/git-export.tar")))
(unless (zero? (system* "git" "-C" repo-dir "archive"
"--format=tar" "-o" archive-path resolved-commit))
(error "failed to archive git freebsd source" resolved-commit))
(unless (zero? (system* "tar" "-xpf" archive-path "-C" tree-root))
(error "failed to extract archived git freebsd source" archive-path))
(delete-path-if-exists archive-path))))))))
(define (normalize-expected-sha256 source)
(let ((sha256 (freebsd-source-sha256 source)))
(and sha256 (string-downcase* sha256))))
(define (resolve-txz-freebsd-source source cache-dir)
(let* ((url (freebsd-source-url source))
(expected-sha256 (or (normalize-expected-sha256 source)
(error "src-txz freebsd source requires sha256 for materialization" source)))
(archive-path (string-append cache-dir "/archives/"
(sha256-string (string-append "txz:" url))
"-src.txz")))
(mkdir-p (dirname archive-path))
(when (file-exists? archive-path)
(let ((actual (string-downcase* (file-hash archive-path))))
(unless (string=? actual expected-sha256)
(delete-file archive-path))))
(unless (file-exists? archive-path)
(unless (zero? (system* "fetch" "-q" "-o" archive-path url))
(error "failed to download FreeBSD src.txz source" url)))
(let ((actual-sha256 (string-downcase* (file-hash archive-path))))
(unless (string=? actual-sha256 expected-sha256)
(error "downloaded src.txz hash mismatch" url expected-sha256 actual-sha256))
`((cache-path . ,archive-path)
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
#:kind 'src-txz
#:url url
#:path #f
#:ref #f
#:commit #f
#:sha256 actual-sha256))
(identity . ((archive-sha256 . ,actual-sha256)))
(populate-tree . ,(lambda (tree-root)
(unless (zero? (system* "tar" "-xpf" archive-path "-C" tree-root))
(error "failed to extract FreeBSD src.txz source" archive-path))))))))
(define (resolve-local-freebsd-source source)
(let* ((path (freebsd-source-path source))
(tree-sha256 (native-build-source-tree-sha256 path)))
`((cache-path . #f)
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
#:kind 'local-tree
#:url #f
#:path path
#:ref #f
#:commit #f
#:sha256 tree-sha256))
(identity . ((tree-sha256 . ,tree-sha256)))
(populate-tree . ,(lambda (tree-root)
(copy-tree-contents path tree-root))))))
(define (detect-materialized-source-relative-root tree-root)
(cond
((file-exists? (string-append tree-root "/Makefile"))
"tree")
((file-exists? (string-append tree-root "/usr/src/Makefile"))
"tree/usr/src")
(else
"tree")))
(define* (materialize-freebsd-source source #:key
(store-dir "/frx/store")
(cache-dir "/frx/var/cache/fruix/freebsd-source"))
(validate-freebsd-source source)
(let* ((resolution (case (freebsd-source-kind source)
((local-tree)
(resolve-local-freebsd-source source))
((git)
(resolve-git-freebsd-source source cache-dir))
((src-txz)
(resolve-txz-freebsd-source source cache-dir))
(else
(error "unsupported freebsd source kind" (freebsd-source-kind source)))))
(effective-source (assoc-ref resolution 'effective-source))
(identity (assoc-ref resolution 'identity))
(manifest (freebsd-source-manifest source effective-source identity))
(display-name (string-append "freebsd-source-"
(safe-name-fragment (freebsd-source-name source))))
(output-path (make-store-path store-dir display-name manifest
#:kind 'freebsd-source))
(info-file (string-append output-path "/.freebsd-source-info.scm"))
(cache-path (assoc-ref resolution 'cache-path))
(populate-tree (assoc-ref resolution 'populate-tree)))
(unless (file-exists? output-path)
(let* ((temp-output (string-append output-path ".tmp"))
(temp-tree-root (string-append temp-output "/tree")))
(delete-path-if-exists temp-output)
(mkdir-p temp-tree-root)
(populate-tree temp-tree-root)
(let* ((relative-root (detect-materialized-source-relative-root temp-tree-root))
(source-root (string-append output-path "/" relative-root))
(temp-source-root (string-append temp-output "/" relative-root))
(tree-sha256 (native-build-source-tree-sha256 temp-source-root)))
(write-file (string-append temp-output "/.references") "")
(write-file (string-append temp-output "/.fruix-source") manifest)
(write-file (string-append temp-output "/.freebsd-source-info.scm")
(object->string
`((materializer-version . ,freebsd-source-materializer-version)
(declared-source . ,(freebsd-source-spec source))
(effective-source . ,(freebsd-source-spec effective-source))
(identity . ,identity)
(source-store . ,output-path)
(source-root . ,source-root)
(source-tree-sha256 . ,tree-sha256)
(cache-path . ,cache-path)))))
(rename-file temp-output output-path)))
(call-with-input-file info-file
(lambda (port)
(let* ((info (read port))
(effective (assoc-ref info 'effective-source)))
`((source-store-path . ,output-path)
(source-root . ,(assoc-ref info 'source-root))
(source-info-file . ,info-file)
(source-tree-sha256 . ,(assoc-ref info 'source-tree-sha256))
(cache-path . ,(assoc-ref info 'cache-path))
(effective-source . ,effective)
(effective-commit . ,(assoc-ref effective 'commit))
(effective-sha256 . ,(assoc-ref effective 'sha256))))))))
(define (freebsd-source-materialization-spec result)
`((source-store-path . ,(assoc-ref result 'source-store-path))
(source-root . ,(assoc-ref result 'source-root))
(source-info-file . ,(assoc-ref result 'source-info-file))
(source-tree-sha256 . ,(assoc-ref result 'source-tree-sha256))
(cache-path . ,(assoc-ref result 'cache-path))
(effective-source . ,(assoc-ref result 'effective-source))))

View File

@@ -0,0 +1,304 @@
(define-module (fruix system freebsd utils)
#:use-module (guix build utils)
#:use-module (ice-9 ftw)
#:use-module (ice-9 format)
#:use-module (ice-9 match)
#:use-module (ice-9 popen)
#:use-module (ice-9 hash-table)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-13)
#:use-module (rnrs io ports)
#:export (getenv*
trim-trailing-newlines
command-output
safe-command-output
write-file
sha256-string
store-hash-string
make-store-path
file-hash
directory-entries
path-signature
tree-content-signature
install-plan-signature
native-build-source-tree-sha256
copy-regular-file
copy-node
materialize-plan-entry
delete-path-if-exists
stage-tree-into-output
string-replace-all
rewrite-text-file
delete-file-if-exists
copy-tree-contents
path-basename
read-lines
run-command
store-reference-closure
copy-store-items-into-rootfs
copy-rootfs-for-image
mktemp-directory))
(define (getenv* name default)
(or (getenv name) default))
(define (trim-trailing-newlines str)
(let loop ((len (string-length str)))
(if (and (> len 0)
(char=? (string-ref str (- len 1)) #\newline))
(loop (- len 1))
(substring str 0 len))))
(define (command-output program . args)
(let* ((port (apply open-pipe* OPEN_READ program args))
(output (get-string-all port))
(status (close-pipe port)))
(unless (zero? status)
(error (format #f "command failed: ~a ~s => ~a" program args status)))
(trim-trailing-newlines output)))
(define (safe-command-output program . args)
(false-if-exception (apply command-output program args)))
(define (write-file path content)
(mkdir-p (dirname path))
(call-with-output-file path
(lambda (port)
(display content port))))
(define (sha256-string text)
(let* ((tmp (string-append (getenv* "TMPDIR" "/tmp") "/fruix-system-hash.txt")))
(write-file tmp text)
(command-output "sha256" "-q" tmp)))
(define store-hash-visible-length 40)
(define store-hash-scheme-version "1")
(define (store-identity-field value)
(cond ((symbol? value)
(symbol->string value))
((string? value)
value)
(else
(object->string value))))
(define* (store-hash-string payload #:key (kind 'item) name (output "out"))
(let* ((identity `((scheme . "fruix-store-path")
(version . ,store-hash-scheme-version)
(kind . ,(store-identity-field kind))
(name . ,(store-identity-field (or name "")))
(output . ,(store-identity-field output))
(payload . ,payload)))
(digest (sha256-string (object->string identity))))
(string-take digest store-hash-visible-length)))
(define* (make-store-path store-dir display-name payload
#:key
(kind 'item)
name
(output "out"))
(string-append store-dir "/"
(store-hash-string payload
#:kind kind
#:name (or name display-name)
#:output output)
"-"
display-name))
(define (file-hash path)
(command-output "sha256" "-q" path))
(define (directory-entries path)
(sort (filter (lambda (entry)
(not (member entry '("." ".."))))
(scandir path))
string<?))
(define (path-signature path)
(let ((st (lstat path)))
(case (stat:type st)
((regular)
(string-append "file:" path ":" (file-hash path)))
((symlink)
(string-append "symlink:" path ":" (readlink path)))
((directory)
(string-join
(cons (string-append "directory:" path)
(apply append
(map (lambda (entry)
(list (path-signature (string-append path "/" entry))))
(directory-entries path))))
"\n"))
(else
(string-append "other:" path ":" (symbol->string (stat:type st)))))))
(define (tree-content-signature root)
(define (walk path relative)
(let ((st (lstat path)))
(case (stat:type st)
((regular)
(string-append "file:" relative ":" (file-hash path)))
((symlink)
(string-append "symlink:" relative ":" (readlink path)))
((directory)
(string-join
(cons (string-append "directory:" relative)
(apply append
(map (lambda (entry)
(let ((child-relative (if (string=? relative ".")
entry
(string-append relative "/" entry))))
(list (walk (string-append path "/" entry)
child-relative))))
(directory-entries path))))
"\n"))
(else
(string-append "other:" relative ":" (symbol->string (stat:type st)))))))
(walk root "."))
(define (install-plan-signature entry)
(match entry
(('file source target)
(string-append "file-target:" target "\n" (path-signature source)))
(('directory source target)
(string-append "directory-target:" target "\n" (path-signature source)))
(_
(error (format #f "unsupported install plan entry: ~s" entry)))))
(define (native-build-source-tree-sha256 source-root)
(let* ((mtree-output (command-output "mtree" "-c" "-k" "type,link,size,mode,sha256digest" "-p" source-root))
(stable-lines (filter (lambda (line)
(not (string-prefix? "#" line)))
(string-split mtree-output #\newline))))
(sha256-string (string-join stable-lines "\n"))))
(define (copy-regular-file source destination)
(let ((mode (stat:perms (stat source))))
(copy-file source destination)
(chmod destination mode)))
(define (copy-node source destination)
(let ((kind (stat:type (lstat source))))
(mkdir-p (dirname destination))
(case kind
((directory)
(mkdir-p destination)
(for-each (lambda (entry)
(copy-node (string-append source "/" entry)
(string-append destination "/" entry)))
(directory-entries source)))
((symlink)
(symlink (readlink source) destination))
(else
(copy-regular-file source destination)))))
(define (materialize-plan-entry output-path entry)
(match entry
(('file source target)
(copy-node source (string-append output-path "/" target)))
(('directory source target)
(copy-node source (string-append output-path "/" target)))
(_
(error (format #f "unsupported install plan entry: ~s" entry)))))
(define (clear-file-flags path)
(false-if-exception (system* "chflags" "-R" "noschg,nouchg" path)))
(define (delete-path-if-exists path)
(when (or (file-exists? path) (false-if-exception (readlink path)))
(clear-file-flags path)
(let ((kind (stat:type (lstat path))))
(case kind
((directory) (delete-file-recursively path))
(else (delete-file path))))))
(define (stage-tree-into-output stage-root output-path)
(mkdir-p output-path)
(for-each (lambda (entry)
(copy-node (string-append stage-root "/" entry)
(string-append output-path "/" entry)))
(directory-entries stage-root)))
(define (string-replace-all str old new)
(let ((old-len (string-length old)))
(let loop ((start 0) (chunks '()))
(let ((index (string-contains str old start)))
(if index
(loop (+ index old-len)
(cons new
(cons (substring str start index) chunks)))
(apply string-append
(reverse (cons (substring str start) chunks))))))))
(define (rewrite-text-file path replacements)
(when (file-exists? path)
(let* ((mode (stat:perms (stat path)))
(original (call-with-input-file path get-string-all))
(updated (fold (lambda (replacement text)
(string-replace-all text (car replacement) (cdr replacement)))
original
replacements)))
(unless (string=? original updated)
(write-file path updated)
(chmod path mode)))))
(define (delete-file-if-exists path)
(when (file-exists? path)
(delete-file path)))
(define (copy-tree-contents source-root target-root)
(mkdir-p target-root)
(for-each (lambda (entry)
(copy-node (string-append source-root "/" entry)
(string-append target-root "/" entry)))
(directory-entries source-root)))
(define (path-basename path)
(let ((parts (filter (lambda (part) (not (string-null? part)))
(string-split path #\/))))
(if (null? parts)
path
(last parts))))
(define (read-lines path)
(if (file-exists? path)
(filter (lambda (line) (not (string-null? line)))
(string-split (call-with-input-file path get-string-all) #\newline))
'()))
(define (run-command . args)
(let ((status (apply system* args)))
(unless (zero? status)
(error "command failed" args status))
#t))
(define (store-reference-closure roots)
(let ((seen (make-hash-table))
(result '()))
(define (visit item)
(unless (hash-ref seen item #f)
(hash-set! seen item #t)
(set! result (cons item result))
(for-each visit (read-lines (string-append item "/.references")))))
(for-each visit roots)
(reverse result)))
(define (copy-store-items-into-rootfs rootfs store-dir items)
(let ((store-root (string-append rootfs store-dir)))
(mkdir-p store-root)
(for-each (lambda (item)
(copy-node item (string-append store-root "/" (path-basename item))))
items)))
(define (copy-rootfs-for-image source-rootfs image-rootfs)
(when (file-exists? image-rootfs)
(delete-file-recursively image-rootfs))
(copy-node source-rootfs image-rootfs))
(define (mktemp-directory pattern)
(command-output "mktemp" "-d" pattern))

787
scripts/fruix.scm Normal file
View File

@@ -0,0 +1,787 @@
#!/tmp/guile-freebsd-validate-install/bin/guile -s
!#
(use-modules (fruix system freebsd)
(fruix packages freebsd)
(ice-9 format)
(ice-9 match)
(srfi srfi-1)
(srfi srfi-13)
(rnrs io ports))
(define (usage code)
(format (if (= code 0) #t (current-error-port))
"Usage: fruix COMMAND ...\n\
\n\
Commands:\n\
system ACTION ... Build or materialize Fruix system artifacts.\n\
source ACTION ... Fetch or snapshot declarative FreeBSD source inputs.\n\
native-build ACTION ... Promote native build results into Fruix store objects.\n\
\n\
System actions:\n\
build Materialize the Fruix system closure in /frx/store.\n\
image Materialize the Fruix disk image in /frx/store.\n\
installer Materialize a bootable Fruix installer image in /frx/store.\n\
installer-iso Materialize a bootable Fruix installer ISO in /frx/store.\n\
install Install the Fruix system onto --target PATH.\n\
rootfs Materialize a rootfs tree at --rootfs DIR or ROOTFS-DIR.\n\
\n\
System options:\n\
--system NAME Scheme variable holding the operating-system object.\n\
--store DIR Store directory to use (default: /frx/store).\n\
--disk-capacity SIZE Disk capacity for 'image', 'installer', or raw-file 'install' targets.\n\
--root-size SIZE Root filesystem size for 'image', 'installer', 'installer-iso', or 'install' (example: 6g).\n\
--target PATH Install target for 'install' (raw image file or /dev/... device).\n\
--install-target-device DEVICE\n\
Target block device used by the booted 'installer' environment.\n\
--rootfs DIR Rootfs target for 'rootfs'.\n\
\n\
Source actions:\n\
materialize Materialize a declared FreeBSD source tree in /frx/store.\n\
\n\
Native-build actions:\n\
promote Promote a native build result root into /frx/store.\n\
\n\
Native-build options:\n\
--store DIR Store directory to use (default: /frx/store).\n\
\n\
Source options:\n\
--source NAME Scheme variable holding the freebsd-source object.\n\
--store DIR Store directory to use (default: /frx/store).\n\
--cache DIR Cache directory to use (default: /frx/var/cache/fruix/freebsd-source).\n\
\n\
Common options:\n\
--help Show this help.\n")
(exit code))
(define (option-value arg prefix)
(and (string-prefix? prefix arg)
(substring arg (string-length prefix))))
(define (stringify value)
(cond ((string? value) value)
((symbol? value) (symbol->string value))
((number? value) (number->string value))
((boolean? value) (if value "true" "false"))
(else (call-with-output-string (lambda (port) (write value port))))))
(define (emit-metadata fields)
(for-each (lambda (field)
(format #t "~a=~a~%" (car field) (stringify (cdr field))))
fields))
(define (read-file-string file)
(call-with-input-file file get-string-all))
(define (lookup-bound-value module symbol)
(let ((var (module-variable module symbol)))
(and var (variable-ref var))))
(define candidate-operating-system-symbols
'(operating-system
phase16-operating-system
phase15-operating-system
phase10-operating-system
phase9-operating-system
phase8-operating-system
phase7-operating-system
default-operating-system
os))
(define candidate-freebsd-source-symbols
'(phase16-source
declared-source
source
src))
(define (resolve-operating-system-symbol module requested)
(or requested
(find (lambda (symbol)
(let ((value (lookup-bound-value module symbol)))
(and value (operating-system? value))))
candidate-operating-system-symbols)
(error "could not infer operating-system variable; use --system NAME")))
(define (resolve-freebsd-source-symbol module requested)
(or requested
(find (lambda (symbol)
(let ((value (lookup-bound-value module symbol)))
(and value (freebsd-source? value))))
candidate-freebsd-source-symbols)
(error "could not infer freebsd-source variable; use --source NAME")))
(define (load-operating-system-from-file file requested-symbol)
(unless (file-exists? file)
(error "operating-system file does not exist" file))
(primitive-load file)
(let* ((module (current-module))
(symbol (resolve-operating-system-symbol module requested-symbol))
(value (lookup-bound-value module symbol)))
(unless (and value (operating-system? value))
(error "resolved variable is not an operating-system" symbol))
(validate-operating-system value)
(values value symbol)))
(define (load-freebsd-source-from-file file requested-symbol)
(unless (file-exists? file)
(error "freebsd-source file does not exist" file))
(primitive-load file)
(let* ((module (current-module))
(symbol (resolve-freebsd-source-symbol module requested-symbol))
(value (lookup-bound-value module symbol)))
(unless (and value (freebsd-source? value))
(error "resolved variable is not a freebsd-source" symbol))
(values value symbol)))
(define (parse-system-arguments action rest)
(let loop ((args rest)
(positional '())
(system-name #f)
(store-dir "/frx/store")
(disk-capacity #f)
(root-size #f)
(target #f)
(install-target-device #f)
(rootfs #f))
(match args
(()
(let ((positional (reverse positional)))
`((command . "system")
(action . ,action)
(positional . ,positional)
(system-name . ,system-name)
(store-dir . ,store-dir)
(disk-capacity . ,disk-capacity)
(root-size . ,root-size)
(target . ,target)
(install-target-device . ,install-target-device)
(rootfs . ,rootfs))))
(("--help")
(usage 0))
(((? (lambda (arg) (string-prefix? "--system=" arg)) arg) . tail)
(loop tail positional (option-value arg "--system=") store-dir disk-capacity root-size target install-target-device rootfs))
(("--system" value . tail)
(loop tail positional value store-dir disk-capacity root-size target install-target-device rootfs))
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
(loop tail positional system-name (option-value arg "--store=") disk-capacity root-size target install-target-device rootfs))
(("--store" value . tail)
(loop tail positional system-name value disk-capacity root-size target install-target-device rootfs))
(((? (lambda (arg) (string-prefix? "--disk-capacity=" arg)) arg) . tail)
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") root-size target install-target-device rootfs))
(("--disk-capacity" value . tail)
(loop tail positional system-name store-dir value root-size target install-target-device rootfs))
(((? (lambda (arg) (string-prefix? "--root-size=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity (option-value arg "--root-size=") target install-target-device rootfs))
(("--root-size" value . tail)
(loop tail positional system-name store-dir disk-capacity value target install-target-device rootfs))
(((? (lambda (arg) (string-prefix? "--target=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity root-size (option-value arg "--target=") install-target-device rootfs))
(("--target" value . tail)
(loop tail positional system-name store-dir disk-capacity root-size value install-target-device rootfs))
(((? (lambda (arg) (string-prefix? "--install-target-device=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity root-size target
(option-value arg "--install-target-device=") rootfs))
(("--install-target-device" value . tail)
(loop tail positional system-name store-dir disk-capacity root-size target value rootfs))
(((? (lambda (arg) (string-prefix? "--rootfs=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity root-size target install-target-device
(option-value arg "--rootfs=")))
(("--rootfs" value . tail)
(loop tail positional system-name store-dir disk-capacity root-size target install-target-device value))
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
(error "unknown option" arg))
((arg . tail)
(loop tail (cons arg positional) system-name store-dir disk-capacity root-size target install-target-device rootfs)))))
(define (parse-source-arguments action rest)
(let loop ((args rest)
(positional '())
(source-name #f)
(store-dir "/frx/store")
(cache-dir "/frx/var/cache/fruix/freebsd-source"))
(match args
(()
(let ((positional (reverse positional)))
`((command . "source")
(action . ,action)
(positional . ,positional)
(source-name . ,source-name)
(store-dir . ,store-dir)
(cache-dir . ,cache-dir))))
(("--help")
(usage 0))
(((? (lambda (arg) (string-prefix? "--source=" arg)) arg) . tail)
(loop tail positional (option-value arg "--source=") store-dir cache-dir))
(("--source" value . tail)
(loop tail positional value store-dir cache-dir))
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
(loop tail positional source-name (option-value arg "--store=") cache-dir))
(("--store" value . tail)
(loop tail positional source-name value cache-dir))
(((? (lambda (arg) (string-prefix? "--cache=" arg)) arg) . tail)
(loop tail positional source-name store-dir (option-value arg "--cache=")))
(("--cache" value . tail)
(loop tail positional source-name store-dir value))
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
(error "unknown option" arg))
((arg . tail)
(loop tail (cons arg positional) source-name store-dir cache-dir)))))
(define (parse-native-build-arguments action rest)
(let loop ((args rest)
(positional '())
(store-dir "/frx/store"))
(match args
(()
(let ((positional (reverse positional)))
`((command . "native-build")
(action . ,action)
(positional . ,positional)
(store-dir . ,store-dir))))
(("--help")
(usage 0))
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
(loop tail positional (option-value arg "--store=")))
(("--store" value . tail)
(loop tail positional value))
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
(error "unknown option" arg))
((arg . tail)
(loop tail (cons arg positional) store-dir)))))
(define (parse-arguments argv)
(match argv
((_)
(usage 1))
((_ "--help")
(usage 0))
((_ "help")
(usage 0))
((_ "system" "--help")
(usage 0))
((_ "source" "--help")
(usage 0))
((_ "native-build" "--help")
(usage 0))
((_ "system" action . rest)
(parse-system-arguments action rest))
((_ "source" action . rest)
(parse-source-arguments action rest))
((_ "native-build" action . rest)
(parse-native-build-arguments action rest))
((_ . _)
(usage 1))))
(define (emit-system-build-metadata os-file resolved-symbol store-dir os result)
(let* ((closure-path (assoc-ref result 'closure-path))
(generated-files (assoc-ref result 'generated-files))
(references (assoc-ref result 'references))
(base-package-stores (assoc-ref result 'base-package-stores))
(host-base-stores (assoc-ref result 'host-base-stores))
(native-base-stores (assoc-ref result 'native-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(base (operating-system-freebsd-base os))
(source (freebsd-base-source base))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "build")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(closure_path . ,closure-path)
(freebsd_base_name . ,(freebsd-base-name base))
(freebsd_base_version_label . ,(freebsd-base-version-label base))
(freebsd_base_release . ,(freebsd-base-release base))
(freebsd_base_branch . ,(freebsd-base-branch base))
(freebsd_base_source_root . ,(freebsd-base-source-root base))
(freebsd_base_target . ,(freebsd-base-target base))
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
(ready_marker . ,(operating-system-ready-marker os))
(kernel_store . ,(assoc-ref result 'kernel-store))
(bootloader_store . ,(assoc-ref result 'bootloader-store))
(guile_store . ,(assoc-ref result 'guile-store))
(guile_extra_store . ,(assoc-ref result 'guile-extra-store))
(shepherd_store . ,(assoc-ref result 'shepherd-store))
(base_package_store_count . ,(length base-package-stores))
(base_package_stores . ,(string-join base-package-stores ","))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-base-stores ","))
(native_base_store_count . ,(length native-base-stores))
(native_base_stores . ,(string-join native-base-stores ","))
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
(store_layout_file . ,(assoc-ref result 'store-layout-file))
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
(host_uname . ,(assoc-ref host-provenance 'uname))
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
(generated_file_count . ,(length generated-files))
(reference_count . ,(length references))))))
(define (emit-system-install-metadata os-file resolved-symbol store-dir os result)
(let* ((install-spec (assoc-ref result 'install-spec))
(store-items (assoc-ref result 'store-items))
(host-base-stores (assoc-ref result 'host-base-stores))
(native-base-stores (assoc-ref result 'native-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(base (operating-system-freebsd-base os))
(source (freebsd-base-source base))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "install")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(target . ,(assoc-ref result 'target))
(target_kind . ,(assoc-ref result 'target-kind))
(target_device . ,(assoc-ref result 'target-device))
(esp_device . ,(assoc-ref result 'esp-device))
(root_device . ,(assoc-ref result 'root-device))
(install_metadata_path . ,(assoc-ref result 'install-metadata-path))
(freebsd_base_name . ,(freebsd-base-name base))
(freebsd_base_version_label . ,(freebsd-base-version-label base))
(freebsd_base_release . ,(freebsd-base-release base))
(freebsd_base_branch . ,(freebsd-base-branch base))
(freebsd_base_source_root . ,(freebsd-base-source-root base))
(freebsd_base_target . ,(freebsd-base-target base))
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
(disk_capacity . ,(assoc-ref install-spec 'disk-capacity))
(root_size . ,(assoc-ref install-spec 'root-size))
(efi_size . ,(assoc-ref install-spec 'efi-size))
(closure_path . ,(assoc-ref result 'closure-path))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-base-stores ","))
(native_base_store_count . ,(length native-base-stores))
(native_base_stores . ,(string-join native-base-stores ","))
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
(store_layout_file . ,(assoc-ref result 'store-layout-file))
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
(host_uname . ,(assoc-ref host-provenance 'uname))
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
(store_item_count . ,(length store-items))))))
(define (emit-system-image-metadata os-file resolved-symbol store-dir os result)
(let* ((image-spec (assoc-ref result 'image-spec))
(store-items (assoc-ref result 'store-items))
(host-base-stores (assoc-ref result 'host-base-stores))
(native-base-stores (assoc-ref result 'native-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(base (operating-system-freebsd-base os))
(source (freebsd-base-source base))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "image")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(freebsd_base_name . ,(freebsd-base-name base))
(freebsd_base_version_label . ,(freebsd-base-version-label base))
(freebsd_base_release . ,(freebsd-base-release base))
(freebsd_base_branch . ,(freebsd-base-branch base))
(freebsd_base_source_root . ,(freebsd-base-source-root base))
(freebsd_base_target . ,(freebsd-base-target base))
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
(disk_capacity . ,(assoc-ref image-spec 'disk-capacity))
(root_size . ,(assoc-ref image-spec 'root-size))
(image_store_path . ,(assoc-ref result 'image-store-path))
(disk_image . ,(assoc-ref result 'disk-image))
(esp_image . ,(assoc-ref result 'esp-image))
(root_image . ,(assoc-ref result 'root-image))
(closure_path . ,(assoc-ref result 'closure-path))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-base-stores ","))
(native_base_store_count . ,(length native-base-stores))
(native_base_stores . ,(string-join native-base-stores ","))
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
(store_layout_file . ,(assoc-ref result 'store-layout-file))
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
(host_uname . ,(assoc-ref host-provenance 'uname))
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
(store_item_count . ,(length store-items))))))
(define (emit-system-installer-metadata os-file resolved-symbol store-dir os result)
(let* ((installer-image-spec (assoc-ref result 'installer-image-spec))
(image-spec (assoc-ref result 'image-spec))
(store-items (assoc-ref result 'store-items))
(target-store-items (assoc-ref result 'target-store-items))
(installer-store-items (assoc-ref result 'installer-store-items))
(host-base-stores (assoc-ref result 'host-base-stores))
(native-base-stores (assoc-ref result 'native-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(base (operating-system-freebsd-base os))
(source (freebsd-base-source base))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "installer")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(freebsd_base_name . ,(freebsd-base-name base))
(freebsd_base_version_label . ,(freebsd-base-version-label base))
(freebsd_base_release . ,(freebsd-base-release base))
(freebsd_base_branch . ,(freebsd-base-branch base))
(freebsd_base_source_root . ,(freebsd-base-source-root base))
(freebsd_base_target . ,(freebsd-base-target base))
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
(disk_capacity . ,(assoc-ref image-spec 'disk-capacity))
(root_size . ,(assoc-ref image-spec 'root-size))
(installer_host_name . ,(assoc-ref installer-image-spec 'installer-host-name))
(install_target_device . ,(assoc-ref result 'install-target-device))
(installer_state_path . ,(assoc-ref result 'installer-state-path))
(installer_log_path . ,(assoc-ref result 'installer-log-path))
(image_store_path . ,(assoc-ref result 'image-store-path))
(disk_image . ,(assoc-ref result 'disk-image))
(esp_image . ,(assoc-ref result 'esp-image))
(root_image . ,(assoc-ref result 'root-image))
(installer_closure_path . ,(assoc-ref result 'installer-closure-path))
(target_closure_path . ,(assoc-ref result 'target-closure-path))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-base-stores ","))
(native_base_store_count . ,(length native-base-stores))
(native_base_stores . ,(string-join native-base-stores ","))
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
(store_layout_file . ,(assoc-ref result 'store-layout-file))
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
(host_uname . ,(assoc-ref host-provenance 'uname))
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
(store_item_count . ,(length store-items))
(target_store_item_count . ,(length target-store-items))
(installer_store_item_count . ,(length installer-store-items))))))
(define (emit-system-installer-iso-metadata os-file resolved-symbol store-dir os result)
(let* ((installer-iso-spec (assoc-ref result 'installer-iso-spec))
(store-items (assoc-ref result 'store-items))
(target-store-items (assoc-ref result 'target-store-items))
(installer-store-items (assoc-ref result 'installer-store-items))
(host-base-stores (assoc-ref result 'host-base-stores))
(native-base-stores (assoc-ref result 'native-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(base (operating-system-freebsd-base os))
(source (freebsd-base-source base))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "installer-iso")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(freebsd_base_name . ,(freebsd-base-name base))
(freebsd_base_version_label . ,(freebsd-base-version-label base))
(freebsd_base_release . ,(freebsd-base-release base))
(freebsd_base_branch . ,(freebsd-base-branch base))
(freebsd_base_source_root . ,(freebsd-base-source-root base))
(freebsd_base_target . ,(freebsd-base-target base))
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
(installer_host_name . ,(assoc-ref installer-iso-spec 'installer-host-name))
(install_target_device . ,(assoc-ref result 'install-target-device))
(iso_volume_label . ,(assoc-ref installer-iso-spec 'iso-volume-label))
(root_size . ,(assoc-ref installer-iso-spec 'root-size))
(installer_state_path . ,(assoc-ref result 'installer-state-path))
(installer_log_path . ,(assoc-ref result 'installer-log-path))
(iso_store_path . ,(assoc-ref result 'iso-store-path))
(iso_image . ,(assoc-ref result 'iso-image))
(boot_efi_image . ,(assoc-ref result 'boot-efi-image))
(root_image . ,(assoc-ref result 'root-image))
(installer_closure_path . ,(assoc-ref result 'installer-closure-path))
(target_closure_path . ,(assoc-ref result 'target-closure-path))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-base-stores ","))
(native_base_store_count . ,(length native-base-stores))
(native_base_stores . ,(string-join native-base-stores ","))
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
(store_layout_file . ,(assoc-ref result 'store-layout-file))
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
(host_uname . ,(assoc-ref host-provenance 'uname))
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
(store_item_count . ,(length store-items))
(target_store_item_count . ,(length target-store-items))
(installer_store_item_count . ,(length installer-store-items))))))
(define (emit-native-build-promotion-metadata store-dir result-root result)
(emit-metadata
`((action . "promote")
(result_root . ,result-root)
(store_dir . ,store-dir)
(executor_kind . ,(assoc-ref result 'executor-kind))
(executor_name . ,(assoc-ref result 'executor-name))
(executor_version . ,(assoc-ref result 'executor-version))
(result_store . ,(assoc-ref result 'result-store))
(result_metadata_file . ,(assoc-ref result 'result-metadata-file))
(artifact_store_count . ,(assoc-ref result 'artifact-store-count))
(artifact_stores . ,(string-join (assoc-ref result 'artifact-stores) ","))
(world_store . ,(assoc-ref result 'world-store))
(kernel_store . ,(assoc-ref result 'kernel-store))
(headers_store . ,(assoc-ref result 'headers-store))
(bootloader_store . ,(assoc-ref result 'bootloader-store)))))
(define (main argv)
(let* ((parsed (parse-arguments argv))
(command (assoc-ref parsed 'command))
(action (assoc-ref parsed 'action))
(store-dir (assoc-ref parsed 'store-dir)))
(cond
((string=? command "system")
(let* ((positional (assoc-ref parsed 'positional))
(disk-capacity (assoc-ref parsed 'disk-capacity))
(root-size (assoc-ref parsed 'root-size))
(target-opt (assoc-ref parsed 'target))
(install-target-device (assoc-ref parsed 'install-target-device))
(rootfs-opt (assoc-ref parsed 'rootfs))
(system-name (assoc-ref parsed 'system-name))
(requested-symbol (and system-name (string->symbol system-name))))
(unless (member action '("build" "image" "installer" "installer-iso" "install" "rootfs"))
(error "unknown system action" action))
(let* ((os-file (match positional
((file . _) file)
(() (error "missing operating-system file argument"))))
(target (or target-opt
(and (string=? action "install")
(match positional
((_ target-path) target-path)
(_ #f)))))
(rootfs (or rootfs-opt
(and (string=? action "rootfs")
(match positional
((_ dir) dir)
((_ _ dir . _) dir)
(_ #f))))))
(call-with-values
(lambda ()
(load-operating-system-from-file os-file requested-symbol))
(lambda (os resolved-symbol)
(let* ((guile-prefix (or (getenv "GUILE_PREFIX") "/tmp/guile-freebsd-validate-install"))
(guile-extra-prefix (or (getenv "GUILE_EXTRA_PREFIX") "/tmp/guile-gnutls-freebsd-validate-install"))
(shepherd-prefix (or (getenv "SHEPHERD_PREFIX") "/tmp/shepherd-freebsd-validate-install"))
(guile-store-path (getenv "FRUIX_GUILE_STORE"))
(guile-extra-store-path (getenv "FRUIX_GUILE_EXTRA_STORE"))
(shepherd-store-path (getenv "FRUIX_SHEPHERD_STORE"))
(declaration-source (read-file-string os-file)))
(cond
((string=? action "build")
(emit-system-build-metadata
os-file resolved-symbol store-dir os
(materialize-operating-system os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:guile-store-path guile-store-path
#:guile-extra-store-path guile-extra-store-path
#:shepherd-store-path shepherd-store-path
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol)))
((string=? action "rootfs")
(unless rootfs
(error "rootfs action requires ROOTFS-DIR or --rootfs DIR"))
(let ((result (materialize-rootfs os rootfs
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol)))
(emit-metadata
`((action . "rootfs")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(rootfs . ,(assoc-ref result 'rootfs))
(closure_path . ,(assoc-ref result 'closure-path))
(ready_marker . ,(assoc-ref result 'ready-marker))
(rc_script . ,(assoc-ref result 'rc-script))))))
((string=? action "image")
(emit-system-image-metadata
os-file resolved-symbol store-dir os
(materialize-bhyve-image os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol
#:root-size (or root-size "256m")
#:disk-capacity disk-capacity)))
((string=? action "installer")
(emit-system-installer-metadata
os-file resolved-symbol store-dir os
(materialize-installer-image os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol
#:install-target-device (or install-target-device "/dev/vtbd1")
#:root-size (or root-size "10g")
#:disk-capacity disk-capacity)))
((string=? action "installer-iso")
(emit-system-installer-iso-metadata
os-file resolved-symbol store-dir os
(materialize-installer-iso os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol
#:install-target-device (or install-target-device "/dev/vtbd0")
#:root-size root-size)))
((string=? action "install")
(unless target
(error "install action requires TARGET or --target PATH"))
(emit-system-install-metadata
os-file resolved-symbol store-dir os
(install-operating-system os
#:target target
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:declaration-source declaration-source
#:declaration-origin os-file
#:declaration-system-symbol resolved-symbol
#:root-size root-size
#:disk-capacity disk-capacity))))))))))
((string=? command "source")
(let* ((positional (assoc-ref parsed 'positional))
(cache-dir (assoc-ref parsed 'cache-dir))
(source-name (assoc-ref parsed 'source-name))
(requested-symbol (and source-name (string->symbol source-name))))
(unless (string=? action "materialize")
(error "unknown source action" action))
(let ((source-file (match positional
((file . _) file)
(() (error "missing freebsd-source file argument")))))
(call-with-values
(lambda ()
(load-freebsd-source-from-file source-file requested-symbol))
(lambda (source resolved-symbol)
(let* ((result (materialize-freebsd-source source
#:store-dir store-dir
#:cache-dir cache-dir))
(effective (assoc-ref result 'effective-source)))
(emit-metadata
`((action . "materialize")
(source_file . ,source-file)
(source_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(cache_dir . ,cache-dir)
(freebsd_source_name . ,(freebsd-source-name source))
(freebsd_source_kind . ,(freebsd-source-kind source))
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
(materialized_source_store . ,(assoc-ref result 'source-store-path))
(materialized_source_root . ,(assoc-ref result 'source-root))
(materialized_source_info_file . ,(assoc-ref result 'source-info-file))
(materialized_source_tree_sha256 . ,(assoc-ref result 'source-tree-sha256))
(materialized_source_cache_path . ,(or (assoc-ref result 'cache-path) ""))
(materialized_source_kind . ,(assoc-ref effective 'kind))
(materialized_source_url . ,(or (assoc-ref effective 'url) ""))
(materialized_source_path . ,(or (assoc-ref effective 'path) ""))
(materialized_source_ref . ,(or (assoc-ref effective 'ref) ""))
(materialized_source_commit . ,(or (assoc-ref result 'effective-commit) ""))
(materialized_source_sha256 . ,(or (assoc-ref result 'effective-sha256) ""))))))))))
((string=? command "native-build")
(let ((positional (assoc-ref parsed 'positional)))
(unless (string=? action "promote")
(error "unknown native-build action" action))
(let ((result-root (match positional
((path . _) path)
(() (error "missing native build result root argument")))))
(emit-native-build-promotion-metadata
store-dir result-root
(promote-native-build-result result-root #:store-dir store-dir)))))
(else
(usage 1)))))
(main (command-line))

View File

@@ -0,0 +1,139 @@
(use-modules (fruix system freebsd)
(ice-9 format)
(srfi srfi-13)
(rnrs io ports))
(define workdir
(or (getenv "WORKDIR")
(error "WORKDIR environment variable is required")))
(define os-file
(or (getenv "OS_FILE")
(error "OS_FILE environment variable is required")))
(define store-dir
(or (getenv "STORE_DIR")
"/frx/store"))
(define guile-prefix
(or (getenv "GUILE_PREFIX")
"/tmp/guile-freebsd-validate-install"))
(define guile-extra-prefix
(or (getenv "GUILE_EXTRA_PREFIX")
"/tmp/guile-gnutls-freebsd-validate-install"))
(define shepherd-prefix
(or (getenv "SHEPHERD_PREFIX")
"/tmp/shepherd-freebsd-validate-install"))
(define metadata-file
(string-append workdir "/phase7-rootfs-metadata.txt"))
(define rootfs
(string-append workdir "/rootfs"))
(primitive-load os-file)
(validate-operating-system phase7-operating-system)
(define (assert-exists path)
(unless (or (file-exists? path)
(false-if-exception (readlink path)))
(error "required path missing" path)))
(define (assert-symlink-target path expected)
(let ((actual (readlink path)))
(unless (string=? actual expected)
(error "unexpected symlink target" path actual expected))
actual))
(let* ((result (materialize-rootfs phase7-operating-system rootfs
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref result 'closure-path))
(ready-marker (assoc-ref result 'ready-marker))
(rc-script (assoc-ref result 'rc-script))
(run-current-system-target (assert-symlink-target (string-append rootfs "/run/current-system")
closure-path))
(activate-target (assert-symlink-target (string-append rootfs "/activate")
"/run/current-system/activate"))
(bin-target (assert-symlink-target (string-append rootfs "/bin")
"/run/current-system/profile/bin"))
(sbin-target (assert-symlink-target (string-append rootfs "/sbin")
"/run/current-system/profile/sbin"))
(lib-target (assert-symlink-target (string-append rootfs "/lib")
"/run/current-system/profile/lib"))
(boot-kernel-target (assert-symlink-target (string-append rootfs "/boot/kernel")
"/run/current-system/boot/kernel"))
(boot-loader-target (assert-symlink-target (string-append rootfs "/boot/loader")
"/run/current-system/boot/loader"))
(boot-loader-efi-target (assert-symlink-target (string-append rootfs "/boot/loader.efi")
"/run/current-system/boot/loader.efi"))
(rc-conf-target (assert-symlink-target (string-append rootfs "/etc/rc.conf")
"/run/current-system/etc/rc.conf"))
(fstab-target (assert-symlink-target (string-append rootfs "/etc/fstab")
"/run/current-system/etc/fstab"))
(passwd-target (assert-symlink-target (string-append rootfs "/etc/passwd")
"/run/current-system/etc/passwd"))
(group-target (assert-symlink-target (string-append rootfs "/etc/group")
"/run/current-system/etc/group"))
(rc-script-target (assert-symlink-target (string-append rootfs "/usr/local/etc/rc.d/fruix-shepherd")
"/run/current-system/usr/local/etc/rc.d/fruix-shepherd"))
(rc-conf-content (call-with-input-file (string-append closure-path "/etc/rc.conf") get-string-all))
(fstab-content (call-with-input-file (string-append closure-path "/etc/fstab") get-string-all))
(activation-content (call-with-input-file (string-append closure-path "/activate") get-string-all))
(shepherd-content (call-with-input-file (string-append closure-path "/shepherd/init.scm") get-string-all))
(loader-conf-content (call-with-input-file (string-append closure-path "/boot/loader.conf") get-string-all)))
(for-each assert-exists
(list rootfs closure-path rc-script
(string-append rootfs "/etc/rc")
(string-append rootfs "/etc/rc.subr")
(string-append rootfs "/etc/rc.d")
(string-append rootfs "/etc/defaults")
(string-append rootfs "/etc/motd")
(string-append rootfs "/usr/sbin")
(string-append rootfs "/usr/bin")
(string-append rootfs "/var/lib/fruix")
(string-append rootfs "/var/log")
(string-append rootfs "/var/run")
(string-append rootfs "/tmp")))
(unless (string-contains rc-conf-content "hostname=\"fruix-freebsd\"")
(error "rc.conf does not contain the expected hostname"))
(unless (string-contains rc-conf-content "fruix_shepherd_enable=\"YES\"")
(error "rc.conf does not enable fruix_shepherd"))
(unless (and (string-contains fstab-content "/dev/ufs/fruix-root")
(string-contains fstab-content "devfs")
(string-contains fstab-content "tmpfs"))
(error "fstab content was incomplete"))
(unless (string-contains activation-content "pw useradd operator")
(error "activation script does not provision the operator account"))
(unless (string-contains shepherd-content ready-marker)
(error "shepherd configuration does not mention the ready marker"))
(unless (string-contains loader-conf-content "console=\"comconsole\"")
(error "loader.conf does not contain the expected serial console setting"))
(call-with-output-file metadata-file
(lambda (port)
(format port "rootfs=~a~%" rootfs)
(format port "closure_path=~a~%" closure-path)
(format port "run_current_system_target=~a~%" run-current-system-target)
(format port "activate_target=~a~%" activate-target)
(format port "bin_target=~a~%" bin-target)
(format port "sbin_target=~a~%" sbin-target)
(format port "lib_target=~a~%" lib-target)
(format port "boot_kernel_target=~a~%" boot-kernel-target)
(format port "boot_loader_target=~a~%" boot-loader-target)
(format port "boot_loader_efi_target=~a~%" boot-loader-efi-target)
(format port "rc_conf_target=~a~%" rc-conf-target)
(format port "fstab_target=~a~%" fstab-target)
(format port "passwd_target=~a~%" passwd-target)
(format port "group_target=~a~%" group-target)
(format port "rc_script=~a~%" rc-script)
(format port "rc_script_target=~a~%" rc-script-target)
(format port "ready_marker=~a~%" ready-marker)
(format port "validation_mode=static-rootfs-check~%")
(format port "ready_state_mode=freebsd-init+rc.d-shepherd~%")))
(when (getenv "METADATA_OUT")
(copy-file metadata-file (getenv "METADATA_OUT")))
(format #t "PASS phase7-rootfs\n")
(format #t "Metadata file: ~a\n" metadata-file)
(when (getenv "METADATA_OUT")
(format #t "Copied metadata to: ~a\n" (getenv "METADATA_OUT")))
(display "--- metadata ---\n")
(display (call-with-input-file metadata-file get-string-all)))

View File

@@ -0,0 +1,102 @@
(use-modules (fruix system freebsd)
(ice-9 format)
(srfi srfi-13)
(rnrs io ports))
(define workdir
(or (getenv "WORKDIR")
(error "WORKDIR environment variable is required")))
(define os-file
(or (getenv "OS_FILE")
(error "OS_FILE environment variable is required")))
(define store-dir
(or (getenv "STORE_DIR")
"/frx/store"))
(define guile-prefix
(or (getenv "GUILE_PREFIX")
"/tmp/guile-freebsd-validate-install"))
(define guile-extra-prefix
(or (getenv "GUILE_EXTRA_PREFIX")
"/tmp/guile-gnutls-freebsd-validate-install"))
(define shepherd-prefix
(or (getenv "SHEPHERD_PREFIX")
"/tmp/shepherd-freebsd-validate-install"))
(define metadata-file
(string-append workdir "/phase7-system-closure-metadata.txt"))
(primitive-load os-file)
(validate-operating-system phase7-operating-system)
(define (assert-exists path)
(unless (file-exists? path)
(error "required path missing" path)))
(let* ((closure-a (materialize-operating-system phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-b (materialize-operating-system phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref closure-a 'closure-path))
(closure-rebuild-path (assoc-ref closure-b 'closure-path))
(kernel-store (assoc-ref closure-a 'kernel-store))
(bootloader-store (assoc-ref closure-a 'bootloader-store))
(guile-store (assoc-ref closure-a 'guile-store))
(guile-extra-store (assoc-ref closure-a 'guile-extra-store))
(shepherd-store (assoc-ref closure-a 'shepherd-store))
(base-package-stores (assoc-ref closure-a 'base-package-stores))
(rc-script (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd"))
(shepherd-config (string-append closure-path "/shepherd/init.scm"))
(activation-script (string-append closure-path "/activate"))
(loader-conf (string-append closure-path "/boot/loader.conf"))
(profile-bin-sh (string-append closure-path "/profile/bin/sh"))
(profile-sbin-init (string-append closure-path "/profile/sbin/init"))
(profile-rc (string-append closure-path "/profile/etc/rc"))
(ready-marker (assoc-ref (operating-system-closure-spec phase7-operating-system)
'ready-marker))
(rc-script-target (readlink (string-append closure-path "/boot/loader")))
(kernel-link (readlink (string-append closure-path "/boot/kernel/kernel"))))
(for-each assert-exists
(list closure-path kernel-store bootloader-store guile-store
guile-extra-store shepherd-store rc-script shepherd-config
activation-script loader-conf profile-bin-sh profile-sbin-init
profile-rc (string-append closure-path "/parameters.scm")))
(unless (string=? closure-path closure-rebuild-path)
(error "closure path was not reproducible" closure-path closure-rebuild-path))
(call-with-output-file metadata-file
(lambda (port)
(format port "store_dir=~a~%" store-dir)
(format port "closure_path=~a~%" closure-path)
(format port "closure_rebuild_path=~a~%" closure-rebuild-path)
(format port "kernel_store=~a~%" kernel-store)
(format port "bootloader_store=~a~%" bootloader-store)
(format port "guile_store=~a~%" guile-store)
(format port "guile_extra_store=~a~%" guile-extra-store)
(format port "shepherd_store=~a~%" shepherd-store)
(format port "base_package_store_count=~a~%" (length base-package-stores))
(format port "base_package_stores=~a~%" (string-join base-package-stores ","))
(format port "rc_script=~a~%" rc-script)
(format port "shepherd_config=~a~%" shepherd-config)
(format port "activation_script=~a~%" activation-script)
(format port "loader_conf=~a~%" loader-conf)
(format port "boot_loader_target=~a~%" rc-script-target)
(format port "kernel_link_target=~a~%" kernel-link)
(format port "profile_bin_sh=~a~%" profile-bin-sh)
(format port "profile_sbin_init=~a~%" profile-sbin-init)
(format port "profile_rc=~a~%" profile-rc)
(format port "ready_marker=~a~%" ready-marker)
(format port "init_integration=freebsd-init+rc.d-shepherd~%")))
(when (getenv "METADATA_OUT")
(copy-file metadata-file (getenv "METADATA_OUT")))
(format #t "PASS phase7-system-closure~%")
(format #t "Metadata file: ~a~%" metadata-file)
(when (getenv "METADATA_OUT")
(format #t "Copied metadata to: ~a~%" (getenv "METADATA_OUT")))
(display "--- metadata ---\n")
(display (call-with-input-file metadata-file get-string-all)))

View File

@@ -0,0 +1,121 @@
(use-modules (fruix system freebsd)
(ice-9 format)
(ice-9 pretty-print)
(ice-9 popen)
(srfi srfi-13)
(rnrs io ports))
(define workdir
(or (getenv "WORKDIR")
(error "WORKDIR environment variable is required")))
(define os-file
(or (getenv "OS_FILE")
(error "OS_FILE environment variable is required")))
(define store-dir
(or (getenv "STORE_DIR")
"/frx/store"))
(define guile-prefix
(or (getenv "GUILE_PREFIX")
"/tmp/guile-freebsd-validate-install"))
(define guile-extra-prefix
(or (getenv "GUILE_EXTRA_PREFIX")
"/tmp/guile-gnutls-freebsd-validate-install"))
(define shepherd-prefix
(or (getenv "SHEPHERD_PREFIX")
"/tmp/shepherd-freebsd-validate-install"))
(define metadata-file
(string-append workdir "/phase8-system-image-metadata.txt"))
(define disk-capacity
(let ((value (getenv "DISK_CAPACITY")))
(and value (not (string-null? value)) value)))
(define (trim-trailing-newlines str)
(let loop ((len (string-length str)))
(if (and (> len 0)
(char=? (string-ref str (- len 1)) #\newline))
(loop (- len 1))
(substring str 0 len))))
(define (command-output program . args)
(let* ((port (apply open-pipe* OPEN_READ program args))
(output (get-string-all port))
(status (close-pipe port)))
(unless (zero? status)
(error "command failed" program args status))
(trim-trailing-newlines output)))
(define (assert-exists path)
(unless (file-exists? path)
(error "required path missing" path)))
(primitive-load os-file)
(validate-operating-system phase7-operating-system)
(let* ((image-a (if disk-capacity
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity)
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix)))
(image-b (if disk-capacity
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity)
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix)))
(image-store-path (assoc-ref image-a 'image-store-path))
(image-store-path-rebuild (assoc-ref image-b 'image-store-path))
(disk-image (assoc-ref image-a 'disk-image))
(esp-image (assoc-ref image-a 'esp-image))
(root-image (assoc-ref image-a 'root-image))
(closure-path (assoc-ref image-a 'closure-path))
(image-spec (assoc-ref image-a 'image-spec))
(store-items (assoc-ref image-a 'store-items))
(raw-sha256 (command-output "sha256" "-q" disk-image))
(image-size-bytes (command-output "stat" "-f" "%z" disk-image)))
(for-each assert-exists
(list image-store-path disk-image esp-image root-image
(string-append image-store-path "/image-spec.scm")
(string-append image-store-path "/closure-path")
(string-append image-store-path "/.references")
(string-append image-store-path "/.fruix-package")))
(unless (string=? image-store-path image-store-path-rebuild)
(error "image store path was not reproducible" image-store-path image-store-path-rebuild))
(call-with-output-file metadata-file
(lambda (port)
(format port "store_dir=~a~%" store-dir)
(format port "image_store_path=~a~%" image-store-path)
(format port "image_store_path_rebuild=~a~%" image-store-path-rebuild)
(format port "disk_image=~a~%" disk-image)
(format port "esp_image=~a~%" esp-image)
(format port "root_image=~a~%" root-image)
(format port "closure_path=~a~%" closure-path)
(format port "disk_capacity=~a~%" (or disk-capacity "<default>"))
(format port "store_item_count=~a~%" (length store-items))
(format port "raw_sha256=~a~%" raw-sha256)
(format port "image_size_bytes=~a~%" image-size-bytes)
(format port "image_spec=~a~%"
(string-map (lambda (ch) (if (char=? ch #\newline) #\space ch))
(with-output-to-string
(lambda ()
(pretty-print image-spec)))))))
(when (getenv "METADATA_OUT")
(copy-file metadata-file (getenv "METADATA_OUT")))
(format #t "PASS phase8-system-image-materialization~%")
(format #t "Metadata file: ~a~%" metadata-file)
(when (getenv "METADATA_OUT")
(format #t "Copied metadata to: ~a~%" (getenv "METADATA_OUT")))
(display "--- metadata ---\n")
(display (call-with-input-file metadata-file get-string-all)))

View File

@@ -0,0 +1,78 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase11-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-kernel
#:bootloader freebsd-bootloader
#:base-packages (list freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-libc
freebsd-rc-scripts
freebsd-sh
freebsd-bash)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,71 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase13-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-bootloader
#:base-packages (list freebsd-native-world)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,71 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase14-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-native-world
#:base-packages (list freebsd-native-world)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,71 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase14-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-native-world
#:base-packages (list freebsd-native-runtime)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,71 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase14-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-native-bootloader
#:base-packages %freebsd-native-system-packages
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,83 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase15-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source-root "/usr/src"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase15-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase15-base
#:kernel (freebsd-native-kernel-for phase15-base)
#:bootloader (freebsd-native-bootloader-for phase15-base)
#:base-packages (freebsd-native-system-packages-for phase15-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,90 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase16-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'local-tree
#:path "/usr/src"))
(define phase16-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase16-source
#:source-root "/usr/src"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase16-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase16-base
#:kernel (freebsd-native-kernel-for phase16-base)
#:bootloader (freebsd-native-bootloader-for phase16-base)
#:base-packages (freebsd-native-system-packages-for phase16-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,7 @@
(use-modules (fruix packages freebsd))
(define phase16-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"))

View File

@@ -0,0 +1,90 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase16-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"))
(define phase16-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase16-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase16-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase16-base
#:kernel (freebsd-native-kernel-for phase16-base)
#:bootloader (freebsd-native-bootloader-for phase16-base)
#:base-packages (freebsd-native-system-packages-for phase16-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,8 @@
(use-modules (fruix packages freebsd))
(define phase16-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'src-txz
#:url "__SOURCE_URL__"
#:sha256 "__SOURCE_SHA256__"))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase17-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"
#:commit "__SOURCE_COMMIT__"))
(define phase17-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase17-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase17-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase17-base
#:kernel (freebsd-native-kernel-for phase17-base)
#:bootloader (freebsd-native-bootloader-for phase17-base)
#:base-packages (freebsd-native-system-packages-for phase17-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase17-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'src-txz
#:url "__SOURCE_URL__"
#:sha256 "__SOURCE_SHA256__"))
(define phase17-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase17-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase17-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase17-base
#:kernel (freebsd-native-kernel-for phase17-base)
#:bootloader (freebsd-native-bootloader-for phase17-base)
#:base-packages (freebsd-native-system-packages-for phase17-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase18-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"
#:commit "__SOURCE_COMMIT__"))
(define phase18-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase18-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase18-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase18-base
#:kernel (freebsd-native-kernel-for phase18-base)
#:bootloader (freebsd-native-bootloader-for phase18-base)
#:base-packages (freebsd-native-system-packages-for phase18-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'freebsd-init+rc.d-shepherd
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase18-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"
#:commit "__SOURCE_COMMIT__"))
(define phase18-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase18-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase18-target-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase18-base
#:kernel (freebsd-native-kernel-for phase18-base)
#:bootloader (freebsd-native-bootloader-for phase18-base)
#:base-packages (freebsd-native-system-packages-for phase18-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'freebsd-init+rc.d-shepherd
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase19-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"
#:commit "__SOURCE_COMMIT__"))
(define phase19-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase19-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase19-operating-system
(operating-system
#:host-name "__HOST_NAME__"
#:freebsd-base phase19-base
#:kernel (freebsd-native-kernel-for phase19-base)
#:bootloader (freebsd-native-bootloader-for phase19-base)
#:base-packages (freebsd-native-system-packages-for phase19-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'freebsd-init+rc.d-shepherd
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,75 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase20-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-native-bootloader
#:base-packages %freebsd-native-system-packages
#:development-packages (list freebsd-native-headers
freebsd-clang-toolchain)
#:build-packages (list freebsd-native-headers
freebsd-clang-toolchain)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,73 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase20-promoted-native-build-result
(promoted-native-build-result
#:store-path "__PROMOTED_RESULT_STORE__"))
(define phase20-promoted-native-base-operating-system
(operating-system-from-promoted-native-build-result
phase20-promoted-native-build-result
#:host-name "fruix-freebsd"
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,53 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase7-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-kernel
#:bootloader freebsd-bootloader
#:base-packages (list freebsd-runtime
freebsd-userland
freebsd-libc
freebsd-rc-scripts
freebsd-sh
freebsd-bash)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/ufs/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker)
#:loader-entries '(("autoboot_delay" . "1")
("console" . "comconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "NO"))
#:ready-marker "/var/lib/fruix/ready"))

View File

@@ -0,0 +1,77 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase7-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-kernel
#:bootloader freebsd-bootloader
#:base-packages (list freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-libc
freebsd-rc-scripts
freebsd-sh
freebsd-bash)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,73 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define postphase20-promoted-native-build-result
(promoted-native-build-result
#:store-path "__PROMOTED_RESULT_STORE__"))
(define postphase20-installed-node-operating-system
(operating-system-from-promoted-native-build-result
postphase20-promoted-native-build-result
#:host-name "__HOST_NAME__"
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'shepherd-pid1
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -0,0 +1,102 @@
#!/bin/sh
set -eu
project_root=${PROJECT_ROOT:-$(pwd)}
os_file=${OS_FILE:-$project_root/tests/system/phase7-minimal-operating-system.scm}
system_name=${SYSTEM_NAME:-phase7-operating-system}
store_dir=${STORE_DIR:-/frx/store}
disk_capacity=${DISK_CAPACITY:-5g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase10-system-command.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
build_out=$workdir/build.txt
image_out=$workdir/image.txt
metadata_file=$workdir/phase10-fruix-system-command-metadata.txt
sudo env \
HOME="$HOME" \
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}" \
"$project_root/bin/fruix" system build "$os_file" --system "$system_name" --store "$store_dir" >"$build_out"
closure_path=$(sed -n 's/^closure_path=//p' "$build_out")
generated_file_count=$(sed -n 's/^generated_file_count=//p' "$build_out")
reference_count=$(sed -n 's/^reference_count=//p' "$build_out")
case "$closure_path" in
/frx/store/*-fruix-system-fruix-freebsd) : ;;
*) echo "unexpected closure path: $closure_path" >&2; exit 1 ;;
esac
[ -x "$closure_path/activate" ] || { echo "missing activate script in closure" >&2; exit 1; }
[ -n "$generated_file_count" ] || { echo "missing generated_file_count" >&2; exit 1; }
[ -n "$reference_count" ] || { echo "missing reference_count" >&2; exit 1; }
sudo env \
HOME="$HOME" \
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}" \
"$project_root/bin/fruix" system image "$os_file" --system "$system_name" --store "$store_dir" --disk-capacity "$disk_capacity" >"$image_out"
image_store_path=$(sed -n 's/^image_store_path=//p' "$image_out")
disk_image=$(sed -n 's/^disk_image=//p' "$image_out")
disk_capacity_reported=$(sed -n 's/^disk_capacity=//p' "$image_out")
store_item_count=$(sed -n 's/^store_item_count=//p' "$image_out")
case "$image_store_path" in
/frx/store/*-fruix-bhyve-image-fruix-freebsd) : ;;
*) echo "unexpected image store path: $image_store_path" >&2; exit 1 ;;
esac
case "$disk_image" in
/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img) : ;;
*) echo "unexpected disk image path: $disk_image" >&2; exit 1 ;;
esac
[ -f "$disk_image" ] || { echo "missing disk image" >&2; exit 1; }
[ -n "$disk_capacity_reported" ] || { echo "missing disk capacity report" >&2; exit 1; }
[ -n "$store_item_count" ] || { echo "missing store item count" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
os_file=$os_file
system_name=$system_name
store_dir=$store_dir
closure_path=$closure_path
generated_file_count=$generated_file_count
reference_count=$reference_count
image_store_path=$image_store_path
disk_image=$disk_image
disk_capacity=$disk_capacity_reported
store_item_count=$store_item_count
EOF
if [ -n "$metadata_target" ]; then
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase10-fruix-system-command\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi

View File

@@ -0,0 +1,196 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase11-shepherd-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase11-operating-system}
metadata_target=${METADATA_OUT:-}
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}
ssh_port=${QEMU_SSH_PORT:-10022}
qemu_smp=${QEMU_SMP:-2}
disk_capacity=${DISK_CAPACITY:-5g}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase11-pid1-qemu.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
phase11_os_file=$workdir/phase11-shepherd-pid1-operating-system.scm
phase8_log=$workdir/phase8-system-image.log
phase8_metadata=$workdir/phase8-system-image-metadata.txt
serial_log=$workdir/serial.log
qemu_pidfile=$workdir/qemu.pid
metadata_file=$workdir/phase11-shepherd-pid1-qemu-metadata.txt
uefi_vars=$workdir/QEMU_UEFI_VARS.fd
cleanup_workdir() {
if [ -f "$qemu_pidfile" ]; then
sudo kill "$(sudo cat "$qemu_pidfile")" >/dev/null 2>&1 || true
fi
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
[ -f "$root_authorized_key_file" ] || {
echo "missing root authorized key file: $root_authorized_key_file" >&2
exit 1
}
[ -f "$root_ssh_private_key_file" ] || {
echo "missing root SSH private key file: $root_ssh_private_key_file" >&2
exit 1
}
command -v qemu-system-x86_64 >/dev/null 2>&1 || {
echo "qemu-system-x86_64 is required" >&2
exit 1
}
[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || {
echo "missing QEMU UEFI firmware" >&2
exit 1
}
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$uefi_vars"
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$os_template" > "$phase11_os_file"
KEEP_WORKDIR=1 WORKDIR="$workdir/phase8-build" OS_FILE="$phase11_os_file" SYSTEM_NAME="$system_name" DISK_CAPACITY="$disk_capacity" \
METADATA_OUT="$phase8_metadata" "$repo_root/tests/system/run-phase8-system-image.sh" >"$phase8_log" 2>&1
disk_image=$(sed -n 's/^disk_image=//p' "$phase8_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$phase8_metadata")
closure_base=$(basename "$closure_path")
raw_sha256=$(sed -n 's/^raw_sha256=//p' "$phase8_metadata")
image_store_path=$(sed -n 's/^image_store_path=//p' "$phase8_metadata")
boot_disk_image=$workdir/boot-disk.img
cp "$disk_image" "$boot_disk_image"
sudo qemu-system-x86_64 \
-machine q35,accel=tcg \
-cpu max \
-m 2048 \
-smp "$qemu_smp" \
-display none \
-serial "file:$serial_log" \
-monitor none \
-pidfile "$qemu_pidfile" \
-daemonize \
-drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \
-drive if=pflash,format=raw,file="$uefi_vars" \
-drive if=virtio,format=raw,file="$boot_disk_image" \
-netdev user,id=net0,hostfwd=tcp::${ssh_port}-:22 \
-device virtio-net-pci,netdev=net0
ssh_guest() {
ssh -p "$ssh_port" -i "$root_ssh_private_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
root@127.0.0.1 "$@"
}
for attempt in $(jot 120 1 120); do
if ssh_guest 'test -f /var/lib/fruix/ready' >/dev/null 2>&1; then
break
fi
sleep 2
done
ready_marker=$(ssh_guest 'cat /var/lib/fruix/ready')
run_current_system_target=$(ssh_guest 'readlink /run/current-system')
pid1_command=$(ssh_guest 'ps -p 1 -o command= | sed "s/^ *//"')
pid1_binary=$(ssh_guest 'procstat -b 1 2>/dev/null | awk "NR==2 {print \$2}"')
shepherd_pid=$(ssh_guest 'cat /var/run/shepherd.pid')
shepherd_socket=$(ssh_guest 'test -S /var/run/shepherd.sock && echo present || echo missing')
shepherd_status=$(ssh_guest 'test -f /var/run/shepherd.pid && kill -0 "$(cat /var/run/shepherd.pid)" >/dev/null 2>&1 && echo running || echo stopped')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ')
shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ')
guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ')
activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
login_conf_kind=$(ssh_guest 'if [ -L /etc/login.conf ]; then echo symlink; elif [ -f /etc/login.conf ]; then echo regular; else echo missing; fi')
login_conf_db=$(ssh_guest 'test -f /etc/login.conf.db && echo present || echo missing')
pwd_dbs=$(ssh_guest 'if [ -f /etc/pwd.db ] && [ -f /etc/spwd.db ]; then echo present; else echo missing; fi')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
[ "$ready_marker" = ready ] || { echo "unexpected ready marker contents: $ready_marker" >&2; exit 1; }
[ "$run_current_system_target" = "/frx/store/$closure_base" ] || {
echo "unexpected /run/current-system target in guest: $run_current_system_target" >&2
exit 1
}
[ "$shepherd_pid" = 1 ] || {
echo "shepherd is not PID 1: shepherd.pid=$shepherd_pid pid1_command=$pid1_command pid1_binary=$pid1_binary" >&2
exit 1
}
[ "$shepherd_socket" = present ] || { echo "shepherd socket is missing" >&2; exit 1; }
[ "$shepherd_status" = running ] || { echo "shepherd is not running" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
[ "$login_conf_kind" = regular ] || { echo "/etc/login.conf is not a regular file in guest: $login_conf_kind" >&2; exit 1; }
[ "$login_conf_db" = present ] || { echo "/etc/login.conf.db is missing in guest" >&2; exit 1; }
[ "$pwd_dbs" = present ] || { echo "pwd.db/spwd.db are missing in guest" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show successful completion: $activate_log" >&2; exit 1 ;;
esac
[ "$operator_home_listing" = /home/operator ] || { echo "operator home missing" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
phase11_os_file=$phase11_os_file
phase8_log=$phase8_log
phase8_metadata=$phase8_metadata
image_store_path=$image_store_path
disk_image=$disk_image
boot_disk_image=$boot_disk_image
closure_path=$closure_path
closure_base=$closure_base
raw_sha256=$raw_sha256
serial_log=$serial_log
qemu_pidfile=$qemu_pidfile
ssh_port=$ssh_port
qemu_smp=$qemu_smp
ready_marker=$ready_marker
run_current_system_target=$run_current_system_target
pid1_command=$pid1_command
pid1_binary=$pid1_binary
shepherd_pid=$shepherd_pid
shepherd_socket=$shepherd_socket
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
shepherd_bootstrap_tail=$shepherd_bootstrap_tail
shepherd_log_tail=$shepherd_log_tail
guest_dmesg_tail=$guest_dmesg_tail
activate_log=$activate_log
login_conf_kind=$login_conf_kind
login_conf_db=$login_conf_db
pwd_dbs=$pwd_dbs
uname_output=$uname_output
operator_home_listing=$operator_home_listing
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase11-shepherd-pid1-qemu\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,247 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase11-shepherd-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase11-operating-system}
metadata_target=${METADATA_OUT:-}
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}
requested_disk_capacity=${DISK_CAPACITY:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase11-xcpng.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
phase11_os_file=$workdir/phase11-shepherd-pid1-operating-system.scm
phase8_log=$workdir/phase8-system-image.log
phase8_metadata=$workdir/phase8-system-image-metadata.txt
arp_scan_log=$workdir/arp-scan.log
ssh_stdout=$workdir/ssh.out
ssh_stderr=$workdir/ssh.err
metadata_file=$workdir/phase11-shepherd-pid1-xcpng-metadata.txt
vdi_info_json=$workdir/vdi-info.json
vm_info_json=$workdir/vm-info.json
upload_image=$workdir/disk.vhd
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
[ -f "$root_authorized_key_file" ] || {
echo "missing root authorized key file: $root_authorized_key_file" >&2
exit 1
}
[ -f "$root_ssh_private_key_file" ] || {
echo "missing root SSH private key file: $root_ssh_private_key_file" >&2
exit 1
}
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
xo-cli list-objects id=$vm_id >"$vm_info_json"
vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="0") | .VDI' | head -n 1)
[ -n "$vdi_id" ] || { echo "failed to discover target VDI for VM $vm_id" >&2; exit 1; }
xo-cli list-objects type=VDI | jq '[.[] | select(.id=="'$vdi_id'")]' >"$vdi_info_json"
vdi_size=$(jq -r '.[0].size' "$vdi_info_json")
[ -n "$vdi_size" ] || { echo "failed to discover VDI size for $vdi_id" >&2; exit 1; }
if [ -n "$requested_disk_capacity" ] && [ "$requested_disk_capacity" != "$vdi_size" ]; then
echo "existing XCP-ng import path requires an image that matches the target VDI size; use DISK_CAPACITY=$vdi_size or leave it unset" >&2
exit 1
fi
disk_capacity=$vdi_size
requested_disk_bytes=$vdi_size
sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$os_template" > "$phase11_os_file"
KEEP_WORKDIR=1 WORKDIR=$workdir/phase8-build OS_FILE=$phase11_os_file SYSTEM_NAME=$system_name DISK_CAPACITY=$disk_capacity \
METADATA_OUT=$phase8_metadata "$repo_root/tests/system/run-phase8-system-image.sh" \
>"$phase8_log" 2>&1
disk_image=$(sed -n 's/^disk_image=//p' "$phase8_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$phase8_metadata")
closure_base=$(basename "$closure_path")
raw_sha256=$(sed -n 's/^raw_sha256=//p' "$phase8_metadata")
image_store_path=$(sed -n 's/^image_store_path=//p' "$phase8_metadata")
guile_store=$(grep 'fruix-guile-runtime-3.0$' "$closure_path/.references" | head -n 1)
guile_extra_store=$(grep 'fruix-guile-extra-3.0$' "$closure_path/.references" | head -n 1)
shepherd_store=$(grep 'fruix-shepherd-runtime-1.0.9$' "$closure_path/.references" | head -n 1)
command -v qemu-img >/dev/null 2>&1 || {
echo "qemu-img is required to convert the raw Fruix image to XCP-ng-compatible VHD" >&2
exit 1
}
qemu-img convert -f raw -O vpc -o subformat=dynamic,force_size=on "$disk_image" "$upload_image"
upload_sha256=$(sha256 -q "$upload_image")
upload_size_bytes=$(stat -f '%z' "$upload_image")
xo-cli vm.stop id=$vm_id force=true >/dev/null 2>&1 || true
xo-cli disk.importContent id=$vdi_id @=$upload_image >"$workdir/disk-import.out"
xo-cli vm.setBootOrder vm=$vm_id order=dcn >"$workdir/set-boot-order.out"
xo-cli vm.start id=$vm_id >"$workdir/vm-start.out"
vm_mac=$(jq -r '.[0].VIFs[0]' "$vm_info_json")
if [ -n "$vm_mac" ] && [ "$vm_mac" != null ]; then
vm_mac=$(xo-cli list-objects type=VIF | jq -r '.[] | select(.id=="'$vm_mac'") | .MAC' | tr 'A-Z' 'a-z')
else
vm_mac=
fi
host_interface=$(route -n get default | awk '/interface:/{print $2; exit}')
host_ip=$(ifconfig "$host_interface" | awk '/inet /{print $2; exit}')
subnet_prefix=${host_ip%.*}
ssh_guest() {
ssh -i "$root_ssh_private_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
root@"$guest_ip" "$@"
}
guest_ip=
for attempt in $(jot 90 1 90); do
: >"$arp_scan_log"
for host in $(jot 254 1 254); do
ip=$subnet_prefix.$host
(
ping -c 1 -W 1000 "$ip" >/dev/null 2>&1 && echo "$ip" >>"$arp_scan_log"
) &
done
wait
if [ -n "$vm_mac" ]; then
guest_ip=$(arp -an | awk -v mac="$vm_mac" 'tolower($4)==mac {gsub(/[()]/,"",$2); print $2; exit}')
fi
if [ -n "$guest_ip" ]; then
if ssh -i "$root_ssh_private_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=3 \
root@"$guest_ip" 'test -f /var/lib/fruix/ready' >"$ssh_stdout" 2>"$ssh_stderr"; then
break
fi
fi
sleep 5
done
[ -n "$guest_ip" ] || {
echo "guest IP was not discovered; manual console inspection is likely required" >&2
exit 1
}
ready_marker=$(ssh_guest 'cat /var/lib/fruix/ready')
run_current_system_target=$(ssh_guest 'readlink /run/current-system')
pid1_command=$(ssh_guest 'ps -p 1 -o command= | sed "s/^ *//"')
shepherd_pid=$(ssh_guest 'cat /var/run/shepherd.pid')
shepherd_socket=$(ssh_guest 'test -S /var/run/shepherd.sock && echo present || echo missing')
shepherd_status=$(ssh_guest 'test -f /var/run/shepherd.pid && kill -0 "$(cat /var/run/shepherd.pid)" >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ')
shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ')
guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
compat_prefix_shims=$(ssh_guest 'for p in /tmp/guile-freebsd-validate-install /tmp/guile-gnutls-freebsd-validate-install /tmp/shepherd-freebsd-validate-install; do if [ -e "$p" ] || [ -L "$p" ]; then echo present; exit 0; fi; done; echo absent')
guile_module_smoke=$(ssh_guest "env LANG='C.UTF-8' LC_ALL='C.UTF-8' LD_LIBRARY_PATH='$guile_extra_store/lib:$guile_store/lib:/usr/local/lib' GUILE_SYSTEM_PATH='$guile_store/share/guile/3.0:$guile_store/share/guile/site/3.0:$guile_store/share/guile/site:$guile_store/share/guile' GUILE_LOAD_PATH='$shepherd_store/share/guile/site/3.0:$guile_extra_store/share/guile/site/3.0' GUILE_SYSTEM_COMPILED_PATH='$guile_store/lib/guile/3.0/ccache:$guile_store/lib/guile/3.0/site-ccache' GUILE_LOAD_COMPILED_PATH='$shepherd_store/lib/guile/3.0/site-ccache:$guile_extra_store/lib/guile/3.0/site-ccache' GUILE_SYSTEM_EXTENSIONS_PATH='$guile_store/lib/guile/3.0/extensions' GUILE_EXTENSIONS_PATH='$guile_extra_store/lib/guile/3.0/extensions' '$guile_store/bin/guile' --no-auto-compile -c '(use-modules (fibers config) (gnutls) (shepherd config)) (display \"ok\") (newline)'")
activate_preview=$(ssh_guest 'head -n 5 /run/current-system/activate' | tr '\n' ' ')
activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
login_conf_kind=$(ssh_guest 'if [ -L /etc/login.conf ]; then echo symlink; elif [ -f /etc/login.conf ]; then echo regular; else echo missing; fi')
login_conf_db=$(ssh_guest 'test -f /etc/login.conf.db && echo present || echo missing')
pwd_dbs=$(ssh_guest 'if [ -f /etc/pwd.db ] && [ -f /etc/spwd.db ]; then echo present; else echo missing; fi')
[ "$ready_marker" = ready ] || { echo "unexpected ready marker contents: $ready_marker" >&2; exit 1; }
[ "$shepherd_pid" = 1 ] || { echo "shepherd is not PID 1: pid=$shepherd_pid command=$pid1_command" >&2; exit 1; }
[ "$shepherd_socket" = present ] || { echo "shepherd socket is missing" >&2; exit 1; }
[ "$shepherd_status" = running ] || { echo "shepherd is not running" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims are still present in /tmp" >&2; exit 1; }
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed: $guile_module_smoke" >&2; exit 1; }
[ "$login_conf_kind" = regular ] || { echo "/etc/login.conf is not a regular file in guest: $login_conf_kind" >&2; exit 1; }
[ "$login_conf_db" = present ] || { echo "/etc/login.conf.db is missing in guest" >&2; exit 1; }
[ "$pwd_dbs" = present ] || { echo "pwd.db/spwd.db are missing in guest" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show successful completion: $activate_log" >&2; exit 1 ;;
esac
[ "$run_current_system_target" = "/frx/store/$closure_base" ] || {
echo "unexpected /run/current-system target in guest: $run_current_system_target" >&2
exit 1
}
[ "$operator_home_listing" = /home/operator ] || { echo "operator home missing" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
vm_id=$vm_id
vdi_id=$vdi_id
vdi_size=$vdi_size
disk_capacity=$disk_capacity
requested_disk_capacity=${requested_disk_capacity:-<auto>}
requested_disk_bytes=$requested_disk_bytes
phase11_os_file=$phase11_os_file
phase8_log=$phase8_log
phase8_metadata=$phase8_metadata
image_store_path=$image_store_path
disk_image=$disk_image
upload_image=$upload_image
upload_format=vhd-dynamic
upload_sha256=$upload_sha256
upload_size_bytes=$upload_size_bytes
closure_path=$closure_path
closure_base=$closure_base
raw_sha256=$raw_sha256
guest_ip=$guest_ip
vm_mac=$vm_mac
ready_marker=$ready_marker
run_current_system_target=$run_current_system_target
pid1_command=$pid1_command
shepherd_pid=$shepherd_pid
shepherd_socket=$shepherd_socket
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
shepherd_bootstrap_tail=$shepherd_bootstrap_tail
shepherd_log_tail=$shepherd_log_tail
guest_dmesg_tail=$guest_dmesg_tail
uname_output=$uname_output
operator_home_listing=$operator_home_listing
compat_prefix_shims=$compat_prefix_shims
guile_module_smoke=$guile_module_smoke
activate_preview=$activate_preview
activate_log=$activate_log
login_conf_kind=$login_conf_kind
login_conf_db=$login_conf_db
pwd_dbs=$pwd_dbs
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
operator_access=ssh-root-key
root_authorized_key_file=$root_authorized_key_file
root_ssh_private_key_file=$root_ssh_private_key_file
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase11-shepherd-pid1-xcpng\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,240 @@
#!/bin/sh
set -eu
project_root=${PROJECT_ROOT:-$(pwd)}
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
fruix_cmd=$project_root/bin/fruix
os_template=${OS_TEMPLATE:-$script_dir/phase13-native-base-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase13-operating-system}
store_dir=${STORE_DIR:-/frx/store}
metadata_target=${METADATA_OUT:-}
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
[ -x "$fruix_cmd" ] || {
echo "fruix command is not executable: $fruix_cmd" >&2
exit 1
}
[ -f "$os_template" ] || {
echo "missing operating-system template: $os_template" >&2
exit 1
}
[ -f "$root_authorized_key_file" ] || {
echo "missing root authorized key file: $root_authorized_key_file" >&2
exit 1
}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase13-native-build.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
phase13_os_file=$workdir/phase13-native-base-operating-system.scm
build_out_a=$workdir/build-a.txt
build_out_b=$workdir/build-b.txt
metadata_file=$workdir/phase13-native-base-build-metadata.txt
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$os_template" > "$phase13_os_file"
action_env() {
sudo env \
HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
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}" \
"$@"
}
printf 'Using fruix command: %s\n' "$fruix_cmd"
printf 'Working directory: %s\n' "$workdir"
printf 'Store directory: %s\n' "$store_dir"
printf 'OS template: %s\n' "$os_template"
action_env "$fruix_cmd" system build "$phase13_os_file" --system "$system_name" --store "$store_dir" >"$build_out_a"
action_env "$fruix_cmd" system build "$phase13_os_file" --system "$system_name" --store "$store_dir" >"$build_out_b"
closure_path=$(sed -n 's/^closure_path=//p' "$build_out_a")
closure_rebuild_path=$(sed -n 's/^closure_path=//p' "$build_out_b")
kernel_store=$(sed -n 's/^kernel_store=//p' "$build_out_a")
bootloader_store=$(sed -n 's/^bootloader_store=//p' "$build_out_a")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$build_out_a")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$build_out_a")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$build_out_a")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$build_out_a")
fruix_runtime_store_count=$(sed -n 's/^fruix_runtime_store_count=//p' "$build_out_a")
fruix_runtime_stores=$(sed -n 's/^fruix_runtime_stores=//p' "$build_out_a")
host_base_provenance_file=$(sed -n 's/^host_base_provenance_file=//p' "$build_out_a")
store_layout_file=$(sed -n 's/^store_layout_file=//p' "$build_out_a")
reference_count=$(sed -n 's/^reference_count=//p' "$build_out_a")
generated_file_count=$(sed -n 's/^generated_file_count=//p' "$build_out_a")
[ -n "$closure_path" ] || { echo "missing closure path" >&2; exit 1; }
[ "$closure_path" = "$closure_rebuild_path" ] || {
echo "native-base closure path was not reproducible: $closure_path != $closure_rebuild_path" >&2
exit 1
}
case "$kernel_store" in
/frx/store/*-freebsd-native-kernel-15.0-STABLE) : ;;
*) echo "unexpected native kernel store path: $kernel_store" >&2; exit 1 ;;
esac
case "$bootloader_store" in
/frx/store/*-freebsd-bootloader-15.0-STABLE) : ;;
*) echo "unexpected bootloader store path: $bootloader_store" >&2; exit 1 ;;
esac
[ "$host_base_store_count" = 1 ] || { echo "expected exactly one host-staged base store, got: $host_base_store_count" >&2; exit 1; }
[ "$native_base_store_count" = 2 ] || { echo "expected exactly two native base stores, got: $native_base_store_count" >&2; exit 1; }
[ -n "$fruix_runtime_store_count" ] || { echo "missing Fruix runtime store count" >&2; exit 1; }
[ -n "$reference_count" ] || { echo "missing reference count" >&2; exit 1; }
[ -n "$generated_file_count" ] || { echo "missing generated file count" >&2; exit 1; }
world_store=$(printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' | head -n 1)
[ -n "$world_store" ] || { echo "failed to recover native world store from metadata" >&2; exit 1; }
world_build_info=$world_store/.freebsd-native-build-info.scm
kernel_build_info=$kernel_store/.freebsd-native-build-info.scm
[ -f "$world_build_info" ] || { echo "missing world build info: $world_build_info" >&2; exit 1; }
[ -f "$kernel_build_info" ] || { echo "missing kernel build info: $kernel_build_info" >&2; exit 1; }
[ -f "$host_base_provenance_file" ] || { echo "missing host base provenance file: $host_base_provenance_file" >&2; exit 1; }
[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; }
for path in \
"$kernel_store/boot/kernel/kernel" \
"$kernel_store/boot/kernel/linker.hints" \
"$world_store/bin/sh" \
"$world_store/sbin/init" \
"$world_store/etc/rc" \
"$world_store/usr/sbin/sshd" \
"$world_store/sbin/dhclient" \
"$world_store/usr/bin/cap_mkdb" \
"$world_store/usr/sbin/pwd_mkdb" \
"$world_store/usr/share/locale/C.UTF-8/LC_CTYPE"
do
[ -e "$path" ] || {
echo "required native world/kernel path missing: $path" >&2
exit 1
}
done
[ ! -e "$world_store/usr/share/man" ] || { echo "native world still contains pruned man pages" >&2; exit 1; }
[ ! -e "$world_store/usr/tests" ] || { echo "native world still contains pruned tests" >&2; exit 1; }
grep -F 'native-base-stores' "$store_layout_file" >/dev/null || {
echo "store layout metadata is missing native-base-stores" >&2
exit 1
}
grep -F '(materialized-source' "$world_build_info" >/dev/null || {
echo "world build info is missing materialized source metadata" >&2
exit 1
}
grep -F 'path . "/usr/src"' "$world_build_info" >/dev/null || {
echo "world build info is missing declared local-tree source provenance" >&2
exit 1
}
grep -F 'source-root . "/frx/store/' "$world_build_info" >/dev/null || {
echo "world build info is not using a materialized source root" >&2
exit 1
}
grep -F '(materialized-source' "$kernel_build_info" >/dev/null || {
echo "kernel build info is missing materialized source metadata" >&2
exit 1
}
grep -F 'path . "/usr/src"' "$kernel_build_info" >/dev/null || {
echo "kernel build info is missing declared local-tree source provenance" >&2
exit 1
}
grep -F 'source-root . "/frx/store/' "$kernel_build_info" >/dev/null || {
echo "kernel build info is not using a materialized source root" >&2
exit 1
}
world_source_tree_sha256=$(grep -o 'source-tree-sha256 . "[^"]*"' "$world_build_info" | head -n 1 | cut -d'"' -f2)
kernel_source_tree_sha256=$(grep -o 'source-tree-sha256 . "[^"]*"' "$kernel_build_info" | head -n 1 | cut -d'"' -f2)
world_build_root=$(grep -o 'build-root . "[^"]*"' "$world_build_info" | head -n 1 | cut -d'"' -f2)
kernel_build_root=$(grep -o 'build-root . "[^"]*"' "$kernel_build_info" | head -n 1 | cut -d'"' -f2)
world_build_log=$(grep -o 'buildworld-log . "[^"]*"' "$world_build_info" | head -n 1 | cut -d'"' -f2)
kernel_build_log=$(grep -o 'buildkernel-log . "[^"]*"' "$kernel_build_info" | head -n 1 | cut -d'"' -f2)
world_install_log=$(grep -o 'install-log . "[^"]*"' "$world_build_info" | head -n 1 | cut -d'"' -f2)
kernel_install_log=$(grep -o 'install-log . "[^"]*"' "$kernel_build_info" | head -n 1 | cut -d'"' -f2)
[ -n "$world_source_tree_sha256" ] || { echo "missing world source-tree hash" >&2; exit 1; }
[ -n "$kernel_source_tree_sha256" ] || { echo "missing kernel source-tree hash" >&2; exit 1; }
[ "$world_source_tree_sha256" = "$kernel_source_tree_sha256" ] || {
echo "native world/kernel source-tree hashes differ: $world_source_tree_sha256 != $kernel_source_tree_sha256" >&2
exit 1
}
[ "$world_build_root" = "$kernel_build_root" ] || {
echo "native world/kernel build roots differ: $world_build_root != $kernel_build_root" >&2
exit 1
}
for path in "$world_build_log" "$kernel_build_log" "$world_install_log" "$kernel_install_log"; do
[ -f "$path" ] || {
echo "expected native build log missing: $path" >&2
exit 1
}
done
closure_base=$(basename "$closure_path")
cat >"$metadata_file" <<EOF
workdir=$workdir
phase13_os_file=$phase13_os_file
closure_path=$closure_path
closure_rebuild_path=$closure_rebuild_path
closure_base=$closure_base
kernel_store=$kernel_store
world_store=$world_store
bootloader_store=$bootloader_store
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
fruix_runtime_store_count=$fruix_runtime_store_count
fruix_runtime_stores=$fruix_runtime_stores
host_base_provenance_file=$host_base_provenance_file
store_layout_file=$store_layout_file
reference_count=$reference_count
generated_file_count=$generated_file_count
world_source_tree_sha256=$world_source_tree_sha256
kernel_source_tree_sha256=$kernel_source_tree_sha256
world_build_root=$world_build_root
kernel_build_root=$kernel_build_root
world_build_log=$world_build_log
kernel_build_log=$kernel_build_log
world_install_log=$world_install_log
kernel_install_log=$kernel_install_log
frontend_invocation=$fruix_cmd system build
native_base_model=freebsd-world+freebsd-kernel-from-usr-src
init_mode=shepherd-pid1
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase13-native-base-build\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,105 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase13-native-base-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase13-operating-system}
disk_capacity=${DISK_CAPACITY:-8g}
root_size=${ROOT_SIZE:-6g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase13-native-qemu.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase11-native-qemu-inner-metadata.txt
metadata_file=$workdir/phase13-native-base-qemu-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" DISK_CAPACITY="$disk_capacity" ROOT_SIZE="$root_size" \
"$repo_root/tests/system/run-phase11-shepherd-pid1-qemu.sh"
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
serial_log=$(sed -n 's/^serial_log=//p' "$inner_metadata")
ssh_port=$(sed -n 's/^ssh_port=//p' "$inner_metadata")
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$phase8_metadata")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$phase8_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$phase8_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$phase8_metadata")
[ "$native_base_store_count" = 2 ] || { echo "expected 2 native base stores, got: $native_base_store_count" >&2; exit 1; }
[ "$host_base_store_count" = 1 ] || { echo "expected 1 host base store, got: $host_base_store_count" >&2; exit 1; }
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-kernel-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native kernel" >&2
exit 1
}
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native world" >&2
exit 1
}
printf '%s\n' "$host_base_stores" | tr ',' '\n' | grep 'freebsd-bootloader-15.0-STABLE$' >/dev/null || {
echo "host base stores do not reduce to the bootloader" >&2
exit 1
}
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
serial_log=$serial_log
ssh_port=$ssh_port
disk_capacity=$disk_capacity
root_size=$root_size
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
native_base_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase13-native-base-qemu\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,111 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase13-native-base-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase13-operating-system}
root_size=${ROOT_SIZE:-6g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase13-native-xcpng.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase11-native-xcpng-inner-metadata.txt
metadata_file=$workdir/phase13-native-base-xcpng-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
"$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh"
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$phase8_metadata")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$phase8_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$phase8_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$phase8_metadata")
[ "$native_base_store_count" = 2 ] || { echo "expected 2 native base stores, got: $native_base_store_count" >&2; exit 1; }
[ "$host_base_store_count" = 1 ] || { echo "expected 1 host base store, got: $host_base_store_count" >&2; exit 1; }
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-kernel-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native kernel" >&2
exit 1
}
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native world" >&2
exit 1
}
printf '%s\n' "$host_base_stores" | tr ',' '\n' | grep 'freebsd-bootloader-15.0-STABLE$' >/dev/null || {
echo "host base stores do not reduce to the bootloader" >&2
exit 1
}
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
vm_id=$vm_id
vdi_id=$vdi_id
guest_ip=$guest_ip
root_size=$root_size
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
compat_prefix_shims=$compat_prefix_shims
guile_module_smoke=$guile_module_smoke
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
native_base_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase13-native-base-xcpng\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,103 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase14-native-boot-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase14-operating-system}
disk_capacity=${DISK_CAPACITY:-8g}
root_size=${ROOT_SIZE:-6g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase14-native-boot-qemu.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase14-native-boot-qemu-inner-metadata.txt
metadata_file=$workdir/phase14-native-boot-qemu-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" DISK_CAPACITY="$disk_capacity" ROOT_SIZE="$root_size" \
"$repo_root/tests/system/run-phase11-shepherd-pid1-qemu.sh"
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
serial_log=$(sed -n 's/^serial_log=//p' "$inner_metadata")
ssh_port=$(sed -n 's/^ssh_port=//p' "$inner_metadata")
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$phase8_metadata")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$phase8_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$phase8_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$phase8_metadata")
[ "$native_base_store_count" = 2 ] || { echo "expected 2 native base stores, got: $native_base_store_count" >&2; exit 1; }
[ "$host_base_store_count" = 0 ] || { echo "expected 0 host base stores, got: $host_base_store_count" >&2; exit 1; }
[ -z "$host_base_stores" ] || { echo "host base stores are not empty: $host_base_stores" >&2; exit 1; }
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-kernel-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native kernel" >&2
exit 1
}
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native world" >&2
exit 1
}
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
serial_log=$serial_log
ssh_port=$ssh_port
disk_capacity=$disk_capacity
root_size=$root_size
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
native_boot_assets=freebsd-native-world
native_base_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase14-native-boot-qemu\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,109 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase14-native-boot-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase14-operating-system}
root_size=${ROOT_SIZE:-6g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase14-native-boot-xcpng.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase14-native-boot-xcpng-inner-metadata.txt
metadata_file=$workdir/phase14-native-boot-xcpng-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
"$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh"
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$phase8_metadata")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$phase8_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$phase8_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$phase8_metadata")
[ "$native_base_store_count" = 2 ] || { echo "expected 2 native base stores, got: $native_base_store_count" >&2; exit 1; }
[ "$host_base_store_count" = 0 ] || { echo "expected 0 host base stores, got: $host_base_store_count" >&2; exit 1; }
[ -z "$host_base_stores" ] || { echo "host base stores are not empty: $host_base_stores" >&2; exit 1; }
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-kernel-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native kernel" >&2
exit 1
}
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native world" >&2
exit 1
}
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
vm_id=$vm_id
vdi_id=$vdi_id
guest_ip=$guest_ip
root_size=$root_size
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
compat_prefix_shims=$compat_prefix_shims
guile_module_smoke=$guile_module_smoke
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
native_boot_assets=freebsd-native-world
native_base_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase14-native-boot-xcpng\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,142 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
store_dir=${STORE_DIR:-/frx/store}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase14-native-development.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
metadata_file=$workdir/phase14-native-development-split-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
action_env() {
sudo env \
HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
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}" \
"$@"
}
native_system_packages=$(GUILE_AUTO_COMPILE=0 /tmp/guile-freebsd-validate-install/bin/guile -L modules -L "$HOME/repos/guix" -c '
(use-modules (fruix packages freebsd) (srfi srfi-13))
(display (string-join (map freebsd-package-name %freebsd-native-system-packages) ","))
(newline)')
native_development_packages=$(GUILE_AUTO_COMPILE=0 /tmp/guile-freebsd-validate-install/bin/guile -L modules -L "$HOME/repos/guix" -c '
(use-modules (fruix packages freebsd) (srfi srfi-13))
(display (string-join (map freebsd-package-name %freebsd-native-development-profile-packages) ","))
(newline)')
materialize_native_package() {
package_name=$1
action_env env LD_LIBRARY_PATH="/tmp/guile-freebsd-validate-install/lib:/usr/local/lib" /tmp/guile-freebsd-validate-install/bin/guile -L modules -L "$HOME/repos/guix" -c '
(use-modules (fruix packages freebsd) (ice-9 hash-table))
(let* ((args (command-line))
(store-dir (list-ref args (- (length args) 2)))
(package-name (list-ref args (- (length args) 1)))
(pkg (case (string->symbol package-name)
((freebsd-native-bootloader) freebsd-native-bootloader)
((freebsd-native-headers) freebsd-native-headers)
(else (error "unsupported package" package-name))))
(system-module (resolve-module (quote (fruix system freebsd))))
(materialize (module-ref system-module (quote materialize-freebsd-package)))
(cache (make-hash-table))
(path (materialize pkg store-dir cache)))
(display "STORE_PATH=")
(display path)
(newline))' dummy "$store_dir" "$package_name"
}
bootloader_store=$(materialize_native_package freebsd-native-bootloader | sed -n 's/^STORE_PATH=//p' | tail -n 1)
headers_store=$(materialize_native_package freebsd-native-headers | sed -n 's/^STORE_PATH=//p' | tail -n 1)
case "$bootloader_store" in
/frx/store/*-freebsd-native-bootloader-15.0-STABLE) : ;;
*) echo "unexpected native bootloader store path: $bootloader_store" >&2; exit 1 ;;
esac
case "$headers_store" in
/frx/store/*-freebsd-native-headers-15.0-STABLE) : ;;
*) echo "unexpected native headers store path: $headers_store" >&2; exit 1 ;;
esac
for path in \
"$bootloader_store/boot/loader" \
"$bootloader_store/boot/loader.efi" \
"$bootloader_store/boot/device.hints" \
"$bootloader_store/boot/defaults/loader.conf" \
"$bootloader_store/boot/lua/loader.lua" \
"$headers_store/usr/include/stdio.h" \
"$headers_store/usr/include/sys/param.h" \
"$headers_store/usr/share/mk/bsd.prog.mk"
do
[ -e "$path" ] || {
echo "expected native split artifact path missing: $path" >&2
exit 1
}
done
[ ! -e "$bootloader_store/usr/include" ] || { echo "native bootloader slice unexpectedly contains headers" >&2; exit 1; }
[ ! -e "$headers_store/boot" ] || { echo "native headers slice unexpectedly contains /boot" >&2; exit 1; }
[ ! -e "$headers_store/bin/sh" ] || { echo "native headers slice unexpectedly contains runtime binaries" >&2; exit 1; }
case ",$native_system_packages," in
*,freebsd-native-runtime,*) : ;;
*) echo "native system package set does not include freebsd-native-runtime" >&2; exit 1 ;;
esac
case ",$native_development_packages," in
*,freebsd-native-runtime,*) : ;;
*) echo "native development package set does not include freebsd-native-runtime" >&2; exit 1 ;;
esac
case ",$native_development_packages," in
*,freebsd-native-headers,*) : ;;
*) echo "native development package set does not include freebsd-native-headers" >&2; exit 1 ;;
esac
case ",$native_development_packages," in
*,freebsd-clang-toolchain,*) : ;;
*) echo "native development package set does not retain the explicit toolchain artifact" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
store_dir=$store_dir
bootloader_store=$bootloader_store
headers_store=$headers_store
native_system_packages=$native_system_packages
native_development_packages=$native_development_packages
runtime_vs_development_split=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase14-native-development-split\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

View File

@@ -0,0 +1,124 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase14-native-runtime-pid1-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase14-operating-system}
disk_capacity=${DISK_CAPACITY:-12g}
root_size=${ROOT_SIZE:-10g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase14-native-runtime-qemu.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase14-native-runtime-qemu-inner-metadata.txt
metadata_file=$workdir/phase14-native-runtime-qemu-metadata.txt
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" DISK_CAPACITY="$disk_capacity" ROOT_SIZE="$root_size" \
"$repo_root/tests/system/run-phase11-shepherd-pid1-qemu.sh"
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
serial_log=$(sed -n 's/^serial_log=//p' "$inner_metadata")
ssh_port=$(sed -n 's/^ssh_port=//p' "$inner_metadata")
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
native_base_store_count=$(sed -n 's/^native_base_store_count=//p' "$phase8_metadata")
native_base_stores=$(sed -n 's/^native_base_stores=//p' "$phase8_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$phase8_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$phase8_metadata")
[ "$native_base_store_count" = 3 ] || { echo "expected 3 native base stores, got: $native_base_store_count" >&2; exit 1; }
[ "$host_base_store_count" = 0 ] || { echo "expected 0 host base stores, got: $host_base_store_count" >&2; exit 1; }
[ -z "$host_base_stores" ] || { echo "host base stores are not empty: $host_base_stores" >&2; exit 1; }
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-kernel-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native kernel" >&2
exit 1
}
printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-world-15.0-STABLE$' >/dev/null || {
echo "native base stores do not include the native boot/world source artifact" >&2
exit 1
}
runtime_store=$(printf '%s\n' "$native_base_stores" | tr ',' '\n' | grep 'freebsd-native-runtime-15.0-STABLE$' | head -n 1)
[ -n "$runtime_store" ] || {
echo "native base stores do not include the explicit native runtime slice" >&2
exit 1
}
for path in \
"$runtime_store/bin/sh" \
"$runtime_store/sbin/init" \
"$runtime_store/etc/rc" \
"$runtime_store/usr/sbin/sshd" \
"$runtime_store/sbin/dhclient" \
"$runtime_store/usr/bin/ssh-keygen" \
"$runtime_store/usr/share/locale/C.UTF-8/LC_CTYPE"
do
[ -e "$path" ] || {
echo "required native runtime path missing: $path" >&2
exit 1
}
done
[ ! -e "$runtime_store/boot" ] || { echo "native runtime still contains /boot" >&2; exit 1; }
[ ! -e "$runtime_store/usr/include" ] || { echo "native runtime still contains /usr/include" >&2; exit 1; }
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
serial_log=$serial_log
ssh_port=$ssh_port
disk_capacity=$disk_capacity
root_size=$root_size
runtime_store=$runtime_store
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
native_runtime_ready=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase14-native-runtime-qemu\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"

Some files were not shown because too many files have changed in this diff Show More