system: validate development environment overlay

This commit is contained in:
2026-04-05 11:56:06 +02:00
parent 4975084baa
commit 9e9a0b59fc
10 changed files with 612 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
# Fruix differences for Guix sysadmins # Fruix differences for Guix sysadmins
Date: 2026-04-04 Date: 2026-04-05
This document is aimed at operators who already know Guix well and want a quick map of where Fruix behaves similarly and where it intentionally differs. This document is aimed at operators who already know Guix well and want a quick map of where Fruix behaves similarly and where it intentionally differs.
@@ -242,6 +242,24 @@ For Guix-familiar operators, the practical takeaway is:
- do **not** assume Fruix store prefixes are byte-for-byte comparable to Guix/Nix ones - do **not** assume Fruix store prefixes are byte-for-byte comparable to Guix/Nix ones
- expect Fruix to prefer a simpler, centralized naming policy unless exact Guix/Nix behavior becomes necessary later - expect Fruix to prefer a simpler, centralized naming policy unless exact Guix/Nix behavior becomes necessary later
## 8. Fruix can expose a separate in-system development profile overlay
For the validated Phase 20.1 path, Fruix can now expose development tooling separately from the main runtime profile.
On those systems, Fruix exposes:
- `/run/current-system/development-profile`
- `/run/current-development`
- `/usr/local/bin/fruix-development-environment`
The intent is:
- keep the main runtime profile lean
- expose headers, `usr/share/mk`, and selected toolchain commands explicitly
- avoid treating a development-heavy system image as the default runtime shape
Compared with Guix, this is conceptually similar to keeping development-oriented state separate from the main runtime identity, but Fruix currently expresses it as a system-attached development overlay rather than through Guix's broader profile/tooling model.
## Where Fruix is intentionally trying to improve on Guix's representation ## 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. Fruix is not trying to improve on Guix's core semantics. Guix already got those right.

View File

@@ -34,6 +34,10 @@ Fruix currently has:
- `fruix system status` - `fruix system status`
- `fruix system switch` - `fruix system switch`
- `fruix system rollback` - `fruix system rollback`
- a validated separate in-system development environment overlay via:
- `/run/current-system/development-profile`
- `/run/current-development`
- `/usr/local/bin/fruix-development-environment`
Validated boot modes still are: Validated boot modes still are:
@@ -46,44 +50,37 @@ The validated Phase 18 installation work currently uses:
## Latest completed achievement ## Latest completed achievement
### 2026-04-04 — Phase 19.3 completed ### 2026-04-05 — Phase 20.1 completed
Fruix now has a validated installed-system operator workflow for switching to a staged candidate generation and rolling back to the recorded previous generation. Fruix now has a validated real-VM path where a booted Fruix-managed FreeBSD system exposes a separate development environment for native base work without collapsing the runtime/development split.
Highlights: Highlights:
- installed systems now ship an in-guest Fruix deployment helper at: - operating-system declarations now support:
- `/usr/local/bin/fruix` - `#:development-packages`
- validated in-guest command surface: - system closures can now carry a separate development profile at:
- `fruix system status` - `/run/current-system/development-profile`
- `fruix system switch /frx/store/...-fruix-system-...` - `/run/current-development`
- `fruix system rollback` - opt-in systems now ship an in-guest helper at:
- switching now records explicit rollback state under: - `/usr/local/bin/fruix-development-environment`
- `/var/lib/fruix/system/rollback` - the validated Phase 20.1 guest path exposes:
- `/var/lib/fruix/system/rollback-generation` - native headers
- switching now records explicit rollback GC roots under: - `usr/share/mk` for `bsd.*.mk`
- `/frx/var/fruix/gcroots/rollback-system` - Clang toolchain commands such as `cc`, `c++`, `ar`, `ranlib`, and `nm`
- the validated installed-system workflow now supports: - the validated guest workflow now supports:
- stage candidate closure in `/frx/store` - `eval "$(/usr/local/bin/fruix-development-environment)"`
- switch to generation 2 - direct compilation with the Fruix-provided toolchain
- reboot into the candidate - a simple `bsd.prog.mk` build on the running Fruix guest
- rollback to generation 1
- reboot into the restored current system
Validation: Validation:
- `PASS phase19-installed-system-rollback-qemu` - `PASS phase20-development-environment-xcpng`
- regression re-checks:
- `PASS phase19-generation-layout-qemu`
- `PASS phase18-installer-iso`
Reports: Reports:
- `docs/system-deployment-workflow.md` - `docs/system-deployment-workflow.md`
- `docs/GUIX_DIFFERENCES.md` - `docs/GUIX_DIFFERENCES.md`
- `docs/reports/phase19-deployment-workflow-freebsd.md` - `docs/reports/phase20-development-environment-freebsd.md`
- `docs/reports/phase19-generation-layout-freebsd.md`
- `docs/reports/phase19-installed-system-rollback-freebsd.md`
## Recent major milestones ## Recent major milestones
@@ -109,6 +106,6 @@ Reports:
Per `docs/PLAN_4.md`, the next planned step is: Per `docs/PLAN_4.md`, the next planned step is:
- **Phase 20.1** — validate a Fruix-managed development environment for native FreeBSD base work - **Phase 20.2** — run host-initiated native base builds inside a Fruix-managed environment
Phase 19.3 is now complete: Fruix validates installed-system generation switching and rollback through the intended operator-facing workflow. Phase 20.1 is now complete: Fruix validates a separate in-system development environment for native FreeBSD base work on the approved real XCP-ng path.

