diff --git a/docs/GUIX_DIFFERENCES.md b/docs/GUIX_DIFFERENCES.md index 95fab5e..5a51a7f 100644 --- a/docs/GUIX_DIFFERENCES.md +++ b/docs/GUIX_DIFFERENCES.md @@ -174,6 +174,7 @@ 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 build-base` - `fruix system reconfigure` - `fruix system status` - `fruix system switch /frx/store/...-fruix-system-...` @@ -186,6 +187,7 @@ What this gives you today: - 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 `build-base` path that can stage a native-build result locally and promote it into the node's own `/frx/store` - 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: @@ -193,6 +195,7 @@ 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 +- a jail-backed local native-base executor; current installed-node `build-base` still uses the validated `self-hosted` executor - the broader generation-management UX Guix operators expect So if you come from Guix, assume that Fruix now has: @@ -200,7 +203,7 @@ 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 +- a real installed-system build/build-base/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 @@ -334,6 +337,8 @@ That means Fruix can now distinguish more explicitly between: 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. +Installed-node `fruix system build-base` now reuses that same self-hosted result/promotion model on the node itself. The next likely refinement is to add a `jail` executor for local native-base builds without changing the result or promotion shape. + ## 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. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index c5d84bc..929ffb2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -73,8 +73,10 @@ Fruix currently has: - installed systems that now carry their own canonical declaration inputs and bundled Fruix node CLI sources - a real XCP-ng validation of in-node: - `fruix system build` + - `fruix system build-base` - `fruix system reconfigure` - `fruix system rollback` +- local installed-node promotion of native-base results into the node's own `/frx/store` Validated boot modes still are: @@ -87,6 +89,37 @@ The validated Phase 18 installation work currently uses: ## Latest completed achievement +### 2026-04-06 — Installed Fruix nodes can now build and promote native base results locally + +Installed Fruix nodes now support a validated local: + +- `fruix system build-base` + +workflow on the approved real XCP-ng path. + +Highlights: + +- the installed helper at `/usr/local/bin/fruix` now supports: + - `fruix system build-base [--jobs COUNT] [--store DIR]` +- installed nodes can now: + - run the strict build-profile-based native base-build helper locally + - stage the mutable result under `/var/lib/fruix/native-builds/...` + - promote that result into immutable node-local `/frx/store/...` objects +- `operating-system-from-promoted-native-build-result` now accepts: + - `#:build-packages` + so build-capable installed nodes can be declared directly from promoted native-base identities +- this validation keeps the current running system generation unchanged while producing a new promoted native-base identity for later declaration/deployment use + +Validation: + +- `PASS postphase20-installed-node-build-base-xcpng` + +Reports: + +- `docs/reports/postphase20-installed-node-build-base-freebsd.md` +- `docs/system-deployment-workflow.md` +- `docs/GUIX_DIFFERENCES.md` + ### 2026-04-06 — Fruix now separates interactive development from strict native base-build environment Fruix now has a more explicit three-layer model for build-capable FreeBSD systems: @@ -206,11 +239,11 @@ Reports: The next practical follow-up is now clearer: -- grow the installed-node command surface from validated `build`/`reconfigure`/`rollback` toward: +- grow the installed-node command surface from validated `build`/`build-base`/`reconfigure`/`rollback` toward: - `upgrade` - - `build-base` - `deploy` - decide how executor policy, native-base promotion, and installed-node reconfiguration should compose in one operator-facing workflow +- turn the current local `self-hosted` build-base path into a cleaner first-class local executor story, with `jail` now the most promising next candidate - determine how much of native-build request/promotion should remain explicit versus being absorbed into higher-level Fruix node actions The immediate architectural direction is no longer just “can guest self-hosting work?” diff --git a/docs/reports/postphase20-installed-node-build-base-freebsd.md b/docs/reports/postphase20-installed-node-build-base-freebsd.md new file mode 100644 index 0000000..d8cee7c --- /dev/null +++ b/docs/reports/postphase20-installed-node-build-base-freebsd.md @@ -0,0 +1,189 @@ +# Post-Phase 20: installed-node `fruix system build-base` + +Date: 2026-04-06 + +## Goal + +Extend the installed-node Fruix command surface so an already installed Fruix machine can build and promote a native FreeBSD base result for itself. + +This is the next concrete step toward making installed systems feel like real Fruix nodes rather than only deployed images with generation-switch helpers. + +The target command was: + +- `fruix system build-base` + +with the following product properties: + +- run from inside the installed node +- use the node's own declared source/input state +- use the stricter build-profile contract rather than the interactive development profile +- stage a mutable native-build result under `/var/lib/fruix/native-builds/...` +- promote that result into immutable `/frx/store/...` objects on the node itself +- leave the current running system generation unchanged + +## What changed + +### Installed helper gained `fruix system build-base` + +The installed helper at: + +- `/usr/local/bin/fruix` + +now also supports: + +- `fruix system build-base [--jobs COUNT] [--store DIR]` + +Current behavior: + +1. call the installed self-hosted native-build helper: + - `/usr/local/bin/fruix-self-hosted-native-build` +2. let that helper stage a result root under: + - `/var/lib/fruix/native-builds/` +3. promote the staged result locally by invoking the bundled Fruix node CLI: + - `fruix native-build promote RESULT_ROOT --store /frx/store` +4. emit combined machine-readable metadata including both: + - staged result-root fields + - promoted store-object fields + +The command does **not** switch the running system generation. It only produces a promoted native-base identity that a later declaration or deployment action can consume. + +### Promoted-result declarations can now request build packages explicitly + +`operating-system-from-promoted-native-build-result` now accepts: + +- `#:build-packages` + +This makes it possible to define installed systems that boot from a promoted native-base result while also carrying the extra build-profile content needed for node-local `build-base` execution. + +### Installed-node command-surface identity bumped + +The installed system closure metadata marker: + +- `installed-system-command-surface-version` + +was bumped so updated closures cannot silently reuse an older installed helper implementation. + +## Validation assets + +Added: + +- `tests/system/postphase20-installed-node-build-base-operating-system.scm.in` +- `tests/system/run-postphase20-installed-node-build-base-xcpng.sh` + +The new declaration template builds an installed node from a promoted native-base result and explicitly adds both: + +- `development-packages` +- `build-packages` + +using the validated narrow toolchain overlay: + +- `freebsd-native-headers` +- `freebsd-clang-toolchain` + +## Validation performed + +Approved real XCP-ng path: + +- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289` +- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` + +Source promoted native-base result used to boot the installed node: + +- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest` + +Validated flow: + +1. boot an installed Fruix node built from that promoted native-base result +2. verify the node exposes: + - embedded declaration metadata + - bundled Fruix node CLI sources + - `/usr/local/bin/fruix-build-environment` + - `/usr/local/bin/fruix-self-hosted-native-build` + - `/run/current-system/build-profile` + - `/run/current-build` +3. run in-guest: + - `fruix system build-base --jobs 8` +4. confirm that the command: + - stages a result root under `/var/lib/fruix/native-builds/...` + - records promotion metadata + - promotes four artifact store objects plus a result-bundle store object + - keeps the current running system closure unchanged + +Passing run: + +- `PASS postphase20-installed-node-build-base-xcpng` + +Representative validated metadata: + +```text +closure_path=/frx/store/db7057b94c8286cf355548a71a35987178cf7537-fruix-system-fruix-node-build-base +run_id=20260406T175942Z +helper_version=5 +executor_kind=self-hosted +executor_name=guest-self-hosted +executor_version=5 +build_jobs=8 +build_profile=/run/current-system/build-profile +source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default +result_root=/var/lib/fruix/native-builds/20260406T175942Z +status_value=ok +result_store=/frx/store/db0da5e0c6cbd0c44bc6d961c1c6b6b246831528-fruix-native-build-result-15.0-STABLE-guest-self-hosted +world_store=/frx/store/f06cf041abd520f451beb8792064658b8bf7548a-fruix-native-world-15.0-STABLE-guest-self-hosted +kernel_store=/frx/store/ba9cc6e275396e8b3ab95888b156300384a684a3-fruix-native-kernel-15.0-STABLE-guest-self-hosted +headers_store=/frx/store/69346161ec2d11a0455f8b319fbf9ec98c6341cc-fruix-native-headers-15.0-STABLE-guest-self-hosted +bootloader_store=/frx/store/45796dd309dd25f2c4e4baff1fc6c620d0487f88-fruix-native-bootloader-15.0-STABLE-guest-self-hosted +guest_run_current=/frx/store/db7057b94c8286cf355548a71a35987178cf7537-fruix-system-fruix-node-build-base +installed_node_build_base=ok +``` + +## Important observation + +The current installed-node `build-base` implementation uses the already validated: + +- `self-hosted` + +native-build executor. + +That is intentional for this step. + +The product boundary needed here was: + +1. installed node can request a native base build locally +2. Fruix records the staged result root locally +3. Fruix validates and promotes that result into immutable local store objects + +The future `jail` executor is still a strong next direction, but it is not required for this milestone to be meaningful. + +So the current validated statement is: + +- an installed Fruix node can now build and promote a native base result for itself + +not yet: + +- local native base builds already run inside a jail-backed executor + +## Result + +Installed Fruix nodes now have a materially stronger operator-facing story. + +They can now: + +- read their own declaration/input state +- build their own system closures +- reconfigure and roll back generations +- build a native FreeBSD base result locally +- promote that result into immutable Fruix store identities on the node itself + +This closes an important part of the gap between: + +- “installed system with helper scripts” + +and + +- “real Fruix-managed node” + +The next natural follow-ups are now clearer: + +- define `fruix system deploy` +- define `fruix system upgrade` +- add a first-class `jail` executor for local native base builds diff --git a/docs/system-deployment-workflow.md b/docs/system-deployment-workflow.md index 1ded1be..e117270 100644 --- a/docs/system-deployment-workflow.md +++ b/docs/system-deployment-workflow.md @@ -179,6 +179,7 @@ Current validated in-guest commands are: ```sh fruix system build +fruix system build-base fruix system reconfigure fruix system status fruix system switch /frx/store/...-fruix-system-... @@ -193,15 +194,18 @@ Installed systems now carry canonical declaration state in: So the in-guest helper can now build from the node's own embedded declaration inputs. -Current validated build/reconfigure behavior is: +Current validated local-node behavior is: - `fruix system build` - with no extra arguments, builds from the embedded current declaration +- `fruix system build-base` + - stages a native FreeBSD base-build result under `/var/lib/fruix/native-builds/...` + - then promotes that result into the node's own `/frx/store/...` - `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` +- `build` and `reconfigure` can also take an explicit declaration file plus `--system NAME` -Current intended usage now has two validated patterns. +Current intended usage now has three validated patterns. ### Pattern A: build elsewhere, then switch/rollback locally @@ -250,6 +254,42 @@ fruix system reconfigure /path/to/candidate.scm --system my-operating-system 5. if needed, run `fruix system rollback` 6. reboot back into the recorded prior generation +### Pattern C: build and promote a native base result from the node itself + +On build-capable installed nodes, Fruix now also supports: + +```sh +fruix system build-base --jobs 8 +``` + +Current validated behavior: + +1. the node evaluates the stricter build helper: + - `/usr/local/bin/fruix-build-environment` +2. the node runs the self-hosted native-build helper: + - `/usr/local/bin/fruix-self-hosted-native-build` +3. Fruix records the staged result under: + - `/var/lib/fruix/native-builds/` +4. Fruix promotes that staged result into immutable node-local store objects under: + - `/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-...` +5. the current running system generation remains unchanged + +This gives the installed node a first local answer to: + +- read my declared inputs +- build a base result locally +- promote it into real Fruix identities + +The current validated executor for this path is still: + +- `self-hosted` + +A future `jail` executor can refine the isolation policy later without changing the staged-result or promotion model. + ### In-guest development and build environments Opt-in systems can now expose two separate overlays above the main runtime profile: @@ -397,7 +437,9 @@ Current validated flow: 1. run the in-guest helper so the guest records a result under: - `/var/lib/fruix/native-builds/` -2. copy that result root back to the host +2. either: + - copy that result root back to the host and run promotion there, or + - on an installed Fruix node, run `fruix system build-base` so the node promotes it locally 3. run: ```sh @@ -746,7 +788,7 @@ 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 +- richer installed-node workflows that compose `build-base`, `reconfigure`, and later deployment actions cleanly ## Provenance and deployment identity @@ -772,7 +814,8 @@ The deployment workflow is now coherent, and Fruix now has a validated installed 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 +- higher-level installed-node composition across `build`, `build-base`, `reconfigure`, and later deployment actions +- a `jail` executor for local native base builds; current validated node-local `build-base` uses `self-hosted` - multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers - generation pruning and retention policy independent of full redeploy @@ -787,6 +830,8 @@ The current canonical Fruix deployment model is: - **materialize** the artifact appropriate to the deployment target - **boot or install** that artifact - **identify deployments by closure path and provenance metadata** +- on installed systems, **build** or **reconfigure** from embedded declaration state when the node should manage itself locally +- on installed systems, **build-base** to stage and promote a node-local native-base result identity - 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 diff --git a/modules/fruix/system/freebsd/build.scm b/modules/fruix/system/freebsd/build.scm index 978d1ee..f4a3294 100644 --- a/modules/fruix/system/freebsd/build.scm +++ b/modules/fruix/system/freebsd/build.scm @@ -637,6 +637,7 @@ (bootloader #f) (base-packages #f) (development-packages #f) + (build-packages #f) (users #f) (groups #f) (file-systems #f) @@ -663,6 +664,8 @@ (promoted-native-build-result-base-packages result))) #:development-packages (fallback development-packages (lambda () (operating-system-development-packages defaults))) + #:build-packages (fallback build-packages (lambda () + (operating-system-build-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))) diff --git a/modules/fruix/system/freebsd/model.scm b/modules/fruix/system/freebsd/model.scm index 416f97c..5a7fee1 100644 --- a/modules/fruix/system/freebsd/model.scm +++ b/modules/fruix/system/freebsd/model.scm @@ -419,7 +419,7 @@ (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") + (installed-system-command-surface-version . "3") (bundled-fruix-node-cli-version . "1") (development-environment-helper-version . ,(if (null? (operating-system-development-packages os)) #f "1")) diff --git a/modules/fruix/system/freebsd/render.scm b/modules/fruix/system/freebsd/render.scm index 7e32099..02f18c9 100644 --- a/modules/fruix/system/freebsd/render.scm +++ b/modules/fruix/system/freebsd/render.scm @@ -556,6 +556,7 @@ " cat <<'EOF'\n" "Usage: fruix system status\n" " fruix system build [DECLARATION [--system NAME] ...]\n" + " fruix system build-base [--jobs COUNT] [--store DIR]\n" " fruix system reconfigure [DECLARATION [--system NAME] ...]\n" " fruix system switch /frx/store/...-fruix-system-...\n" " fruix system rollback\n" @@ -653,6 +654,79 @@ " run_node_cli system build \"$@\"\n" " fi\n" "}\n\n" + "build_base_system()\n" + "{\n" + " store_dir=$default_store_dir\n" + " jobs=\"\"\n" + " while [ $# -gt 0 ]; do\n" + " case \"$1\" in\n" + " --jobs)\n" + " [ $# -ge 2 ] || die \"build-base requires a value after --jobs\"\n" + " jobs=$2\n" + " shift 2\n" + " ;;\n" + " --jobs=*)\n" + " jobs=${1#--jobs=}\n" + " shift\n" + " ;;\n" + " --store)\n" + " [ $# -ge 2 ] || die \"build-base requires a value after --store\"\n" + " store_dir=$2\n" + " shift 2\n" + " ;;\n" + " --store=*)\n" + " store_dir=${1#--store=}\n" + " shift\n" + " ;;\n" + " *)\n" + " die \"unsupported build-base argument: $1\"\n" + " ;;\n" + " esac\n" + " done\n" + " case \"$jobs\" in\n" + " '')\n" + " ;;\n" + " *[!0-9]*)\n" + " die \"build-base job count must be numeric: $jobs\"\n" + " ;;\n" + " esac\n" + " [ -x /usr/local/bin/fruix-self-hosted-native-build ] || die \"build-base helper is missing: /usr/local/bin/fruix-self-hosted-native-build\"\n" + " build_output=$(mktemp /tmp/fruix-system-build-base.XXXXXX)\n" + " promote_output=$(mktemp /tmp/fruix-system-build-base-promote.XXXXXX)\n" + " cleanup_build_base() {\n" + " rm -f \"$build_output\" \"$promote_output\"\n" + " }\n" + " if [ -n \"$jobs\" ]; then\n" + " if ! env FRUIX_SELF_HOSTED_NATIVE_BUILD_JOBS=\"$jobs\" /usr/local/bin/fruix-self-hosted-native-build > \"$build_output\"; then\n" + " cat \"$build_output\" >&2 || true\n" + " cleanup_build_base\n" + " exit 1\n" + " fi\n" + " else\n" + " if ! /usr/local/bin/fruix-self-hosted-native-build > \"$build_output\"; then\n" + " cat \"$build_output\" >&2 || true\n" + " cleanup_build_base\n" + " exit 1\n" + " fi\n" + " fi\n" + " result_root=$(sed -n 's/^result_root=//p' \"$build_output\" | tail -n 1)\n" + " [ -n \"$result_root\" ] || { cleanup_build_base; die \"failed to recover result_root from build-base output\"; }\n" + " if ! run_node_cli native-build promote \"$result_root\" --store \"$store_dir\" > \"$promote_output\"; then\n" + " cat \"$build_output\" >&2 || true\n" + " cat \"$promote_output\" >&2 || true\n" + " cleanup_build_base\n" + " exit 1\n" + " fi\n" + " result_store=$(sed -n 's/^result_store=//p' \"$promote_output\" | tail -n 1)\n" + " [ -n \"$result_store\" ] || { cleanup_build_base; die \"failed to recover result_store from promotion output\"; }\n" + " cat \"$build_output\"\n" + " cat \"$promote_output\"\n" + " printf 'build_base_result_root=%s\\n' \"$result_root\"\n" + " printf 'build_base_result_store=%s\\n' \"$result_store\"\n" + " printf 'build_base_store_dir=%s\\n' \"$store_dir\"\n" + " printf 'build_base=ok\\n'\n" + " cleanup_build_base\n" + "}\n\n" "reconfigure_system()\n" "{\n" " build_output=$(mktemp /tmp/fruix-system-reconfigure.XXXXXX)\n" @@ -871,6 +945,10 @@ " shift 2\n" " system_build \"$@\"\n" " ;;\n" + " build-base)\n" + " shift 2\n" + " build_base_system \"$@\"\n" + " ;;\n" " reconfigure)\n" " shift 2\n" " reconfigure_system \"$@\"\n" diff --git a/tests/system/postphase20-installed-node-build-base-operating-system.scm.in b/tests/system/postphase20-installed-node-build-base-operating-system.scm.in new file mode 100644 index 0000000..78792b7 --- /dev/null +++ b/tests/system/postphase20-installed-node-build-base-operating-system.scm.in @@ -0,0 +1,77 @@ +(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-build-base-operating-system + (operating-system-from-promoted-native-build-result + postphase20-promoted-native-build-result + #:host-name "__HOST_NAME__" + #: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__"))) diff --git a/tests/system/run-postphase20-installed-node-build-base-xcpng.sh b/tests/system/run-postphase20-installed-node-build-base-xcpng.sh new file mode 100755 index 0000000..df28cad --- /dev/null +++ b/tests/system/run-postphase20-installed-node-build-base-xcpng.sh @@ -0,0 +1,338 @@ +#!/bin/sh +set -eu + +repo_root=${PROJECT_ROOT:-$(pwd)} +os_template=${OS_TEMPLATE:-$repo_root/tests/system/postphase20-installed-node-build-base-operating-system.scm.in} +system_name=${SYSTEM_NAME:-postphase20-installed-node-build-base-operating-system} +result_store=${RESULT_STORE:-/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest} +root_size=${ROOT_SIZE:-28g} +host_name=${HOST_NAME:-fruix-node-build-base} +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} +cleanup=0 + +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-postphase20-installed-node-build-base-xcpng.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +os_file=$workdir/operating-system.scm +inner_metadata=$workdir/postphase20-installed-node-build-base-inner-metadata.txt +build_base_out=$workdir/build-base.txt +metadata_file=$workdir/postphase20-installed-node-build-base-xcpng-metadata.txt + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +[ -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; } +[ -f "$root_ssh_private_key_file" ] || { echo "missing root SSH private key file: $root_ssh_private_key_file" >&2; exit 1; } +[ -d "$result_store" ] || { echo "promoted result store does not exist: $result_store" >&2; exit 1; } + +root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") +sed \ + -e "s|__PROMOTED_RESULT_STORE__|$result_store|g" \ + -e "s|__HOST_NAME__|$host_name|g" \ + -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ + "$os_template" > "$os_file" + +KEEP_WORKDIR=1 WORKDIR="$workdir/boot" METADATA_OUT="$inner_metadata" \ + ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \ + ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \ + OS_TEMPLATE="$os_file" 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") + +[ "$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 + +for path in \ + "$closure_path/metadata/system-declaration.scm" \ + "$closure_path/metadata/system-declaration-info.scm" \ + "$closure_path/metadata/system-declaration-system" \ + "$closure_path/share/fruix/node/scripts/fruix.scm" \ + "$closure_path/share/fruix/node/modules/fruix/system/freebsd/build.scm" \ + "$closure_path/share/fruix/node/modules/fruix/system/freebsd/render.scm" \ + "$closure_path/share/fruix/node/guix/guix/build/utils.scm" \ + "$closure_path/usr/local/bin/fruix-build-environment" \ + "$closure_path/usr/local/bin/fruix-self-hosted-native-build" +do + [ -f "$path" ] || { + echo "required installed-node build-base path missing: $path" >&2 + exit 1 + } +done + +grep -F "$host_name" "$closure_path/metadata/system-declaration.scm" >/dev/null || { + echo "embedded declaration does not mention host name" >&2 + exit 1 +} +grep -F "$system_name" "$closure_path/metadata/system-declaration-system" >/dev/null || { + echo "embedded declaration system name is missing" >&2 + exit 1 +} + +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_build_jobs=${GUEST_BUILD_JOBS:-$(ssh_guest 'sysctl -n hw.ncpu')} +case "$guest_build_jobs" in + ''|*[!0-9]*) + echo "invalid guest build job count: $guest_build_jobs" >&2 + exit 1 + ;; +esac + +ssh_guest 'sh -s' </dev/null +EOF + +ssh_guest "/usr/local/bin/fruix system build-base --jobs $guest_build_jobs" > "$build_base_out" + +run_id=$(sed -n 's/^run_id=//p' "$build_base_out" | tail -n 1) +helper_version=$(sed -n 's/^helper_version=//p' "$build_base_out" | tail -n 1) +executor_kind=$(sed -n 's/^executor_kind=//p' "$build_base_out" | tail -n 1) +executor_name=$(sed -n 's/^executor_name=//p' "$build_base_out" | tail -n 1) +executor_version=$(sed -n 's/^executor_version=//p' "$build_base_out" | tail -n 1) +build_profile=$(sed -n 's/^build_profile=//p' "$build_base_out" | tail -n 1) +source_store=$(sed -n 's/^source_store=//p' "$build_base_out" | tail -n 1) +source_root=$(sed -n 's/^source_root=//p' "$build_base_out" | tail -n 1) +build_jobs=$(sed -n 's/^build_jobs=//p' "$build_base_out" | tail -n 1) +build_root=$(sed -n 's/^build_root=//p' "$build_base_out" | tail -n 1) +result_root=$(sed -n 's/^result_root=//p' "$build_base_out" | tail -n 1) +status_file=$(sed -n 's/^status_file=//p' "$build_base_out" | tail -n 1) +promotion_file=$(sed -n 's/^promotion_file=//p' "$build_base_out" | tail -n 1) +latest_link=$(sed -n 's/^latest_link=//p' "$build_base_out" | tail -n 1) +world_artifact=$(sed -n 's/^world_artifact=//p' "$build_base_out" | tail -n 1) +kernel_artifact=$(sed -n 's/^kernel_artifact=//p' "$build_base_out" | tail -n 1) +headers_artifact=$(sed -n 's/^headers_artifact=//p' "$build_base_out" | tail -n 1) +bootloader_artifact=$(sed -n 's/^bootloader_artifact=//p' "$build_base_out" | tail -n 1) +world_artifact_size=$(sed -n 's/^world_artifact_size=//p' "$build_base_out" | tail -n 1) +kernel_artifact_size=$(sed -n 's/^kernel_artifact_size=//p' "$build_base_out" | tail -n 1) +headers_artifact_size=$(sed -n 's/^headers_artifact_size=//p' "$build_base_out" | tail -n 1) +bootloader_artifact_size=$(sed -n 's/^bootloader_artifact_size=//p' "$build_base_out" | tail -n 1) +sha_kernel=$(sed -n 's/^sha_kernel=//p' "$build_base_out" | tail -n 1) +sha_loader=$(sed -n 's/^sha_loader=//p' "$build_base_out" | tail -n 1) +sha_param=$(sed -n 's/^sha_param=//p' "$build_base_out" | tail -n 1) +result_metadata_file=$(sed -n 's/^result_metadata_file=//p' "$build_base_out" | tail -n 1) +artifact_store_count=$(sed -n 's/^artifact_store_count=//p' "$build_base_out" | tail -n 1) +result_store_guest=$(sed -n 's/^result_store=//p' "$build_base_out" | tail -n 1) +world_store=$(sed -n 's/^world_store=//p' "$build_base_out" | tail -n 1) +kernel_store=$(sed -n 's/^kernel_store=//p' "$build_base_out" | tail -n 1) +headers_store=$(sed -n 's/^headers_store=//p' "$build_base_out" | tail -n 1) +bootloader_store=$(sed -n 's/^bootloader_store=//p' "$build_base_out" | tail -n 1) +build_base_result_root=$(sed -n 's/^build_base_result_root=//p' "$build_base_out" | tail -n 1) +build_base_result_store=$(sed -n 's/^build_base_result_store=//p' "$build_base_out" | tail -n 1) +build_base_store_dir=$(sed -n 's/^build_base_store_dir=//p' "$build_base_out" | tail -n 1) +build_base_marker=$(sed -n 's/^build_base=//p' "$build_base_out" | tail -n 1) + +status_value=$(ssh_guest "cat '$status_file'") +latest_target=$(ssh_guest "readlink '$latest_link'") +guest_hostname=$(ssh_guest 'hostname') +guest_run_current=$(ssh_guest 'readlink /run/current-system') +root_df=$(ssh_guest 'df -h / | tail -n 1 | tr -s " " | tr "\t" " "') + +[ "$helper_version" = 5 ] || { echo "unexpected helper version: $helper_version" >&2; exit 1; } +[ "$executor_kind" = self-hosted ] || { echo "unexpected executor kind: $executor_kind" >&2; exit 1; } +[ "$executor_name" = guest-self-hosted ] || { echo "unexpected executor name: $executor_name" >&2; exit 1; } +[ "$executor_version" = 5 ] || { echo "unexpected executor version: $executor_version" >&2; exit 1; } +[ "$build_profile" = /run/current-system/build-profile ] || { echo "unexpected build profile: $build_profile" >&2; exit 1; } +[ "$build_jobs" = "$guest_build_jobs" ] || { echo "unexpected build jobs: $build_jobs" >&2; exit 1; } +[ "$status_value" = ok ] || { echo "build-base status is not ok: $status_value" >&2; exit 1; } +[ "$latest_target" = "$result_root" ] || { echo "latest link target mismatch: $latest_target" >&2; exit 1; } +[ "$artifact_store_count" = 4 ] || { echo "unexpected artifact store count: $artifact_store_count" >&2; exit 1; } +[ "$build_base_result_root" = "$result_root" ] || { echo "build-base result root mismatch" >&2; exit 1; } +[ "$build_base_result_store" = "$result_store_guest" ] || { echo "build-base result store mismatch" >&2; exit 1; } +[ "$build_base_store_dir" = /frx/store ] || { echo "unexpected build-base store dir: $build_base_store_dir" >&2; exit 1; } +[ "$build_base_marker" = ok ] || { echo "missing build-base success marker" >&2; exit 1; } +[ "$guest_hostname" = "$host_name" ] || { echo "unexpected guest host name: $guest_hostname" >&2; exit 1; } +[ "$guest_run_current" = "$closure_path" ] || { echo "unexpected current closure after build-base: $guest_run_current" >&2; exit 1; } + +case "$source_store" in + /frx/store/*-freebsd-source-*) : ;; + *) echo "unexpected source store path: $source_store" >&2; exit 1 ;; +esac +case "$source_root" in + /frx/store/*-freebsd-source-*/*) : ;; + *) echo "unexpected source root path: $source_root" >&2; exit 1 ;; +esac +case "$build_root" in + /var/tmp/fruix-self-hosted-native-builds/*) : ;; + *) echo "unexpected build root path: $build_root" >&2; exit 1 ;; +esac +case "$result_root" in + /var/lib/fruix/native-builds/*) : ;; + *) echo "unexpected result root path: $result_root" >&2; exit 1 ;; +esac +case "$result_store_guest" in + /frx/store/*-fruix-native-build-result-*) : ;; + *) echo "unexpected result store path: $result_store_guest" >&2; exit 1 ;; +esac +case "$world_store" in + /frx/store/*-fruix-native-world-*) : ;; + *) echo "unexpected world store path: $world_store" >&2; exit 1 ;; +esac +case "$kernel_store" in + /frx/store/*-fruix-native-kernel-*) : ;; + *) echo "unexpected kernel store path: $kernel_store" >&2; exit 1 ;; +esac +case "$headers_store" in + /frx/store/*-fruix-native-headers-*) : ;; + *) echo "unexpected headers store path: $headers_store" >&2; exit 1 ;; +esac +case "$bootloader_store" in + /frx/store/*-fruix-native-bootloader-*) : ;; + *) echo "unexpected bootloader store path: $bootloader_store" >&2; exit 1 ;; +esac +printf '%s\n' "$run_id" | grep -E '^[0-9]{8}T[0-9]{6}Z$' >/dev/null || { + echo "unexpected run id: $run_id" >&2 + exit 1 +} +printf '%s\n' "$sha_kernel" | grep -E '^[0-9a-f]{64}$' >/dev/null || { + echo "invalid kernel sha256: $sha_kernel" >&2 + exit 1 +} +printf '%s\n' "$sha_loader" | grep -E '^[0-9a-f]{64}$' >/dev/null || { + echo "invalid loader sha256: $sha_loader" >&2 + exit 1 +} +printf '%s\n' "$sha_param" | grep -E '^[0-9a-f]{64}$' >/dev/null || { + echo "invalid param.h sha256: $sha_param" >&2 + exit 1 +} + +ssh_guest "[ -f '$promotion_file' ]" +ssh_guest "[ -f '$result_metadata_file' ]" +ssh_guest "[ -f '$world_artifact/bin/sh' ]" +ssh_guest "[ -f '$kernel_artifact/boot/kernel/kernel' ]" +ssh_guest "[ -f '$headers_artifact/usr/include/sys/param.h' ]" +ssh_guest "[ -f '$bootloader_artifact/boot/loader.efi' ]" +ssh_guest "[ -d '$result_store_guest' ]" +ssh_guest "[ -L '$result_store_guest/artifacts/world' ]" +ssh_guest "[ -L '$result_store_guest/artifacts/kernel' ]" +ssh_guest "[ -L '$result_store_guest/artifacts/headers' ]" +ssh_guest "[ -L '$result_store_guest/artifacts/bootloader' ]" +ssh_guest "[ -d '$world_store' ]" +ssh_guest "[ -d '$kernel_store' ]" +ssh_guest "[ -d '$headers_store' ]" +ssh_guest "[ -d '$bootloader_store' ]" + +cat > "$metadata_file" <