diff --git a/bin/fruix b/bin/fruix new file mode 100755 index 0000000..688bd9b --- /dev/null +++ b/bin/fruix @@ -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" "$@" diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index dd67e1b..4f05387 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2291,3 +2291,65 @@ Next recommended step: - login-class warnings around `daemon` - the `gpart: Unknown command: show` rc noise - residual syslog/cron/runtime polish issues where they still matter + +## 2026-04-02 — Phase 10.1: added a real `fruix system` command + +Completed work: + +- started Optional Phase 10 with the first user-facing Fruix system-management command surface +- wrote the subphase report: + - `docs/reports/phase10-fruix-system-command-freebsd.md` +- added a real CLI wrapper: + - `bin/fruix` +- added the corresponding Guile entry point: + - `scripts/fruix.scm` +- added a dedicated validation harness: + - `tests/system/run-phase10-fruix-system-command.sh` + +What the new command does: + +- exposes declarative FreeBSD system artifact generation through a Fruix CLI instead of only phase-specific harness scripts +- currently supports: + - `fruix system build OS-FILE` + - `fruix system image OS-FILE` + - `fruix system rootfs OS-FILE ROOTFS-DIR` +- currently supports options: + - `--system NAME` + - `--store DIR` + - `--disk-capacity SIZE` + - `--rootfs DIR` + - `--help` +- loads an operating-system object from a Scheme file, validates it, and dispatches to: + - `materialize-operating-system` + - `materialize-rootfs` + - `materialize-bhyve-image` +- emits machine-readable `key=value` metadata for the produced artifacts + +Important findings: + +- the project already had the core declarative system machinery by the end of Phase 9; the missing piece here was a direct Fruix operator interface to that machinery +- a small dedicated wrapper in this repo is currently the most practical way to expose that functionality without waiting for deeper upstream command-framework integration +- keeping the command aligned with the already validated local FreeBSD Guile / Fibers / Shepherd toolchain avoids introducing a second, divergent runtime path + +Validation: + +- `tests/system/run-phase10-fruix-system-command.sh` passes +- passing run workdir: + - `/tmp/phase10-fruix-cmd-1775117490` +- the test validated that: + - `fruix system build` materializes a closure under `/frx/store/*-fruix-system-fruix-freebsd` + - the produced closure contains the activation path + - `fruix system image` materializes a disk image under `/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img` + - the command returns expected metadata fields for both actions + +Current assessment: + +- Fruix now has a real user-facing system command in this repo, which is a concrete step from “prototype scripts” toward “OS tooling” +- this does not replace all earlier harnesses yet, but it establishes `fruix system` as the new canonical interface for system closure/rootfs/image materialization work +- system/image creation still typically requires `sudo` because the command writes into `/frx/store` and uses privileged image-building operations + +Next recommended step: + +1. continue Phase 10 by making more of the existing system workflows call `bin/fruix` directly instead of bespoke phase scripts +2. reduce the current runtime compatibility shims for locally built Guile / Shepherd prefixes and move toward a more native store-path-aware Fruix runtime arrangement +3. consider adding the next operator-facing subcommand on top of the now-working image path, such as a `vm`/deploy-oriented flow for the active XCP-ng workflow diff --git a/docs/reports/phase10-fruix-system-command-freebsd.md b/docs/reports/phase10-fruix-system-command-freebsd.md new file mode 100644 index 0000000..743df81 --- /dev/null +++ b/docs/reports/phase10-fruix-system-command-freebsd.md @@ -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 diff --git a/scripts/fruix.scm b/scripts/fruix.scm new file mode 100644 index 0000000..d7f5e05 --- /dev/null +++ b/scripts/fruix.scm @@ -0,0 +1,217 @@ +#!/tmp/guile-freebsd-validate-install/bin/guile -s +!# + +(use-modules (fruix system freebsd) + (ice-9 format) + (ice-9 match) + (srfi srfi-1) + (srfi srfi-13)) + +(define (usage code) + (format (if (= code 0) #t (current-error-port)) + "Usage: fruix system ACTION OS-FILE [OPTIONS]\n\ +\n\ +Actions:\n\ + build Materialize the Fruix system closure in /frx/store.\n\ + image Materialize the Fruix disk image in /frx/store.\n\ + rootfs Materialize a rootfs tree at --rootfs DIR or ROOTFS-DIR.\n\ +\n\ +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' (example: 30g).\n\ + --rootfs DIR Rootfs target for 'rootfs'.\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 (lookup-bound-value module symbol) + (let ((var (module-variable module symbol))) + (and var (variable-ref var)))) + +(define candidate-operating-system-symbols + '(operating-system + phase10-operating-system + phase9-operating-system + phase8-operating-system + phase7-operating-system + default-operating-system + os)) + +(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 (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 (parse-arguments argv) + (match argv + ((_) + (usage 1)) + ((_ "--help") + (usage 0)) + ((_ "help") + (usage 0)) + ((_ "system" "--help") + (usage 0)) + ((_ "system" action . rest) + (let loop ((args rest) + (positional '()) + (system-name #f) + (store-dir "/frx/store") + (disk-capacity #f) + (rootfs #f)) + (match args + (() + (let ((positional (reverse positional))) + `((action . ,action) + (positional . ,positional) + (system-name . ,system-name) + (store-dir . ,store-dir) + (disk-capacity . ,disk-capacity) + (rootfs . ,rootfs)))) + (("--help") + (usage 0)) + (((? (lambda (arg) (string-prefix? "--system=" arg)) arg) . tail) + (loop tail positional (option-value arg "--system=") store-dir disk-capacity rootfs)) + (("--system" value . tail) + (loop tail positional value store-dir disk-capacity rootfs)) + (((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail) + (loop tail positional system-name (option-value arg "--store=") disk-capacity rootfs)) + (("--store" value . tail) + (loop tail positional system-name value disk-capacity rootfs)) + (((? (lambda (arg) (string-prefix? "--disk-capacity=" arg)) arg) . tail) + (loop tail positional system-name store-dir (option-value arg "--disk-capacity=") rootfs)) + (("--disk-capacity" value . tail) + (loop tail positional system-name store-dir value rootfs)) + (((? (lambda (arg) (string-prefix? "--rootfs=" arg)) arg) . tail) + (loop tail positional system-name store-dir disk-capacity (option-value arg "--rootfs="))) + (("--rootfs" value . tail) + (loop tail positional system-name store-dir disk-capacity value)) + (((? (lambda (arg) (string-prefix? "--" arg)) arg) . _) + (error "unknown option" arg)) + ((arg . tail) + (loop tail (cons arg positional) system-name store-dir disk-capacity rootfs))))) + ((_ . _) + (usage 1)))) + +(define (main argv) + (let* ((parsed (parse-arguments argv)) + (action (assoc-ref parsed 'action)) + (positional (assoc-ref parsed 'positional)) + (store-dir (assoc-ref parsed 'store-dir)) + (disk-capacity (assoc-ref parsed 'disk-capacity)) + (rootfs-opt (assoc-ref parsed 'rootfs)) + (system-name (assoc-ref parsed 'system-name)) + (requested-symbol (and system-name (string->symbol system-name)))) + (cond + ((member action '("build" "image" "rootfs")) #t) + (else (error "unknown system action" action))) + (let* ((os-file (match positional + ((file . _) file) + (() (error "missing operating-system file argument")))) + (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"))) + (cond + ((string=? action "build") + (let* ((result (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 result 'closure-path)) + (generated-files (assoc-ref result 'generated-files)) + (references (assoc-ref result 'references))) + (emit-metadata + `((action . "build") + (os_file . ,os-file) + (system_variable . ,resolved-symbol) + (store_dir . ,store-dir) + (closure_path . ,closure-path) + (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)) + (generated_file_count . ,(length generated-files)) + (reference_count . ,(length references)))))) + ((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))) + (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") + (let* ((result (materialize-bhyve-image os + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix + #:disk-capacity disk-capacity)) + (image-spec (assoc-ref result 'image-spec)) + (store-items (assoc-ref result 'store-items))) + (emit-metadata + `((action . "image") + (os_file . ,os-file) + (system_variable . ,resolved-symbol) + (store_dir . ,store-dir) + (disk_capacity . ,(assoc-ref image-spec 'disk-capacity)) + (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)) + (store_item_count . ,(length store-items))))))))))))) + +(main (command-line)) diff --git a/tests/system/run-phase10-fruix-system-command.sh b/tests/system/run-phase10-fruix-system-command.sh new file mode 100755 index 0000000..ca4fdbb --- /dev/null +++ b/tests/system/run-phase10-fruix-system-command.sh @@ -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" <