View File

@@ -0,0 +1,160 @@
# Phase 20.1: Fruix-managed development environment for native FreeBSD base work
Date: 2026-04-05
## Goal
Validate that a booted Fruix-managed FreeBSD system can expose a usable development environment for deeper native base work without collapsing the runtime/development boundary back into one broad profile.
This step explicitly builds on the Phase 14 split between:
- native runtime/boot artifacts for the running system
- separate development-facing artifacts such as headers and toolchain pieces
The goal is **not** full self-hosting yet.
It is to prove that a running Fruix system can expose the tools and paths needed for native FreeBSD build work in a controlled way.
## Implementation
### New operating-system field
`modules/fruix/system/freebsd/model.scm` now supports:
- `#:development-packages`
- `operating-system-development-packages`
The default remains empty, so existing systems do not change unless they opt in.
### Separate development profile inside the system closure
`modules/fruix/system/freebsd/media.scm` now materializes an additional profile tree when `development-packages` is non-empty:
- `/frx/store/...-fruix-system-.../development-profile`
The main runtime tree remains:
- `/frx/store/...-fruix-system-.../profile`
This preserves the runtime/development split:
- runtime stays under `profile`
- development tooling stays under `development-profile`
The development stores are also now part of the closure references and recorded in `metadata/store-layout.scm`.
### In-guest development environment helper
Opt-in systems with development packages now ship:
- `/usr/local/bin/fruix-development-environment`
and expose a stable runtime link:
- `/run/current-development -> /run/current-system/development-profile`
The helper emits shell exports for the active development profile, including at least:
- `FRUIX_DEVELOPMENT_PROFILE`
- `FRUIX_DEVELOPMENT_INCLUDE`
- `FRUIX_DEVELOPMENT_SHARE_MK`
- `FRUIX_CC`
- `FRUIX_CXX`
- `FRUIX_AR`
- `FRUIX_RANLIB`
- `FRUIX_NM`
- `FRUIX_BMAKE`
- `CPPFLAGS`
- `MAKEFLAGS`
- `PATH`
Intended use:
```sh
eval "$(/usr/local/bin/fruix-development-environment)"
```
### Chosen development overlay for this phase
For the validated Phase 20.1 guest path, the development overlay is intentionally narrow:
- `freebsd-native-headers`
- `freebsd-clang-toolchain`
This was chosen deliberately.
The earlier standalone Phase 14 development-profile package set was designed for broad profile composition, but for a running Fruix system it unnecessarily duplicated runtime pieces already present in the system profile.
For native base work, the important additions here are:
- `usr/include`
- `usr/share/mk`
- Clang/binutils-style frontend tools
while the running system already supplies:
- base runtime
- `/usr/bin/make`
- system libraries and boot/runtime state
That keeps the Phase 20.1 environment focused on native base development rather than reintroducing a broad mixed profile.
## New files
Added:
- `tests/system/phase20-development-operating-system.scm.in`
- `tests/system/run-phase20-development-environment-xcpng.sh`
## Validation
Passing run:
- `PASS phase20-development-environment-xcpng`
- workdir: `/tmp/fruix-phase20-development-xcpng`
Validated on the approved real XCP-ng path:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
Representative result:
```text
closure_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd
development_profile_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd/development-profile
development_profile_guest=/run/current-system/development-profile
cc_version=FreeBSD clang version 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd708029e0b2)
hello_direct=hello-from-direct-dev-env
hello_make=hello-from-make-dev-env
development_environment=ok
```
The harness verified all of the following on the real booted Fruix guest:
- runtime boot/regression still succeeds on XCP-ng
- the closure contains a separate `development-profile`
- runtime `profile` does **not** regain headers or `usr/share/mk`
- `/usr/local/bin/fruix-development-environment` exists and emits the expected exports
- `/run/current-development` points at `/run/current-system/development-profile`
- direct compilation works with the exported Clang toolchain and headers
- a simple native FreeBSD `make` build using `.include <bsd.prog.mk>` also succeeds
## Result
Phase 20.1 is complete.
Fruix now validates a real booted FreeBSD system path where:
- the running system remains lean and runtime-focused
- native development artifacts are exposed separately and explicitly
- the guest can compile code directly with the Fruix-provided toolchain
- the guest can also drive a simple `bsd.prog.mk` build using the exported development environment
This is enough to say that Fruix can host a controlled native FreeBSD base-development environment, without yet claiming full self-hosting.
## Next step
Per `docs/PLAN_4.md`, the next planned step is:
- **Phase 20.2** — run host-initiated native base builds inside a Fruix-managed environment

