Plan to integrate the freebsd-launchd work (livecd architecture + launchd PID 1 + kmodloader + configd stub) into the gershwin-on-freebsd project. Sequenced: livecd rework first, then launchd, then the Apple-shaped helper daemons. Holds the line on packages — gershwin-on-freebsd's existing resources/packages/ lists are the source of truth and we add nothing new in either phase.
feat/libs-corebase branch). One new dep:
freebsd-launchd cloned into /Developer/Library/Sources/freebsd-launchd/ in Phase 2.pkgdemon/gershwin-on-freebsd:main:
1d2150b — pin gershwin-developer to feat/libs-corebase branch (one-line build.sh change).7caefbf — split CI workflow into build → boot-test → release jobs; add tests/boot-test.sh (qemu+OVMF+expect). Release only publishes when boot-test passes.3b97e40 — architectural rework: replace per-subdir nullfs + cp -R model with single-root unionfs + init_chroot kenv pivot, mirroring freebsd-livecd-unionfs. /init.sh at cdroot top, mdconfig the uzip, mount UFS at /sysroot, tmpfs at /upper, unionfs combining them, devfs at /sysroot/dev, then kenv init_chroot=/sysroot + exit. /sbin/init stays PID 1 and chroots into the unionfs before multi-user. RAM saving at idle: ~15-70 MB (no physical /etc + /var tmpfs copies). Mount points: 6 instead of 19.a7f01f4 — mkdir ${CD_ROOT}/etc for mkisoimages.sh's transient fstab (it writes /etc/fstab at line 69 and removes it at line 71; the dir needs to exist).02e4e9a — boot diagnostic settings: boot_verbose="YES", console="comconsole vidconsole", comment out boot_mute in loader.mute.d/loader.conf, tighten boot-test markers (drop "Welcome to" — the loader's beastie banner was matching it falsely on the prior "green" CI runs and producing a 1m5s false positive). Reverted at cleanup phase.206f8a7, e342e57 — successive attempts to make init.sh's output visible: explicit exec >/dev/console 2>&1, then progress markers and fail-fast guards on each mount.04c7bc5 — root cause for the silent boot, identified by reading init.c + diff'ing against freebsd-livecd-unionfs: (a) init mounts devfs after running init_script (init.c:343-389 is post-326-336), so init's open_console() falls back to /dev/null for the child's stdio. Fix: init.sh mounts devfs at /dev first, then re-execs stdio. (b) boot_serial="YES" was missing — userspace stdio needs it to route to comconsole; without it everything goes to vidconsole (invisible under QEMU -display none).6ff5abe — final boot blockers: root_rw_mount="NO" in /sysroot/etc/rc.conf (otherwise /etc/rc.d/root tries mount -uw / which dispatches to mount_cd9660 and aborts the entire boot). Plus kld_list reassignment instead of += (FreeBSD sh doesn't parse var+= in sourced rc.conf). Plus /rescue-compat for the SMBIOS hostname / EFI MonkeyPatch / clear calls (xargs, grep, clear missing from /rescue).pkgdemon/gershwin-on-freebsd:main:
fc6cbce — cutover commit. New build_launchd stage (host-clone freebsd-launchd, run make-launchd.sh in chroot, install bedrock plists + getty wrapper, copy gershwin overlays). 3 gershwin plists authored (org.gnustep.gdomap, org.gershwin.dshelper, org.gershwin.loginwindow). init.sh tail swapped to exec chroot /sysroot /sbin/launchd. loader.conf flipped to Option D (init_path="/init.sh"). Packages: added dhcpcd + wpa_supplicant; removed FreeBSD-dhclient + FreeBSD-wpa. configure_system: stripped the two replaced _enable=YES lines.8da5b4d — round 1 fixes from first CI. Dropped vidconsole from console= (dual-console claimed ttyu0 as kernel console-session, blocking getty). Added /System/Library/Libraries to ldconfig -m so dshelper finds libdispatch.so. Added -f to gdomap's ProgramArguments to stop it from forking-and-detaching.ebe7320 — round 2 fixes. Dropped getty plists (console + vty0) entirely — gershwin uses LoginWindow as the login UI; getty was redundant and looping on login_tty: Operation not permitted. Updated boot-test markers to LoginWindow- and dhcpcd-derived signals (LoginWindow[, Successfully registered, em0: leased). The "no getty" decision matches macOS shape (loginwindow.app on macOS isn't paired with text getty either).dshelper: first instance runs fine; subsequent KeepAlive respawns log "Already running, PID 103" from dshelper's own pidfile guard. Cosmetic noise. Fix: change dshelper's daemonization to be launchd-friendly (don't fork-and-detach; let launchd track the foreground PID).gdomap: "I can't find the loopback interface". lo0 isn't auto-up. Need a one-shot plist or init.sh step to ifconfig lo0 up before gdomap probes. Not blocking the boot today.boot_mute, drop the verbose / dual-console diagnostics) and retire the CI boot-test gate (collapse the workflow back to a single build → publish job, delete tests/boot-test.sh). See §Cleanup.resources/packages/{base,vital-base,gershwin,drivers,vital-gershwin} are the source of truth, and the existing entries are not re-audited. We don't re-derive what gershwin's pkg-base list should contain — that work is done. No new kernel modules / GPU drivers (no nvidia-drm-kmod, etc.). Phase 2 swaps the DHCP/WPA stacks: dhcpcd and wpa_supplicant from ports get added to resources/packages/gershwin; FreeBSD-dhclient and FreeBSD-wpa get removed from resources/packages/base (and vital-base) in the same commit. The ISO ships exactly one DHCP client and one supplicant.base.txz+tar -xJf approach is dropped on the floor when the work moves over.loginwindow.app. Phase 2 ships a launchd plist that starts gershwin's existing greeter binary (or wraps the existing loginwindow_enable=YES rc.d service). Real Cocoa-style LoginWindow is a Phase 4+ research item gated on a per-session launchd domain model and a keychain substitute that don't exist yet.Take what we've learned building freebsd-launchd — the livecd boot pattern (cd9660 → mkuzip → tmpfs → unionfs cascade), the /boot/firmware symlink workaround for kernel-namei firmware loading, the in-chroot GNUstep build, the launchd-as-PID-1 Option D pivot, the kmodloader hardware-bind daemon, the netconfigd stub — and land it in the gershwin-on-freebsd project as a sequenced, reversible series of changes.
Gershwin-on-freebsd already does most of the work. It already uses pkg-base. It already builds a hybrid EFI/BIOS ISO via mkisoimages.sh. It already does the cd9660+uzip+tmpfs+unionfs cascade. It already builds the GNUstep stack into /System/Library/. The integration is therefore not a rewrite — it's a cleanup pass on the livecd (Phase 1) followed by an additive launchd port (Phase 2).
gershwin-on-freebsd/resources/packages/ is the canonical package set, and we do not re-audit it. Don't redo the work of figuring out what pkg-base entries gershwin needs — that's been settled. No new kernel modules / GPU drivers (no nvidia-drm-kmod; existing drm-kmod in drivers stays as-is). The only package edits across this whole plan are four lines, all in Phase 2:
dhcpcd to resources/packages/gershwinwpa_supplicant to resources/packages/gershwinFreeBSD-dhclient from resources/packages/{base,vital-base}FreeBSD-wpa from resources/packages/{base,vital-base}feat/libs-corebase branch is the canonical installer for the GNUstep system domain plus the desktop. build.sh already invokes it; the only change is the branch flag (git clone --branch feat/libs-corebase --depth 1 …). We do not vendor or fork the GNUstep build steps into gershwin-on-freebsd.
init_script → /init.sh move and the architectural unionfs rework (this is what was originally underscoped as "cosmetic"). No launchd plists land until Phase 2. Phase 2 only starts after the Phase 1 continuous-release ISO boots through the existing rc.d flow on real hardware.
/Developer/Library/Sources/freebsd-launchd/ via chroot git clone from inside build.sh, matching gershwin's existing pattern (build.sh:281 already clones gershwin-developer in-chroot). The standalone freebsd-launchd repo's "chroot stays git-free + rsync from host" rule is project-local — when integrating into gershwin we follow gershwin's pattern.
| Repo | Owns | Branch |
|---|---|---|
| gershwin-on-freebsd | Package lists (base, vital-base, gershwin, drivers, vital-gershwin); livecd build script (build.sh); init_script / /init.sh; loader config; ISO mastering; CI; LaunchDaemon plists in Phase 2. |
main |
| gershwin-developer | GNUstep system-domain build (libdispatch → tools-make → libobjc2 → libs-base → libs-corebase → libs-gui → libs-back); desktop apps build; clones into /Developer/Library/Sources/; Install-System-Domain.sh drives make install. |
feat/libs-corebase |
| freebsd-launchd | launchd PID 1 binary + LaunchDaemon plists; kmodloader; netconfigd stub. Cloned by gershwin-on-freebsd's build.sh in Phase 2 into /Developer/Library/Sources/freebsd-launchd/. |
main |
Three repos, one direction of dependency: gershwin-on-freebsd consumes both gershwin-developer and freebsd-launchd; the other two don't know about each other.
Phase 1 (landed) keeps stock /sbin/init+rc.d but introduces the unionfs+chroot architecture. Phase 2 changes the last line of init.sh and the loader knobs to swap stock init for launchd; the rest of the cascade body is reused unchanged. Phase 3 fills in the Apple-shaped helpers.
Three commits on pkgdemon/gershwin-on-freebsd:main. Init stays stock; rc.d stays the service manager — but the boot architecture now mirrors freebsd-livecd-unionfs (single uzip rootfs + tmpfs upper + unionfs + init_chroot kenv pivot), making Phase 2's launchd swap a single-line change.
feat/libs-corebase done — 1d2150bOne-line change at build.sh:281:
git clone --branch feat/libs-corebase --depth 1 \
https://github.com/gershwin-desktop/gershwin-developer "${RELEASE_DIR}/Developer"
Diff between main and feat/libs-corebase in gershwin-developer: two commits (~10 lines) that add libs-corebase to the build orchestrator. There is no patched/forked libs-corebase — it's plain upstream gnustep/libs-corebase HEAD with a vanilla configure invocation. Once feat/libs-corebase merges to main upstream, drop the --branch flag — but until then, the user's open upstream PR (gershwin-developer#32) is the source of truth and we leave it alone.
This enables CoreFoundation-shaped APIs (libgnustep-corebase.so) for everything downstream — including freebsd-launchd in Phase 2, which links against it for plist parsing.
/boot/firmware symlink — not needed/sysroot, but does so via /sbin/init's init_chroot kenv — the kernel's view of /sysroot is a real mount stack (uzip + tmpfs upper + unionfs), and /sysroot/boot/firmware resolves through that stack to the actual firmware files in the uzip. The "kernel can't see what userspace can see" bug doesn't apply because the kernel's namei is operating against the same mount tree. If post-rework hardware testing surfaces firmware-loading failures we revisit; until then no symlink.
init_chroot pivot done — 3b97e40This was originally underscoped as "tighten init_script (cosmetic)." That was wrong — gershwin's livecd had to actually move to the freebsd-livecd-unionfs runtime model before Phase 2 could land cleanly. What changed:
Before: resources/overlays/boot/init_script ran a 14-mount cascade (per-subdir nullfs of /Developer, /System, /Local, /bin, /lib, /libexec, /sbin, /usr, /boot, /root, plus tmpfs at /nvidia, /compat, /tmp, /media) plus 5 unionfs mounts on top, plus cp -R /media/.uzip/var → /var (~10–50 MB physical copy) and cp -R /media/.uzip/etc → /tmp; nullfs /tmp/etc /etc (~5–20 MB physical copy). No chroot — kernel root stayed cd9660. Mount points to track: 19.
After: resources/overlays/init.sh at cdroot top-level. Cascade:
mdconfig -a -t vnode -o readonly -f /rootfs.uzip -u 0
mount -t ufs -o ro /dev/md0.uzip /sysroot # lower
mount -t tmpfs tmpfs /upper # writable upper
mount -t unionfs /upper /sysroot # combined
mount -t devfs devfs /sysroot/dev
# … gershwin live-mode tweaks against /sysroot/… …
kenv init_chroot=/sysroot
exit 0
/sbin/init (still PID 1, real FreeBSD binary from /rescue/init since /sbin/init isn't on the cd9660) reads init_chroot kenv at init.c:333 and chroots into /sysroot before continuing multi-user. Kernel root stays cd9660; userland sees the unionfs as /. Mount points: 6.
Loader.conf:
# Phase 1 additions:
unionfs_load="YES"
init_shell="/rescue/sh"
init_script="/init.sh" # was /boot/init_script
Build.sh changes:
prepare_boot_env: dropped the 30+ mountpoint mkdir list (now only /sysroot, /upper, /dev); dropped the /etc/login.conf workaround (chroot makes it unnecessary); added unionfs.ko to the kept-modules list; cp init.sh from overlays/ top-level instead of overlays/boot/init_script.generate_iso: rootfs.uzip now lives at cdroot top-level (not /boot/rootfs.uzip) so /init.sh can mdconfig -f /rootfs.uzip directly.Live-mode tweaks preserved (rcorder surgery, SMBIOS hostname, VirtualBox detect, sendmail/linux/dbus rc.conf overrides) — paths retargeted to /sysroot/… since they run before the chroot.
Footprint impact: disk unchanged; RAM at idle ~15–70 MB lower; boot speed faster (two recursive cp -R passes go away); cognitive load much lower (one mount stack instead of nineteen).
Workflow split into three jobs: build → test → release. release only fires on push to main and only when both prior jobs pass.
tests/boot-test.sh (lifted from freebsd-launchd, simplified to single stage): runs qemu-system-x86_64 with OVMF, KVM if available else TCG single-thread, -display none -serial stdio. Watches the serial log for any of: "login:" (getty prompt), "Starting local daemons" (rc multi-user marker), or "Welcome to Gershwin/FreeBSD" (banner). 10-minute timeout. Boot log uploaded as artifact on failure.
The test job runs on ubuntu-latest (not the freebsd-vm) and pulls qemu+expect+ovmf via apt-get — same pattern as freebsd-launchd's CI.
/etc/rc.d/root within 10 minutes (CI run 25495932941, commit 6ff5abe).init.sh cascade clean, /etc/rc reaches multi-user.Goal: replace stock /sbin/init+rc.d with launchd as PID 1, while keeping the same package set and the same observable services. This is the bulk of the work.
Add a new build stage in build.sh, between build_gershwin_components and configure_system. Cloned inside the chroot to match gershwin's existing pattern (build.sh:281 already clones gershwin-developer the same way).
build_launchd() {
log "Building freebsd-launchd from source..."
# Host-side git clone writing into the chroot's Sources tree.
# Matches gershwin's existing build.sh:281 pattern verbatim — same
# destination convention (gershwin-developer's $SCRIPT_DIR/../Sources
# from Checkout.sh:10) and same in-chroot Sources layout. Lands as
# a sibling to libdispatch, tools-make, libobjc2, libs-base,
# libs-corebase, libs-gui, libs-back already cloned by Checkout.sh.
git clone --depth 1 https://github.com/pkgdemon/freebsd-launchd \
"${RELEASE_DIR}/Developer/Library/Sources/freebsd-launchd"
# Build setup mirrors build_gershwin_components: resolv.conf for
# any network-touching configure step, devfs for /dev/null-style
# subprocess pipes that gmake/configure want.
cp /etc/resolv.conf "${RELEASE_DIR}/etc/resolv.conf"
mount -t devfs devfs "${RELEASE_DIR}/dev" 2>/dev/null || true
# GNUstep environment is already populated by gershwin-developer's
# make install. launchd just needs to compile against libgnustep-base
# and libgnustep-corebase, both already at /System/Library/Libraries.
chroot "${RELEASE_DIR}" sh -c "
. /System/Library/Makefiles/GNUstep.sh &&
cd /Developer/Library/Sources/freebsd-launchd &&
./configure --prefix=/ &&
gmake -j\$(sysctl -n hw.ncpu) &&
gmake install
"
umount "${RELEASE_DIR}/dev" 2>/dev/null || true
rm -f "${RELEASE_DIR}/etc/resolv.conf"
}
Wired into main at build.sh:441:
build_gershwin_components
build_launchd # NEW
configure_system
Phase 1 already moved init.sh to cdroot top-level, restructured to the /sysroot unionfs model, and delivered the live-mount cascade body. Phase 2 only needs to change how the kernel exec's init.sh (init_script kenv → Option D shebang) and what init.sh does at the end (set init_chroot kenv → exec launchd in the chroot).
Concrete migration:
loader.conf: remove init_script="/init.sh" and init_shell="/rescue/sh"; add init_path="/init.sh". (The #!/rescue/sh shebang on init.sh is already there from Phase 1.) With Option D, the kernel's imgact_shell resolves the shebang and exec's /init.sh as PID 1 directly — no /sbin/init, no init_chroot kenv path.init.sh tail: replace
kenv init_chroot=/sysroot
kenv -u init_script
kenv -u init_shell
exit 0
with
exec chroot /sysroot /sbin/launchd
The cascade body above (mdconfig + ufs + tmpfs + unionfs + devfs + gershwin live tweaks) stays unchanged.init.sh. launchd reads its own dependency graph from plist RunAtLoad/KeepAlive/socket-activation keys; it doesn't care about # REQUIRE: lines in /etc/rc.d/*.Reference: freebsd-launchd's plan §boot walks through the Option D mechanics. The diff against Phase 1's init.sh is small (delete the rcorder block, swap the last 4 lines for one).
Phase 2's launchd plist set is small. Per user direction 2026-05-07: only port what's actually needed to boot gershwin to login. Most services don't earn their plist yet.
| Plist | Source | Notes |
|---|---|---|
org.freebsd.varrunorg.freebsd.syslogdorg.freebsd.cronorg.freebsd.gettyorg.freebsd.dhcpcdorg.freebsd.kmodloader |
freebsd-launchd | Bedrock plists; install via gmake install in Phase 2's build_launchd stage. Already authored. |
org.gershwin.dshelper |
NEW (gershwin-specific) | Replaces dshelper_enable=YES. Authored against the rc.d source for dshelper (user will share when we're authoring). |
org.gershwin.loginwindow |
NEW (gershwin-specific) | Replaces loginwindow_enable=YES. Authored against the rc.d source for loginwindow. |
Everything else in configure_system stays as-is. dbus, cupsd, avahi-daemon, avahi-dnsconfd, ntpd, smartd, moused, webcamd, dsbdriverd, initgfx — none get launchd plists in Phase 2. With launchd as PID 1 there is no /sbin/init, no /etc/rc, and so the _enable=YES lines for those services don't fire — they sit harmlessly in rc.conf. Gershwin boots to login with just the bedrock + dshelper + loginwindow + getty + dhcpcd; the desktop reaches a usable login screen without dbus or the rest.
Phase 3 follow-ups (one at a time, each on user signal):
org.freedesktop.dbus.plist — likely first, enables IPC for desktop components. User flagged this 2026-05-07.Two new lines added to configure_system's sysrc block, two implicitly removed:
root_rw_mount="NO" if not already in init.sh's runtime overrides — but init.sh already covers this; leave configure_system alone unless the runtime override stops being applied.dshelper_enable="YES" and loginwindow_enable="YES" — replaced by their plists. Removing prevents rc.d (which isn't running) from being a phantom dependency in future audits._enable=YES line. Dead weight under launchd PID 1, but harmless.login: prompt" criterion — getty isn't shipped under Phase 2's gershwin shape.)org.gershwin.dshelper, org.gershwin.loginwindow, org.gnustep.gdomap) load — dshelper PID 103 confirmed running in CI run 25504336947.dhcpcd leases an IP on the test interface (em0: leased 10.0.2.15 from QEMU's NAT).Out of scope for Phase 2: dbus, cupsd, avahi, ntpd, smartd, moused, webcamd. Those start under rc.d today; with launchd as PID 1 they don't start at all. Their plists land in Phase 3+ on user signal.
Known gap — single-user mode (deferred): boot -s at the loader sets RB_SINGLE in kern.boothowto, but our init.sh doesn't check it — it always exec's launchd, which then tries to load LoginWindow on a system with no X. Stock FreeBSD and macOS both handle single-user as a "root shell on console, no auth, no daemons" recovery mode. Implementation sketch for the future: branch in init.sh after the cascade — if boothowto & 0x10, exec chroot /sysroot /bin/sh -i; else exec chroot /sysroot /sbin/launchd. No getty needed (single-user bypasses auth). Routing around launchd in init.sh is simpler than porting macOS's single-user-aware launchd codepath. Per user direction 2026-05-07: "this can come much later it isn't important right now. i just want to document the gap."
Phase 3 is the long tail of Apple-shaped helper daemons that earn their keep one at a time. None of them block Phase 2 from being usable. Listed in priority order:
| Component | Replaces | Trigger to land |
|---|---|---|
| kmodloader | initgfx + dsbdriverd + manual kld_list= entries | When a class of hardware (GPU, NIC, USB) regularly fails to bind in the field. Most of the freebsd-launchd version is reusable wholesale; only the GPU vendor map needs gershwin-specific tuning if any. |
| netconfigd stub | Nothing yet — it's foundational | When the desktop needs a programmable view of network state (Network preference pane, WiFi switcher). Phase 1 stub from freebsd-launchd ships as-is. |
| ASL | FreeBSD-syslogd | When console-noise filtering becomes worth months of work. Today: dhclient and dhcpcd-style spam goes to /var/log/messages; ASL's structured filtering would cleanly suppress per-Sender at the daemon side. Major port; defer. |
| mDNSResponder | Avahi | When .local resolution against Avahi is unreliable. Apple's mDNSPosix layer is portable; the swap is mostly configuration. Could be small. |
| notifyd | Nothing yet | When a desktop component wants Apple's lightweight pub/sub event bus. |
| DiskArbitration | Workspace File Viewer's ad-hoc kqueue polling | When the Devices sidebar needs proper attach/detach events. |
Once Phase 2 lands and gershwin boots reliably with launchd as PID 1 + a working org.gershwin.loginwindow.plist, do this single follow-up commit on pkgdemon/gershwin-on-freebsd:main. End-user UX takes precedence over development diagnostics from this point.
resources/overlays/boot/loader.mute.d/loader.conf — uncomment boot_mute="YES" (restore the polished animation).resources/overlays/boot/loader.conf — drop the diagnostics block: boot_verbose="YES", console="comconsole vidconsole", comconsole_speed="115200"..github/workflows/build.yml — collapse the build → test → release split back to the original single-job build + publish flow. Drop the test job and its needs: wiring on release.tests/boot-test.sh — delete.tests/ — delete the directory if no other tests live there.Why deferred, not skipped: during Phase 1/2 the verbose boot output + the boot-test gate are valuable diagnostic surface — both for catching kernel/init regressions in CI and for reading dmesg on real hardware while the architecture is in flux. After launchd + loginwindow stabilize, neither earns its keep. The CI overhead of a per-push boot-test (build + qemu run on every push) becomes pure tax once the fast-iteration phase ends.
What does NOT need reverting: the exec >/dev/console 2>&1 in init.sh (commit 206f8a7). It lives inside the else branch of the boot_mute check — when boot_mute="YES" is restored, the silencing branch fires and the explicit redirect is dead code. Leave it; it gives future debugging cycles visible output for free.
Single commit suggested title: "post-Phase-2 cleanup: restore quiet boot, retire CI boot-test gate" with reference to commits 02e4e9a + 7caefbf in the body.
Three things share the name "LoginWindow"; keep them straight.
| Artifact | What it is | Phase |
|---|---|---|
/Local/Library/Preferences/LoginWindow.plist |
Existing gershwin state file. Tracks last-logged-in user and last session script. Written by init_script today (lines 133-139). Two-key dictionary, no launchd semantics. |
Already shipped. Keep as-is. |
/System/Library/LaunchDaemons/org.gershwin.loginwindow.plist |
NEW launchd job to be authored in Phase 2. Replaces the rc.d loginwindow_enable=YES. Starts whatever binary today's gershwin loginwindow rc script starts (likely a SLiM-style greeter or a Gershwin-native equivalent — needs identification before authoring). |
Phase 2. |
Apple's loginwindow.app |
Closed-source macOS daemon. Per the LoginWindow research: not portable — depends on SkyLight/CGSSession internals with no FreeBSD analog, on per-session launchd domains we don't yet model, on Security.framework keychain. Real port is a clean-room reimplementation against GNUstep AppKit + a chosen WindowServer + OpenPAM, gated on prerequisites that don't exist. | Phase 4+, research only. |
Phase 2's org.gershwin.loginwindow.plist is the small, concrete, useful thing. Sketch:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.gershwin.loginwindow</string>
<key>ProgramArguments</key>
<array>
<!-- TODO: identify what gershwin's current loginwindow rc script
actually exec's; pkg shows it under /usr/local/etc/rc.d/loginwindow
after install_gershwin_software lands. Likely a wrapper script
around slim or a custom greeter -->
<string>/usr/local/sbin/loginwindow</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/loginwindow.log</string>
<key>StandardErrorPath</key>
<string>/var/log/loginwindow.err</string>
</dict>
</plist>
Authored once we read the actual rc script in the post-install image — the ProgramArguments path is a guess until then. StandardOutPath/StandardErrorPath are recorded as not-yet-implemented in freebsd-launchd's plist parser; they're written for the eventual implementation, ignored harmlessly today.
| File | Phase 1 | Phase 2 |
|---|---|---|
build.sh:281 | done — 1d2150b --branch feat/libs-corebase added. | Insert build_launchd stage between build_gershwin_components and configure_system. |
build.sh:224-257 (configure_system rc.conf block) | Untouched. | Strip _enable=YES lines for converted services; keep tunables. |
build.sh:319-389 (prepare_boot_env) | done — 3b97e40 dropped 30+ mountpoint mkdir; only /sysroot, /upper, /dev; dropped /etc/login.conf workaround; added unionfs.ko to keep list; cp init.sh from overlays top-level. | Untouched (init.sh tail change is in the overlay, not build.sh). |
build.sh generate_iso | done — 3b97e40 uzip moved from /boot/rootfs.uzip to /rootfs.uzip. | Untouched. |
resources/overlays/boot/init_script (old) | done — 3b97e40 deleted; replaced by resources/overlays/init.sh. | — |
resources/overlays/init.sh (new) | done — 3b97e40 single-root unionfs cascade + gershwin live tweaks against /sysroot/… + kenv init_chroot=/sysroot + exit. | Delete rcorder surgery block; replace last 4 lines (kenv init_chroot + cleanup + exit 0) with single line exec chroot /sysroot /sbin/launchd. |
resources/overlays/boot/loader.conf | done — 3b97e40 init_script="/init.sh", init_shell="/rescue/sh", unionfs_load="YES". | Drop init_script + init_shell; add init_path="/init.sh". |
resources/overlays/Local/Library/Preferences/LoginWindow.plist | Untouched. | Untouched (this is the state file, not the launchd plist). |
resources/overlays/System/Library/LaunchDaemons/*.plist | — | NEW directory; ~15 plists land here over the rollout. |
resources/packages/gershwin | Untouched. | Add dhcpcd and wpa_supplicant. |
resources/packages/base | Untouched. | Remove FreeBSD-dhclient and FreeBSD-wpa. |
resources/packages/vital-base | Untouched. | Remove FreeBSD-dhclient and FreeBSD-wpa if present. |
resources/packages/{drivers,vital-gershwin} | Untouched. | Untouched — settled work; not re-audited. |
tests/boot-test.sh | done — 7caefbf qemu+OVMF+expect; multi-marker. | Add a second grep for launchd: PID 1 ready. |
.github/workflows/build.yml | done — 7caefbf split into build → test → release; release gated by boot-test. | Untouched (boot-test detects launchd via grep). |
| File | Change |
|---|---|
| (none required) | We consume the feat/libs-corebase branch as-is. If the branch needs to merge to main and pick up an upstream PR before we can drop the --branch flag, that's a separate gershwin-developer task. |
| File | Change |
|---|---|
build.sh (the freebsd-launchd one) | Untouched. freebsd-launchd remains independently buildable as a standalone livecd. Gershwin-on-freebsd just consumes the launchd binary and plists, not the build.sh. |
kmodloader/, configd/ | Untouched in Phase 2; potentially adopted in Phase 3. |
init_chroot chroot operates against a real mount stack (uzip + tmpfs + unionfs at /sysroot); the kernel's namei sees the firmware files through the same mount tree the userspace chroot sees. The freebsd-launchd kernel/userspace namespace split that motivated the symlink doesn't apply. Revisit only if hardware testing surfaces a firmware-loading failure under the new architecture.
loginwindow_enable=YES sysrc points at an rc.d script in /usr/local/etc/rc.d/loginwindow that lands during install_gershwin_software. Need to read that script after a fresh build to identify the command_args + executable path — drives the Phase 2 launchd plist's ProgramArguments. Likely shipped by one of the gershwin-* packages (gershwin-system or gershwin-workspace).
dsbdriverd overlap with kmodloader. Phase 2 keeps dsbdriverd. Phase 3's kmodloader does devmatch-based kmod loading + a GPU PCI scan, which is approximately what dsbdriverd does. Need a side-by-side coverage comparison before retiring either. Tabling for Phase 3.
/usr/share/keys/pkgbase-15 — version-suffixed. When gershwin-on-freebsd bumps from 15 to 16, this path moves. build.sh currently doesn't reference it (host's pkg knows; the chroot inherits the repo conf). Worth a comment somewhere, no immediate change.
feat/libs-corebase. Per user direction 2026-05-06./Developer/Library/Sources/freebsd-launchd/, matching gershwin-developer's $SCRIPT_DIR/../Sources convention. Per user direction 2026-05-06.dhcpcd from ports and removes FreeBSD-dhclient from the base list in the same commit. Lifts freebsd-launchd's existing org.freebsd.dhcpcd.plist verbatim. Per user direction 2026-05-06.wpa_supplicant from ports and removes FreeBSD-wpa from the base list in the same commit. Per user direction 2026-05-06.resources/packages/{base,vital-base,gershwin,drivers,vital-gershwin} entries are not re-audited, re-derived, or pruned beyond those four lines, regardless of what looks redundant under launchd. Per user direction 2026-05-06.init_chroot kenv pivot (mirroring freebsd-livecd-unionfs). Phase 2 then becomes a single-line tail change to init.sh + loader.conf knob swap.init_chroot kenv (read by stock /sbin/init after init_script exits). Phase 2 swaps to Option D shebang (kernel exec's /init.sh directly as PID 1, replacing init entirely). Both are documented mechanisms in FreeBSD; Option D is freebsd-launchd-specific because launchd replaces /sbin/init. Per agent research 2026-05-06.init_chroot chroot operates against a real mount tree, so kernel-namei resolves /sysroot/boot/firmware through the unionfs to the real files in the uzip. The freebsd-launchd bug doesn't apply. Reconsider only if hardware testing fails. Decision 2026-05-06.build.sh:281 clones gershwin-developer inside the chroot, and Checkout.sh (running in chroot) clones each upstream into /Developer/Library/Sources/. Phase 2's build_launchd follows gershwin's pattern (chroot git clone) to keep one consistent integration shape. Per user direction 2026-05-07.pkgdemon/gershwin-on-freebsd (the user's fork) directly on main; no PRs to gershwin-desktop, no topic branches. Per user direction 2026-05-06.init.c + diff'ing freebsd-livecd-unionfs:
(a) /sbin/init mounts devfs after running init_script, so init's open_console() for the child's stdio falls back to /dev/null. Fix: init.sh mounts devfs first, then re-execs stdio onto /dev/console.
(b) boot_serial="YES" was missing in loader.conf — userspace stdio routed to vidconsole (invisible under QEMU -display none). Working repo had it; we didn't.
/etc/rc.d/root aborts the boot trying mount -uw / (mount(8) inspects the global kernel mount table, which still reports / as cd9660 even after the chroot). Fix: root_rw_mount="NO" in /sysroot/etc/rc.conf, same as freebsd-livecd-unionfs.login:, Starting local daemons, Setting hostname, Mounting local filesystems.Plan v3, 2026-05-07 — Phase 1 + Phase 2 fully landed and CI-green (Phase 2 ends at run 25504336947, commit ebe7320). Phase 3 polish (dbus, kmodloader, gdomap lo0 fix, dshelper daemonization, ASL, single-user mode) follows on user signal.