system: add installed-node build-base

This commit is contained in:
2026-04-06 21:23:08 +02:00
parent 2a1a6c3b81
commit 0c1ee5ee8f
9 changed files with 778 additions and 10 deletions
+6 -1
View File
@@ -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.
+35 -2
View File
@@ -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?”
@@ -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/<run-id>`
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
+51 -6
View File
@@ -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/<run-id>`
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/<run-id>`
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
+3
View File
@@ -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)))
+1 -1
View File
@@ -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"))
+78
View File
@@ -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"
@@ -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__")))
@@ -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' <<EOF
set -eu
[ -x /usr/local/bin/fruix ]
[ -x /usr/local/bin/fruix-build-environment ]
[ -x /usr/local/bin/fruix-self-hosted-native-build ]
[ -L /run/current-build ]
[ -L /usr/include ]
[ -L /usr/share/mk ]
[ -f /run/current-system/metadata/system-declaration.scm ]
[ -f /run/current-system/metadata/system-declaration-info.scm ]
[ -f /run/current-system/metadata/system-declaration-system ]
[ -f /run/current-system/share/fruix/node/scripts/fruix.scm ]
[ -f /run/current-system/share/fruix/node/modules/fruix/system/freebsd/build.scm ]
[ -f /run/current-system/share/fruix/node/modules/fruix/system/freebsd/render.scm ]
[ -f /run/current-system/share/fruix/node/guix/guix/build/utils.scm ]
grep -F '$system_name' /run/current-system/metadata/system-declaration-system >/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" <<EOF
workdir=$workdir
inner_metadata=$inner_metadata
phase8_metadata=$phase8_metadata
closure_path=$closure_path
closure_base=$closure_base
vm_id=$vm_id
vdi_id=$vdi_id
guest_ip=$guest_ip
root_size=$root_size
host_name=$host_name
source_result_store=$result_store
build_base_out=$build_base_out
run_id=$run_id
helper_version=$helper_version
executor_kind=$executor_kind
executor_name=$executor_name
executor_version=$executor_version
build_jobs=$build_jobs
build_profile=$build_profile
source_store=$source_store
source_root=$source_root
build_root=$build_root
result_root=$result_root
status_file=$status_file
promotion_file=$promotion_file
latest_link=$latest_link
latest_target=$latest_target
status_value=$status_value
world_artifact=$world_artifact
kernel_artifact=$kernel_artifact
headers_artifact=$headers_artifact
bootloader_artifact=$bootloader_artifact
world_artifact_size=$world_artifact_size
kernel_artifact_size=$kernel_artifact_size
headers_artifact_size=$headers_artifact_size
bootloader_artifact_size=$bootloader_artifact_size
sha_kernel=$sha_kernel
sha_loader=$sha_loader
sha_param=$sha_param
result_metadata_file=$result_metadata_file
artifact_store_count=$artifact_store_count
result_store=$result_store_guest
world_store=$world_store
kernel_store=$kernel_store
headers_store=$headers_store
bootloader_store=$bootloader_store
build_base_result_root=$build_base_result_root
build_base_result_store=$build_base_result_store
build_base_store_dir=$build_base_store_dir
guest_hostname=$guest_hostname
guest_run_current=$guest_run_current
root_df=$root_df
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
installed_node_build_base=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS postphase20-installed-node-build-base-xcpng\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"