View File

@@ -1,6 +1,6 @@
# Fruix system deployment workflow # Fruix system deployment workflow
Date: 2026-04-04 Date: 2026-04-05
## Purpose ## Purpose
@@ -197,6 +197,32 @@ Important current limitation:
- `fruix system switch` does **not** yet fetch or copy the candidate closure onto the target for you - `fruix system switch` does **not** yet fetch or copy the candidate closure onto the target for you
- it assumes the selected closure is already present in the installed system's `/frx/store` - it assumes the selected closure is already present in the installed system's `/frx/store`
### In-guest development environment
Opt-in systems can also expose a separate development overlay under:
- `/run/current-system/development-profile`
- `/run/current-development`
Those systems now ship a helper at:
- `/usr/local/bin/fruix-development-environment`
Intended use:
```sh
eval "$(/usr/local/bin/fruix-development-environment)"
```
That helper exports a development-oriented environment while keeping the main runtime profile separate. The validated Phase 20.1 path currently uses this to expose at least:
- native headers under `usr/include`
- FreeBSD `share/mk` files for `bsd.*.mk`
- Clang toolchain commands such as `cc`, `c++`, `ar`, `ranlib`, and `nm`
- `MAKEFLAGS` pointing at the development profile's `usr/share/mk`
This is the current Fruix-native way to make a running system suitable for controlled native base-development work without merging development content back into the main runtime profile.
## Deployment patterns ## Deployment patterns
### 1. Build-first workflow ### 1. Build-first workflow

View File

@@ -31,6 +31,7 @@
operating-system-kernel operating-system-kernel
operating-system-bootloader operating-system-bootloader
operating-system-base-packages operating-system-base-packages
operating-system-development-packages
operating-system-users operating-system-users
operating-system-groups operating-system-groups
operating-system-file-systems operating-system-file-systems

View File

