Validate FreeBSD Fruix rootfs trees

This commit is contained in:
2026-04-01 18:49:40 +02:00
parent e4288dc330
commit 514ec97f6b
5 changed files with 378 additions and 1 deletions

View File

@@ -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

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,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,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"

View File

@@ -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