diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 257536d..cec44b6 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2082,3 +2082,75 @@ Current assessment: - Phase 8.1 is now satisfied on the current FreeBSD prototype track - the next step is to integrate this image generation path into the declarative Fruix system-composition layer so that a single operating-system description can drive image generation end-to-end + +## 2026-04-01 — Phase 8.2 completed: image generation integrated into the declarative system layer + +Completed work: + +- extended the FreeBSD system module with integrated image-generation operations: + - `operating-system-image-spec` + - `materialize-bhyve-image` +- added the Phase 8.2 integration harnesses: + - `tests/system/materialize-phase8-system-image.scm` + - `tests/system/run-phase8-system-image.sh` +- wrote the Phase 8.2 report: + - `docs/reports/phase8-system-image-freebsd.md` +- ran the integrated system-image harness successfully and captured metadata under: + - `/tmp/phase8-system-image-metadata.txt` + +Important findings: + +- image generation is now a direct output of the Fruix FreeBSD system-definition layer rather than an external shell-only follow-up to Phase 7 +- the integrated path now stores the resulting image artifact itself under `/frx/store`, preserving the store-centered Fruix composition story even at the VM-image layer +- rerunning `materialize-bhyve-image` for the same operating-system description produced the same image store path, which is the current prototype proof that one declarative system object can drive image generation end-to-end +- observed metadata confirmed: + - `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` + +Current assessment: + +- Phase 8.2 is now satisfied on the current FreeBSD prototype track +- Phase 8 as a whole is now complete on the active FreeBSD amd64 prototype path + +## 2026-04-01 — Phase 8 completed on the current FreeBSD prototype track + +Phase 8 is now considered complete for the active FreeBSD amd64 prototype path. + +Why this milestone is satisfied: + +- **Phase 8.1** success criteria were met on the prototype track: + - a reproducible raw GPT+UEFI+UFS image can now be generated from the Fruix system outputs + - that image passes static boot-structure sanity checks +- **Phase 8.2** success criteria were met on the prototype track: + - the image builder is now integrated with the declarative Fruix system-definition layer + - a single operating-system description now drives image generation end-to-end + - the integrated output is itself a store-backed Fruix image artifact under `/frx/store` + +Important scope note: + +- this completes the **image-construction milestone** for the current prototype track, not the first successful bhyve boot yet +- the generated image is now ready for the next phase’s VM-launch and serial-console validation work +- the current first-boot strategy remains explicit and unchanged: + - FreeBSD init + `rc.d` + Shepherd +- the image path still reflects the current prototype system/runtime limitations, including the fact that deeper runtime closure completeness for locally copied Guile/Shepherd dependencies will be exercised more fully in Phase 9 boot validation + +Next recommended step: + +1. begin Phase 9.1 by creating a bhyve launcher and serial-console validation harness for the generated image +2. keep the current deterministic ready-state target visible: + - Shepherd startup leading to the generated `/var/lib/fruix/ready` marker path +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 diff --git a/docs/reports/phase8-system-image-freebsd.md b/docs/reports/phase8-system-image-freebsd.md new file mode 100644 index 0000000..241c8f1 --- /dev/null +++ b/docs/reports/phase8-system-image-freebsd.md @@ -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 project’s 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 diff --git a/modules/fruix/system/freebsd.scm b/modules/fruix/system/freebsd.scm index ac85853..0bb17f0 100644 --- a/modules/fruix/system/freebsd.scm +++ b/modules/fruix/system/freebsd.scm @@ -47,8 +47,10 @@ operating-system-ready-marker validate-operating-system operating-system-closure-spec + operating-system-image-spec materialize-operating-system materialize-rootfs + materialize-bhyve-image default-minimal-operating-system)) (define-record-type @@ -761,3 +763,174 @@ (closure-path . ,closure-path) (ready-marker . ,(operating-system-ready-marker os)) (rc-script . ,(string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd"))))) + +(define* (operating-system-image-spec os + #:key + (boot-mode 'uefi) + (image-format 'raw) + (partition-scheme 'gpt) + (efi-size "64m") + (root-size "256m") + (efi-partition-label "efiboot") + (root-partition-label "fruix-root") + (serial-console "comconsole")) + `((host-name . ,(operating-system-host-name os)) + (boot-mode . ,boot-mode) + (image-format . ,image-format) + (partition-scheme . ,partition-scheme) + (efi-size . ,efi-size) + (root-size . ,root-size) + (efi-partition-label . ,efi-partition-label) + (root-partition-label . ,root-partition-label) + (serial-console . ,serial-console) + (init-mode . freebsd-init+rc.d-shepherd))) + +(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)) + +(define* (materialize-bhyve-image os + #:key + (store-dir "/frx/store") + (guile-prefix "/tmp/guile-freebsd-validate-install") + (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") + (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (efi-size "64m") + (root-size "256m") + (efi-partition-label "efiboot") + (root-partition-label "fruix-root") + (serial-console "comconsole")) + (let* ((closure (materialize-operating-system os + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix)) + (closure-path (assoc-ref closure 'closure-path)) + (image-spec (operating-system-image-spec os + #:efi-size efi-size + #:root-size root-size + #:efi-partition-label efi-partition-label + #:root-partition-label root-partition-label + #:serial-console serial-console)) + (store-items (store-reference-closure (list closure-path))) + (manifest (string-append + "image-spec=\n" + (object->string image-spec) + "closure-path=\n" + closure-path + "\nstore-items=\n" + (string-join store-items "\n") + "\n")) + (hash (string-hash manifest)) + (image-store-path (string-append store-dir "/" hash "-fruix-bhyve-image-" + (operating-system-host-name os))) + (disk-image (string-append image-store-path "/disk.img")) + (esp-image (string-append image-store-path "/esp.img")) + (root-image (string-append image-store-path "/root.ufs"))) + (unless (file-exists? image-store-path) + (let* ((build-root (mktemp-directory "/tmp/fruix-bhyve-image-build.XXXXXX")) + (rootfs (string-append build-root "/rootfs")) + (image-rootfs (string-append build-root "/image-rootfs")) + (esp-stage (string-append build-root "/esp-stage")) + (temp-output (mktemp-directory (string-append store-dir "/.fruix-bhyve-image.XXXXXX"))) + (temp-disk (string-append build-root "/disk.img")) + (temp-esp (string-append build-root "/esp.img")) + (temp-root (string-append build-root "/root.ufs"))) + (dynamic-wind + (lambda () #t) + (lambda () + (materialize-rootfs os rootfs + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix) + (copy-rootfs-for-image rootfs image-rootfs) + (copy-store-items-into-rootfs image-rootfs store-dir store-items) + (mkdir-p (string-append esp-stage "/EFI/BOOT")) + (copy-regular-file (string-append closure-path "/boot/loader.efi") + (string-append esp-stage "/EFI/BOOT/BOOTX64.EFI")) + (run-command "makefs" "-t" "ffs" "-T" "0" "-B" "little" + "-s" root-size + "-o" "label=fruix-root,version=2,bsize=32768,fsize=4096,density=16384" + temp-root image-rootfs) + (run-command "makefs" "-t" "msdos" "-T" "0" + "-o" "fat_type=32" + "-o" "sectors_per_cluster=1" + "-o" "volume_label=EFISYS" + "-o" "volume_id=305419896" + "-s" efi-size + temp-esp esp-stage) + (run-command "mkimg" "-s" "gpt" "-f" "raw" "-t" "0" + "-p" (string-append "efi/" efi-partition-label ":=" temp-esp) + "-p" (string-append "freebsd-ufs/" root-partition-label ":=" temp-root) + "-o" temp-disk) + (mkdir-p temp-output) + (copy-regular-file temp-disk (string-append temp-output "/disk.img")) + (copy-regular-file temp-esp (string-append temp-output "/esp.img")) + (copy-regular-file temp-root (string-append temp-output "/root.ufs")) + (write-file (string-append temp-output "/image-spec.scm") (object->string image-spec)) + (write-file (string-append temp-output "/closure-path") closure-path) + (write-file (string-append temp-output "/.references") (string-join store-items "\n")) + (write-file (string-append temp-output "/.fruix-package") manifest) + (chmod temp-output #o755) + (for-each (lambda (path) + (chmod path #o644)) + (list (string-append temp-output "/disk.img") + (string-append temp-output "/esp.img") + (string-append temp-output "/root.ufs") + (string-append temp-output "/image-spec.scm") + (string-append temp-output "/closure-path") + (string-append temp-output "/.references") + (string-append temp-output "/.fruix-package"))) + (rename-file temp-output image-store-path)) + (lambda () + (when (file-exists? build-root) + (delete-file-recursively build-root)))))) + `((image-store-path . ,image-store-path) + (disk-image . ,disk-image) + (esp-image . ,esp-image) + (root-image . ,root-image) + (closure-path . ,closure-path) + (image-spec . ,image-spec) + (store-items . ,store-items)))) diff --git a/tests/system/materialize-phase8-system-image.scm b/tests/system/materialize-phase8-system-image.scm new file mode 100644 index 0000000..a71b4af --- /dev/null +++ b/tests/system/materialize-phase8-system-image.scm @@ -0,0 +1,103 @@ +(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 (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 (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 (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 "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))) diff --git a/tests/system/run-phase8-system-image.sh b/tests/system/run-phase8-system-image.sh new file mode 100755 index 0000000..d5a6d6d --- /dev/null +++ b/tests/system/run-phase8-system-image.sh @@ -0,0 +1,172 @@ +#!/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-phase8-system-image.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-phase8-system-image.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +build_metadata=$workdir/phase8-system-image-build-metadata.txt +metadata_file=$workdir/phase8-system-image-validation-metadata.txt +gpart_log=$workdir/gpart-show.txt +mnt_esp=$workdir/mnt-esp +mnt_root=$workdir/mnt-root +md_unit= + +cleanup_workdir() { + if [ -n "$md_unit" ]; then + sudo umount "$mnt_esp" >/dev/null 2>&1 || true + sudo umount "$mnt_root" >/dev/null 2>&1 || true + sudo mdconfig -d -u "$md_unit" >/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 + +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="$build_metadata" \ + "$guile_bin" -s "$runner_scm" + +image_store_path=$(sed -n 's/^image_store_path=//p' "$build_metadata") +disk_image=$(sed -n 's/^disk_image=//p' "$build_metadata") +closure_path=$(sed -n 's/^closure_path=//p' "$build_metadata") +raw_sha256=$(sed -n 's/^raw_sha256=//p' "$build_metadata") +image_size_bytes=$(sed -n 's/^image_size_bytes=//p' "$build_metadata") +store_item_count=$(sed -n 's/^store_item_count=//p' "$build_metadata") +closure_base=$(basename "$closure_path") + +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 + +md=$(sudo mdconfig -a -t vnode -f "$disk_image") +md_unit=${md#md} +sudo mkdir -p "$mnt_esp" "$mnt_root" +sudo gpart show -lp "/dev/$md" >"$gpart_log" +esp_fstype=$(sudo fstyp "/dev/${md}p1") +root_fstype=$(sudo fstyp "/dev/${md}p2") +[ "$esp_fstype" = msdosfs ] || { echo "unexpected ESP filesystem: $esp_fstype" >&2; exit 1; } +[ "$root_fstype" = ufs ] || { echo "unexpected root filesystem: $root_fstype" >&2; exit 1; } +sudo mount -t msdosfs "/dev/${md}p1" "$mnt_esp" +sudo mount -t ufs -o ro "/dev/${md}p2" "$mnt_root" + +[ -f "$mnt_esp/EFI/BOOT/BOOTX64.EFI" ] || { echo "missing EFI boot file in integrated image" >&2; exit 1; } +run_current_system_target=$(readlink "$mnt_root/run/current-system") +boot_loader_target=$(readlink "$mnt_root/boot/loader") +boot_loader_conf_target=$(readlink "$mnt_root/boot/loader.conf") +rc_conf_target=$(readlink "$mnt_root/etc/rc.conf") +rc_script_target=$(readlink "$mnt_root/usr/local/etc/rc.d/fruix-shepherd") +[ "$run_current_system_target" = "/frx/store/$closure_base" ] || { echo "unexpected /run/current-system target: $run_current_system_target" >&2; exit 1; } +[ "$boot_loader_target" = /run/current-system/boot/loader ] || { echo "unexpected /boot/loader target: $boot_loader_target" >&2; exit 1; } +[ "$boot_loader_conf_target" = /run/current-system/boot/loader.conf ] || { echo "unexpected /boot/loader.conf target: $boot_loader_conf_target" >&2; exit 1; } +[ "$rc_conf_target" = /run/current-system/etc/rc.conf ] || { echo "unexpected /etc/rc.conf target: $rc_conf_target" >&2; exit 1; } +[ "$rc_script_target" = /run/current-system/usr/local/etc/rc.d/fruix-shepherd ] || { echo "unexpected fruix_shepherd rc target: $rc_script_target" >&2; exit 1; } +loader_conf_image=$mnt_root/frx/store/$closure_base/boot/loader.conf +rc_conf_image=$mnt_root/frx/store/$closure_base/etc/rc.conf +grep -F 'console="comconsole"' "$loader_conf_image" >/dev/null || { echo "loader.conf is missing serial console config" >&2; exit 1; } +grep -F 'hostname="fruix-freebsd"' "$rc_conf_image" >/dev/null || { echo "rc.conf is missing hostname" >&2; exit 1; } + +cat >"$metadata_file" <