@@ -79,11 +79,16 @@
(kernel-package (operating-system-kernel os)) (kernel-package (operating-system-kernel os))
(bootloader-package (operating-system-bootloader os)) (bootloader-package (operating-system-bootloader os))
(base-packages (operating-system-base-packages os)) (base-packages (operating-system-base-packages os))
(development-packages (operating-system-development-packages os))
(kernel-store (materialize-freebsd-package kernel-package store-dir cache source-cache)) (kernel-store (materialize-freebsd-package kernel-package store-dir cache source-cache))
(bootloader-store (materialize-freebsd-package bootloader-package store-dir cache source-cache)) (bootloader-store (materialize-freebsd-package bootloader-package store-dir cache source-cache))
(base-package-stores (map (lambda (package) (base-package-stores (map (lambda (package)
(materialize-freebsd-package package store-dir cache source-cache)) (materialize-freebsd-package package store-dir cache source-cache))
base-packages)) base-packages))
(development-package-stores
(map (lambda (package)
(materialize-freebsd-package package store-dir cache source-cache))
development-packages))
(base-package-pairs (map cons base-packages base-package-stores)) (base-package-pairs (map cons base-packages base-package-stores))
(store-classification (store-classification
(append (list (cons kernel-package kernel-store) (append (list (cons kernel-package kernel-store)
@@ -148,6 +153,8 @@
(host-base-stores . ,host-base-stores) (host-base-stores . ,host-base-stores)
(native-base-store-count . ,(length native-base-stores)) (native-base-store-count . ,(length native-base-stores))
(native-base-stores . ,native-base-stores) (native-base-stores . ,native-base-stores)
(development-package-store-count . ,(length development-package-stores))
(development-package-stores . ,development-package-stores)
(fruix-runtime-store-count . ,(length fruix-runtime-stores)) (fruix-runtime-store-count . ,(length fruix-runtime-stores))
(fruix-runtime-stores . ,fruix-runtime-stores) (fruix-runtime-stores . ,fruix-runtime-stores)
(host-base-replacement-order . ,%freebsd-host-staged-replacement-order) (host-base-replacement-order . ,%freebsd-host-staged-replacement-order)
@@ -161,7 +168,12 @@
. ,(render-activation-rc-script)) . ,(render-activation-rc-script))
("usr/local/etc/rc.d/fruix-shepherd" ("usr/local/etc/rc.d/fruix-shepherd"
. ,(render-rc-script shepherd-store guile-store guile-extra-store))))) . ,(render-rc-script shepherd-store guile-store guile-extra-store)))))
(references (delete-duplicates (append materialized-source-stores host-base-stores native-base-stores fruix-runtime-stores))) (references (delete-duplicates
(append materialized-source-stores
host-base-stores
native-base-stores
development-package-stores
fruix-runtime-stores)))
(manifest (string-append (manifest (string-append
"closure-spec=\n" "closure-spec=\n"
(object->string (operating-system-closure-spec os)) (object->string (operating-system-closure-spec os))
@@ -175,7 +187,9 @@
(display-name (string-append "fruix-system-" (display-name (string-append "fruix-system-"
(operating-system-host-name os))) (operating-system-host-name os)))
(closure-path (make-store-path store-dir display-name manifest (closure-path (make-store-path store-dir display-name manifest
#:kind 'operating-system))) #:kind 'operating-system))
(development-profile-path (and (not (null? development-package-stores))
(string-append closure-path "/development-profile"))))
(unless (file-exists? closure-path) (unless (file-exists? closure-path)
(mkdir-p closure-path) (mkdir-p closure-path)
(mkdir-p (string-append closure-path "/boot/kernel")) (mkdir-p (string-append closure-path "/boot/kernel"))
@@ -193,6 +207,11 @@
(for-each (lambda (output) (for-each (lambda (output)
(merge-output-into-tree output (string-append closure-path "/profile"))) (merge-output-into-tree output (string-append closure-path "/profile")))
base-package-stores) base-package-stores)
(when development-profile-path
(mkdir-p development-profile-path)
(for-each (lambda (output)
(merge-output-into-tree output development-profile-path))
development-package-stores))
(for-each (for-each
(lambda (entry) (lambda (entry)
(write-file (string-append closure-path "/" (car entry)) (cdr entry))) (write-file (string-append closure-path "/" (car entry)) (cdr entry)))
@@ -204,6 +223,8 @@
(chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd") #o555) (chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd") #o555)
(when (file-exists? (string-append closure-path "/usr/local/bin/fruix")) (when (file-exists? (string-append closure-path "/usr/local/bin/fruix"))
(chmod (string-append closure-path "/usr/local/bin/fruix") #o555)) (chmod (string-append closure-path "/usr/local/bin/fruix") #o555))
(when (file-exists? (string-append closure-path "/usr/local/bin/fruix-development-environment"))
(chmod (string-append closure-path "/usr/local/bin/fruix-development-environment") #o555))
(when (file-exists? (string-append closure-path "/boot/fruix-pid1")) (when (file-exists? (string-append closure-path "/boot/fruix-pid1"))
(chmod (string-append closure-path "/boot/fruix-pid1") #o555)) (chmod (string-append closure-path "/boot/fruix-pid1") #o555))
(write-file (string-append closure-path "/parameters.scm") (write-file (string-append closure-path "/parameters.scm")
@@ -218,6 +239,8 @@
(guile-extra-store . ,guile-extra-store) (guile-extra-store . ,guile-extra-store)
(shepherd-store . ,shepherd-store) (shepherd-store . ,shepherd-store)
(base-package-stores . ,base-package-stores) (base-package-stores . ,base-package-stores)
(development-package-stores . ,development-package-stores)
(development-profile-path . ,development-profile-path)
(host-base-stores . ,host-base-stores) (host-base-stores . ,host-base-stores)
(native-base-stores . ,native-base-stores) (native-base-stores . ,native-base-stores)
(fruix-runtime-stores . ,fruix-runtime-stores) (fruix-runtime-stores . ,fruix-runtime-stores)
@@ -351,6 +374,12 @@
'("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf")) '("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf"))
(symlink-force "/run/current-system/usr/local/bin/fruix" (symlink-force "/run/current-system/usr/local/bin/fruix"
(string-append rootfs "/usr/local/bin/fruix")) (string-append rootfs "/usr/local/bin/fruix"))
(when (file-exists? (string-append closure-path "/development-profile"))
(symlink-force "/run/current-system/development-profile"
(string-append rootfs "/run/current-development")))
(when (file-exists? (string-append closure-path "/usr/local/bin/fruix-development-environment"))
(symlink-force "/run/current-system/usr/local/bin/fruix-development-environment"
(string-append rootfs "/usr/local/bin/fruix-development-environment")))
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate" (symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate"
(string-append rootfs "/usr/local/etc/rc.d/fruix-activate")) (string-append rootfs "/usr/local/etc/rc.d/fruix-activate"))
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-shepherd" (symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-shepherd"

View File

@@ -33,6 +33,7 @@
operating-system-kernel operating-system-kernel
operating-system-bootloader operating-system-bootloader
operating-system-base-packages operating-system-base-packages
operating-system-development-packages
operating-system-users operating-system-users
operating-system-groups operating-system-groups
operating-system-file-systems operating-system-file-systems
@@ -95,8 +96,8 @@
(make-file-system device mount-point type options needed-for-boot?)) (make-file-system device mount-point type options needed-for-boot?))
(define-record-type <operating-system> (define-record-type <operating-system>
(make-operating-system host-name freebsd-base kernel bootloader base-packages users groups (make-operating-system host-name freebsd-base kernel bootloader base-packages development-packages
file-systems services loader-entries rc-conf-entries users groups file-systems services loader-entries rc-conf-entries
init-mode ready-marker root-authorized-keys) init-mode ready-marker root-authorized-keys)
operating-system? operating-system?
(host-name operating-system-host-name) (host-name operating-system-host-name)
@@ -104,6 +105,7 @@
(kernel operating-system-kernel) (kernel operating-system-kernel)
(bootloader operating-system-bootloader) (bootloader operating-system-bootloader)
(base-packages operating-system-base-packages) (base-packages operating-system-base-packages)
(development-packages operating-system-development-packages)
(users operating-system-users) (users operating-system-users)
(groups operating-system-groups) (groups operating-system-groups)
(file-systems operating-system-file-systems) (file-systems operating-system-file-systems)
@@ -120,6 +122,7 @@
(kernel freebsd-kernel) (kernel freebsd-kernel)
(bootloader freebsd-bootloader) (bootloader freebsd-bootloader)
(base-packages %freebsd-system-packages) (base-packages %freebsd-system-packages)
(development-packages '())
(users (list (user-account #:name "root" (users (list (user-account #:name "root"
#:uid 0 #:uid 0
#:group "wheel" #:group "wheel"
@@ -161,8 +164,8 @@
(init-mode 'freebsd-init+rc.d-shepherd) (init-mode 'freebsd-init+rc.d-shepherd)
(ready-marker "/var/lib/fruix/ready") (ready-marker "/var/lib/fruix/ready")
(root-authorized-keys '())) (root-authorized-keys '()))
(make-operating-system host-name freebsd-base kernel bootloader base-packages users groups (make-operating-system host-name freebsd-base kernel bootloader base-packages development-packages
file-systems services loader-entries rc-conf-entries users groups file-systems services loader-entries rc-conf-entries
init-mode ready-marker root-authorized-keys)) init-mode ready-marker root-authorized-keys))
(define default-minimal-operating-system (operating-system)) (define default-minimal-operating-system (operating-system))
@@ -231,6 +234,8 @@
(define (validate-operating-system os) (define (validate-operating-system os)
(let* ((host-name (operating-system-host-name os)) (let* ((host-name (operating-system-host-name os))
(base (operating-system-freebsd-base os)) (base (operating-system-freebsd-base os))
(base-packages (operating-system-base-packages os))
(development-packages (operating-system-development-packages os))
(users (operating-system-users os)) (users (operating-system-users os))
(groups (operating-system-groups os)) (groups (operating-system-groups os))
(file-systems (operating-system-file-systems os)) (file-systems (operating-system-file-systems os))
@@ -242,6 +247,10 @@
(error "operating-system host-name must not be empty")) (error "operating-system host-name must not be empty"))
(unless (freebsd-base? base) (unless (freebsd-base? base)
(error "operating-system freebsd-base must be a <freebsd-base> record")) (error "operating-system freebsd-base must be a <freebsd-base> record"))
(unless (every freebsd-package? base-packages)
(error "operating-system base-packages must be a list of <freebsd-package> records"))
(unless (every freebsd-package? development-packages)
(error "operating-system development-packages must be a list of <freebsd-package> records"))
(validate-freebsd-source (freebsd-base-source base)) (validate-freebsd-source (freebsd-base-source base))
(let ((dups (duplicate-elements user-names))) (let ((dups (duplicate-elements user-names)))
(unless (null? dups) (unless (null? dups)
@@ -297,6 +306,9 @@
"activate" "activate"
"shepherd/init.scm" "shepherd/init.scm"
"usr/local/bin/fruix") "usr/local/bin/fruix")
(if (null? (operating-system-development-packages os))
'()
'("usr/local/bin/fruix-development-environment"))
(if (pid1-init-mode? os) (if (pid1-init-mode? os)
'("boot/fruix-pid1") '("boot/fruix-pid1")
'()) '())
@@ -316,6 +328,8 @@
(bootloader-package . ,(freebsd-package-name (operating-system-bootloader os))) (bootloader-package . ,(freebsd-package-name (operating-system-bootloader os)))
(base-package-count . ,(length (operating-system-base-packages os))) (base-package-count . ,(length (operating-system-base-packages os)))
(base-packages . ,(package-names (operating-system-base-packages os))) (base-packages . ,(package-names (operating-system-base-packages os)))
(development-package-count . ,(length (operating-system-development-packages os)))
(development-packages . ,(package-names (operating-system-development-packages os)))
(user-count . ,(length (operating-system-users os))) (user-count . ,(length (operating-system-users os)))
(users . ,(map user-account-name (operating-system-users os))) (users . ,(map user-account-name (operating-system-users os)))
(group-count . ,(length (operating-system-groups os))) (group-count . ,(length (operating-system-groups os)))

View File

@@ -751,6 +751,40 @@
" exit 1\n" " exit 1\n"
" ;;\n" " ;;\n"
"esac\n")) "esac\n"))
(define (render-development-environment-script os)
(string-append
"#!/bin/sh\n"
"set -eu\n"
"profile=/run/current-system/development-profile\n"
"[ -d \"$profile\" ] || {\n"
" echo \"fruix-development-environment: development profile is not available\" >&2\n"
" exit 1\n"
"}\n"
"cat <<EOF\n"
"export FRUIX_DEVELOPMENT_PROFILE=\"$profile\"\n"
"export FRUIX_DEVELOPMENT_INCLUDE=\"$profile/usr/include\"\n"
"export FRUIX_DEVELOPMENT_LIB=\"$profile/lib\"\n"
"export FRUIX_DEVELOPMENT_SHARE_MK=\"$profile/usr/share/mk\"\n"
"export FRUIX_DEVELOPMENT_BIN=\"$profile/bin\"\n"
"export FRUIX_DEVELOPMENT_USR_BIN=\"$profile/usr/bin\"\n"
"export FRUIX_CC=\"$profile/bin/cc\"\n"
"export FRUIX_CXX=\"$profile/bin/c++\"\n"
"export FRUIX_AR=\"$profile/bin/ar\"\n"
"export FRUIX_RANLIB=\"$profile/bin/ranlib\"\n"
"export FRUIX_NM=\"$profile/bin/nm\"\n"
"export FRUIX_BMAKE=\"/usr/bin/make\"\n"
"export CC=\"$profile/bin/cc\"\n"
"export CXX=\"$profile/bin/c++\"\n"
"export AR=\"$profile/bin/ar\"\n"
"export RANLIB=\"$profile/bin/ranlib\"\n"
"export NM=\"$profile/bin/nm\"\n"
"export CPPFLAGS=\"-I$profile/usr/include\"\n"
"export CFLAGS=\"-I$profile/usr/include\"\n"
"export CXXFLAGS=\"-I$profile/usr/include\"\n"
"export LDFLAGS=\"-L$profile/lib\"\n"
"export MAKEFLAGS=\"-m $profile/usr/share/mk\"\n"
"export PATH=\"/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$profile/bin:$profile/sbin:$profile/usr/bin:$profile/usr/sbin\"\n"
"EOF\n"))
(define* (operating-system-generated-files os #:key guile-store guile-extra-store shepherd-store) (define* (operating-system-generated-files os #:key guile-store guile-extra-store shepherd-store)
@@ -772,6 +806,10 @@
#:shepherd-store shepherd-store)) #:shepherd-store shepherd-store))
("shepherd/init.scm" . ,(render-shepherd-config os)) ("shepherd/init.scm" . ,(render-shepherd-config os))
("usr/local/bin/fruix" . ,(render-installed-system-fruix os))) ("usr/local/bin/fruix" . ,(render-installed-system-fruix os)))
(if (null? (operating-system-development-packages os))
'()
`(("usr/local/bin/fruix-development-environment"
. ,(render-development-environment-script os))))
(if (pid1-init-mode? os) (if (pid1-init-mode? os)
`(("boot/fruix-pid1" . ,(render-pid1-script os shepherd-store guile-store guile-extra-store))) `(("boot/fruix-pid1" . ,(render-pid1-script os shepherd-store guile-store guile-extra-store)))
'()) '())

View File

@@ -0,0 +1,73 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase20-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-native-kernel
#:bootloader freebsd-native-bootloader
#:base-packages %freebsd-native-system-packages
#:development-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__")))

View File

@@ -0,0 +1,220 @@
#!/bin/sh
set -eu
repo_root=${PROJECT_ROOT:-$(pwd)}
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase20-operating-system}
root_size=${ROOT_SIZE:-8g}
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-phase20-development-xcpng.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
inner_metadata=$workdir/phase20-development-inner-metadata.txt
metadata_file=$workdir/phase20-development-environment-xcpng-metadata.txt
action_cleanup() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir"
fi
}
trap action_cleanup EXIT INT TERM
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" 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_template" 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")
development_profile_path=$closure_path/development-profile
runtime_profile_path=$closure_path/profile
development_env_script=$closure_path/usr/local/bin/fruix-development-environment
[ "$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 \
"$development_profile_path/bin/cc" \
"$development_profile_path/bin/c++" \
"$development_profile_path/bin/ar" \
"$development_profile_path/usr/include/sys/param.h" \
"$development_profile_path/usr/share/mk/bsd.prog.mk" \
"$development_env_script"
do
[ -e "$path" ] || {
echo "required development environment path missing: $path" >&2
exit 1
}
done
[ ! -e "$runtime_profile_path/include" ] || { echo "runtime profile unexpectedly contains headers" >&2; exit 1; }
[ ! -e "$runtime_profile_path/usr/share/mk" ] || { echo "runtime profile unexpectedly contains /usr/share/mk" >&2; exit 1; }
[ ! -e "$runtime_profile_path/bin/cc" ] || { echo "runtime profile unexpectedly contains cc" >&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_dev_metadata=$(ssh -i "$root_ssh_private_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
root@"$guest_ip" 'sh -s' <<'EOF'
set -eu
[ -x /usr/local/bin/fruix-development-environment ]
[ -L /run/current-development ]
[ "$(readlink /run/current-development)" = "/run/current-system/development-profile" ]
exports=$(/usr/local/bin/fruix-development-environment)
printf '%s\n' "$exports" | grep '^export FRUIX_DEVELOPMENT_PROFILE="/run/current-system/development-profile"$' >/dev/null
printf '%s\n' "$exports" | grep '^export MAKEFLAGS="-m /run/current-system/development-profile/usr/share/mk"$' >/dev/null
eval "$exports"
[ -d "$FRUIX_DEVELOPMENT_PROFILE" ]
[ -x "$FRUIX_CC" ]
[ -x "$FRUIX_CXX" ]
[ -x "$FRUIX_AR" ]
[ -f "$FRUIX_DEVELOPMENT_INCLUDE/sys/param.h" ]
[ -f "$FRUIX_DEVELOPMENT_SHARE_MK/bsd.prog.mk" ]
cc_version=$($FRUIX_CC --version | awk 'NR==1 { print; exit }')
tmp=/tmp/fruix-phase20-development-env
rm -rf "$tmp"
mkdir -p "$tmp/direct" "$tmp/mk"
cat > "$tmp/direct/hello.c" <<'EOS'
#include <stdio.h>
int main(void){puts("hello-from-direct-dev-env");return 0;}
EOS
$FRUIX_CC $CPPFLAGS $LDFLAGS "$tmp/direct/hello.c" -o "$tmp/direct/hello"
hello_direct=$($tmp/direct/hello)
cat > "$tmp/mk/hello.c" <<'EOS'
#include <stdio.h>
int main(void){puts("hello-from-make-dev-env");return 0;}
EOS
cat > "$tmp/mk/Makefile" <<'EOS'
PROG=hello
SRCS=hello.c
NO_MAN=yes
.include <bsd.prog.mk>
EOS
cat > "$tmp/mk/hello.1" <<'EOS'
.Dd April 5, 2026
.Dt HELLO 1
.Sh NAME
.Nm hello
.Nd phase20 development environment smoke binary
EOS
cd "$tmp/mk"
make clean >/dev/null 2>&1 || true
make > "$tmp/mk/make.log" 2>&1
hello_make=$(./hello)
make_log_tail=$(tail -n 20 "$tmp/mk/make.log" | tr '\n' ' ')
exports_flat=$(printf '%s' "$exports" | tr '\n' ' ')
printf 'development_profile=%s\n' "$FRUIX_DEVELOPMENT_PROFILE"
printf 'cc_version=%s\n' "$cc_version"
printf 'hello_direct=%s\n' "$hello_direct"
printf 'hello_make=%s\n' "$hello_make"
printf 'exports=%s\n' "$exports_flat"
printf 'make_log_tail=%s\n' "$make_log_tail"
EOF
)
development_profile=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^development_profile=//p')
cc_version=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^cc_version=//p')
hello_direct=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^hello_direct=//p')
hello_make=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^hello_make=//p')
development_exports=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^exports=//p')
make_log_tail=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^make_log_tail=//p')
[ "$development_profile" = "/run/current-system/development-profile" ] || {
echo "unexpected guest development profile path: $development_profile" >&2
exit 1
}
case "$cc_version" in
*"FreeBSD clang version"*) : ;;
*) echo "unexpected cc version output: $cc_version" >&2; exit 1 ;;
esac
[ "$hello_direct" = hello-from-direct-dev-env ] || {
echo "unexpected direct compile output: $hello_direct" >&2
exit 1
}
[ "$hello_make" = hello-from-make-dev-env ] || {
echo "unexpected make-based compile output: $hello_make" >&2
exit 1
}
case "$development_exports" in
*'export FRUIX_CC="/run/current-system/development-profile/bin/cc"'*) : ;;
*) echo "development environment exports do not include FRUIX_CC" >&2; exit 1 ;;
esac
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
development_profile_path=$development_profile_path
development_env_script=$development_env_script
shepherd_pid=$shepherd_pid
sshd_status=$sshd_status
compat_prefix_shims=$compat_prefix_shims
guile_module_smoke=$guile_module_smoke
development_profile_guest=$development_profile
cc_version=$cc_version
hello_direct=$hello_direct
hello_make=$hello_make
development_exports=$development_exports
make_log_tail=$make_log_tail
boot_backend=xcp-ng-xo-cli
init_mode=shepherd-pid1
development_environment=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase20-development-environment-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"