6.2 KiB
Post-Phase-10: local Shepherd-as-PID-1 boot prototype on FreeBSD
Date: 2026-04-02
Goal
Begin the next post-Phase-10 runtime-integration pass by exploring two related directions:
- reduce reliance on the
freebsd-init + rc.d + shepherdbridge, and - compare Fruix's boot path with how Guix boots Shepherd as PID 1.
The concrete goal for this subphase was not yet a full real-VM migration of the main boot track, but a first validated local prototype where the generated FreeBSD image boots with Shepherd as PID 1 under QEMU/UEFI.
Comparison with Guix
Guix's system model treats Shepherd as a first-class root service.
In ~/repos/guix/gnu/services/shepherd.scm, the key pattern is:
shepherd-root-service-typeextends the boot service graphshepherd-boot-gexpultimately does anexeclof Shepherd as PID 1- higher-level system services extend that root Shepherd instance declaratively
In other words, Guix does not merely start Shepherd from a late init script; it composes the system boot graph around Shepherd directly.
Fruix is not at that level of native service composition yet, but this subphase adopts the same basic architectural direction:
- boot into a generated Shepherd config directly,
- let Shepherd own the service graph from PID 1,
- and keep the imperative compatibility/bootstrap logic as small as possible.
Implementation
1. Added an explicit init-mode to the FreeBSD operating-system model
modules/fruix/system/freebsd.scm now has an init-mode field on the declarative operating-system record.
Supported values are currently:
freebsd-init+rc.d-shepherdshepherd-pid1
The existing boot path remains the default.
2. Added a generated PID 1 launcher for the shepherd-pid1 mode
For shepherd-pid1, the generated system now contains:
boot/fruix-pid1
and the generated loader configuration adds:
init_exec="/run/current-system/boot/fruix-pid1"
That means FreeBSD init(8) directly execs the generated Fruix launcher as its very first action, replacing itself as PID 1.
The launcher currently performs the minimum bootstrap steps needed before turning control over to Shepherd:
- remount
/read-write on this very-early path - mount declared non-root filesystems such as:
devfson/devtmpfson/tmp
- set the hostname
- run
/run/current-system/activate - export the Guile/Shepherd runtime environment
execShepherd directly
Because Shepherd is a Guile script, the actual PID 1 process image is the Guile interpreter running Shepherd. The important validation point is that Shepherd's own pidfile records PID 1 and the service socket is owned by that process.
3. Generated a different Shepherd config for PID 1 mode
For shepherd-pid1, the generated shepherd/init.scm now includes the minimal helper procedures it needs inline, rather than importing the repo-side (fruix shepherd freebsd) module at runtime.
This avoids depending on checkout-only Scheme modules being present in the guest.
The PID 1 config currently starts a minimal service graph:
fruix-loggernetifthrough FreeBSDservice(8)sshdthrough FreeBSDservice(8)fruix-ready
So this prototype still uses some FreeBSD rc scripts as service implementations, but now under Shepherd control rather than under /etc/rc as the primary boot manager.
4. Made activation more store-friendly for this early-boot path
The generated activation script now treats:
cap_mkdb /etc/login.confpwd_mkdb -p /etc/master.passwd
as best-effort operations.
That matters because Fruix currently symlinks these files from the immutable system closure, and on the very early PID 1 path they should not be allowed to abort the whole boot.
Validation
New PID 1 template
Added:
tests/system/phase11-shepherd-pid1-operating-system.scm.in
This declares the same minimal FreeBSD Fruix guest shape as the current Phase 9 system, but with:
#:init-mode 'shepherd-pid1
New local QEMU validation harness
Added:
tests/system/run-phase11-shepherd-pid1-qemu.sh
This harness:
- builds the image through the canonical
fruix system imagepath - boots it locally with QEMU/TCG + UEFI
- injects the root SSH key
- waits for the ready marker over forwarded SSH
- verifies that Shepherd is running and that Shepherd's pidfile says PID 1
Successful run
Passing validation run:
PASS phase11-shepherd-pid1-qemu- workdir:
/tmp/pid1-qemu6-1775128407
Key validated results:
ready_marker=ready
run_current_system_target=/frx/store/8b44506c37da85cebf265c813ed3a9d2a42408b077ac85854e7d6209d2f910ec-fruix-system-fruix-freebsd
shepherd_pid=1
shepherd_socket=present
shepherd_status=running
sshd_status=running
pid1_command=[guile]
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
The important detail is:
shepherd_pid=1
which shows that the running Shepherd instance in the guest is the system's PID 1 process.
Assessment
This is a meaningful architectural step beyond the earlier rc.d bridge milestone.
Fruix now has a validated local boot path where:
- the generated image boots on FreeBSD,
- the generated launcher becomes PID 1 via
init_exec, - Shepherd itself owns PID 1,
- networking and SSH come up under Shepherd-managed service ordering,
- and the ready marker still appears.
Remaining limitations
This is still a prototype, not yet the replacement for the main boot path.
Notable current limitations:
- the PID 1 path still relies on a small generated shell launcher before entering Shepherd
- some early boot/runtime actions are still expressed imperatively there
- the Guile/Shepherd local-runtime compatibility-prefix shims are not eliminated yet; they remain part of activation for the currently locally built runtime artifacts
- this subphase validated the path locally under QEMU/TCG, not yet on the real XCP-ng VM
Recommended next step
Use this validated local PID 1 prototype as the base for the next subphase:
- try the
shepherd-pid1image on the real XCP-ng VM - if that succeeds, decide whether
shepherd-pid1should become a selectable supported boot mode rather than just a prototype - continue reducing the remaining compatibility-prefix shims by moving the Guile/Shepherd runtime artifacts toward a more native store-aware arrangement