From 514ec97f6bb4bac38701a5a30fecb04cfa3a0bee Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 18:49:40 +0200 Subject: [PATCH] Validate FreeBSD Fruix rootfs trees --- docs/PROGRESS.md | 81 ++++++++++++ docs/reports/phase7-rootfs-freebsd.md | 78 ++++++++++++ tests/system/materialize-phase7-rootfs.scm | 139 +++++++++++++++++++++ tests/system/run-phase7-rootfs.sh | 79 ++++++++++++ tests/system/run-phase7-system-closure.sh | 2 +- 5 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 docs/reports/phase7-rootfs-freebsd.md create mode 100644 tests/system/materialize-phase7-rootfs.scm create mode 100755 tests/system/run-phase7-rootfs.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1cc3cc6..dfb6291 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1960,6 +1960,87 @@ Current assessment: - Phase 7.2 is now satisfied on the current FreeBSD prototype track - the next step is to materialize and statically validate an installable root filesystem tree from this system closure +## 2026-04-01 — Phase 7.3 completed: installable rootfs tree validated from the system closure + +Completed work: + +- added the Phase 7.3 rootfs materialization harnesses: + - `tests/system/materialize-phase7-rootfs.scm` + - `tests/system/run-phase7-rootfs.sh` +- wrote the Phase 7.3 report: + - `docs/reports/phase7-rootfs-freebsd.md` +- ran the rootfs harness successfully and captured metadata under: + - `/tmp/phase7-rootfs-metadata.txt` + +Important findings: + +- the declarative Fruix FreeBSD system can now be materialized as a root filesystem tree rather than only as a store closure directory +- the rootfs uses a Guix-like anchor: + - `/run/current-system` + so that boot assets, generated configuration, and system-profile content remain tied to the declarative system closure +- static validation confirmed: + - boot asset linkage + - generated `/etc` linkage + - activation payload presence + - Shepherd `rc.d` launch integration + - declared filesystem entries + - declared user/group provisioning in the activation path + - deterministic ready-state wiring through `/var/lib/fruix/ready` +- observed metadata confirmed: + - `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` + +Current assessment: + +- Phase 7.3 is now satisfied on the current FreeBSD prototype track +- Phase 7 as a whole is now complete on the active FreeBSD amd64 prototype path + +## 2026-04-01 — Phase 7 completed on the current FreeBSD prototype track + +Phase 7 is now considered complete for the active FreeBSD amd64 prototype path. + +Why this milestone is satisfied: + +- **Phase 7.1** success criteria were met on the prototype track: + - a minimal Fruix operating-system object now exists for FreeBSD + - it evaluates into a coherent system-closure specification +- **Phase 7.2** success criteria were met on the prototype track: + - that system model now materializes into a reproducible closure under `/frx/store` + - the closure contains boot assets, generated `/etc` files, activation payloads, and Shepherd launch integration +- **Phase 7.3** success criteria were met on the prototype track: + - the closure now materializes into a concrete rootfs tree + - the resulting rootfs passes static validation for later image-construction work + +Important scope note: + +- this completes the **declarative system-composition milestone** for the current prototype track, not a fully booted Fruix guest yet +- the current output is a validated closure plus rootfs tree; Phase 8 still needs to turn that into a reproducible bhyve-friendly disk image +- the chosen first system-init strategy remains: + - FreeBSD init + `rc.d` launching Shepherd + rather than Shepherd-as-PID-1 +- the current system model remains Fruix-owned and FreeBSD-oriented rather than attempting full upstream Guix System integration prematurely + +Next recommended step: + +1. begin Phase 8.1 by creating a reproducible disk-image build path from the generated Fruix rootfs tree +2. keep the current init decision explicit for the first boot target: + - FreeBSD init + `rc.d` + Shepherd +3. continue preserving the selective Fruix naming policy: + - Fruix at the product boundary + - `/frx` as the canonical store root + - stable upstream-derived internal names unless there is strong architectural value in renaming them + Current assessment: - Phase 7.1 is now satisfied on the current FreeBSD prototype track diff --git a/docs/reports/phase7-rootfs-freebsd.md b/docs/reports/phase7-rootfs-freebsd.md new file mode 100644 index 0000000..a46c442 --- /dev/null +++ b/docs/reports/phase7-rootfs-freebsd.md @@ -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 diff --git a/tests/system/materialize-phase7-rootfs.scm b/tests/system/materialize-phase7-rootfs.scm new file mode 100644 index 0000000..5edbc1e --- /dev/null +++ b/tests/system/materialize-phase7-rootfs.scm @@ -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))) diff --git a/tests/system/run-phase7-rootfs.sh b/tests/system/run-phase7-rootfs.sh new file mode 100755 index 0000000..e39b782 --- /dev/null +++ b/tests/system/run-phase7-rootfs.sh @@ -0,0 +1,79 @@ +#!/bin/sh +set -eu + +project_root=${PROJECT_ROOT:-$(pwd)} +guix_source_dir=${GUIX_SOURCE_DIR:-"$HOME/repos/guix"} +script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +runner_scm=$script_dir/materialize-phase7-rootfs.scm +os_file=$script_dir/phase7-minimal-operating-system.scm +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} +store_dir=${STORE_DIR:-/frx/store} +metadata_target=${METADATA_OUT:-} + +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 +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase7-rootfs.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 + +if [ -n "${GUILE_LOAD_PATH:-}" ]; then + gui_load_path="$project_root/modules:$guix_source_dir:$GUILE_LOAD_PATH" +else + gui_load_path="$project_root/modules:$guix_source_dir" +fi + +printf 'Using Guile: %s\n' "$guile_bin" +printf 'Working directory: %s\n' "$workdir" +printf 'Store directory: %s\n' "$store_dir" + +sudo env \ + GUILE_AUTO_COMPILE=0 \ + GUILE_LOAD_PATH="$gui_load_path" \ + LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + WORKDIR="$workdir" \ + OS_FILE="$os_file" \ + STORE_DIR="$store_dir" \ + GUILE_PREFIX="$guile_prefix" \ + GUILE_EXTRA_PREFIX="$guile_extra_prefix" \ + SHEPHERD_PREFIX="$shepherd_prefix" \ + METADATA_OUT="$metadata_target" \ + "$guile_bin" -s "$runner_scm" diff --git a/tests/system/run-phase7-system-closure.sh b/tests/system/run-phase7-system-closure.sh index 0e23f9f..32c02a8 100755 --- a/tests/system/run-phase7-system-closure.sh +++ b/tests/system/run-phase7-system-closure.sh @@ -50,7 +50,7 @@ fi cleanup_workdir() { if [ "$cleanup" -eq 1 ]; then - rm -rf "$workdir" + rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir" fi } trap cleanup_workdir EXIT INT TERM