-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgershwin-livecd-launchd-plan.html
More file actions
647 lines (551 loc) · 58.7 KB
/
Copy pathgershwin-livecd-launchd-plan.html
File metadata and controls
647 lines (551 loc) · 58.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>gershwin-on-freebsd: livecd rework + launchd port — plan</title>
<style>
:root {
--bg: #fbfbf8;
--fg: #1a1a1a;
--muted: #555;
--accent: #b03000;
--accent2: #0a4d68;
--ok: #1f7a1f;
--warn: #b06800;
--bad: #b00020;
--code-bg: #f0ece4;
--rule: #d6cfc0;
--card: #fff;
}
html { -webkit-text-size-adjust: 100%; }
body { margin: 0 auto; max-width: 980px; padding: 2.5rem 1.5rem 6rem;
font: 16px/1.55 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
color: var(--fg); background: var(--bg); }
h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 .25rem; }
h2 { font-size: 1.4rem; margin: 2.5rem 0 .75rem; padding-bottom: .25rem; border-bottom: 2px solid var(--rule); }
h3 { font-size: 1.15rem; margin: 1.75rem 0 .5rem; color: var(--accent2); }
h4 { margin: 1.25rem 0 .35rem; }
.subtitle { color: var(--muted); font-size: 1.05rem; margin: 0 0 2rem; }
code, pre, kbd { font-family: "SF Mono", Menlo, Consolas, monospace; }
code { background: var(--code-bg); padding: 1px 5px; border-radius: 3px; font-size: .9em; }
pre { background: var(--code-bg); padding: .85rem 1rem; border-radius: 6px;
overflow-x: auto; font-size: .82rem; line-height: 1.45;
border-left: 3px solid var(--accent2); }
pre code { background: none; padding: 0; }
pre.shell { border-left-color: var(--ok); }
pre.plist { border-left-color: var(--accent); }
pre.warn-pre { border-left-color: var(--warn); background: #fff5e6; }
a { color: var(--accent2); }
a:hover { color: var(--accent); }
.tldr { background: var(--card); border: 1px solid var(--rule); border-left: 4px solid var(--accent2);
padding: 1rem 1.25rem; border-radius: 6px; margin-bottom: 2rem; }
.tldr h3 { margin-top: 0; color: var(--accent2); }
.pill { display: inline-block; font-size: .72rem; padding: 1px 8px; border-radius: 999px;
background: #eee; color: #333; margin-left: .35rem; vertical-align: middle;
font-weight: 600; letter-spacing: .02em; }
.pill.ok { background: #d8efd8; color: var(--ok); }
.pill.warn { background: #f6e4cb; color: var(--warn); }
.pill.bad { background: #f5d0d6; color: var(--bad); }
.pill.info { background: #d6e6f3; color: var(--accent2); }
.grid { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; margin: 1rem 0; }
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
.card { background: var(--card); border: 1px solid var(--rule); border-radius: 6px;
padding: 1rem 1.1rem; }
.card h4 { margin-top: 0; }
.phase { background: var(--card); border: 1px solid var(--rule); border-radius: 6px;
padding: 1.2rem 1.4rem; margin: 1rem 0; }
.phase h3 { margin-top: 0; }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .92rem; }
th, td { text-align: left; padding: .5rem .65rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
th { background: #eee5d6; }
tr:nth-child(even) td { background: #faf6ed; }
blockquote { margin: 1rem 0; padding: .5rem 1rem; border-left: 3px solid var(--accent);
background: #fff8f3; color: #333; }
.footnote { font-size: .85rem; color: var(--muted); }
ol li, ul li { margin: .25rem 0; }
.nav { position: sticky; top: 0; background: var(--bg); margin: -2.5rem -1.5rem 2rem;
padding: .75rem 1.5rem; border-bottom: 1px solid var(--rule);
font-size: .88rem; z-index: 10; }
.nav a { margin-right: .9rem; text-decoration: none; }
.filename { color: var(--accent); font-family: "SF Mono", Menlo, Consolas, monospace; font-size: .9em; }
hr { border: 0; border-top: 1px dashed var(--rule); margin: 2rem 0; }
.resolved { background: #ecf7ec; border-left: 4px solid var(--ok); padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
.resolved strong { color: var(--ok); }
.risk { background: #fff5e6; border-left: 4px solid var(--warn); padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
.risk strong { color: var(--warn); }
.open-q { background: #fff8d6; border: 1px solid #e5d76b; padding: .8rem 1rem; margin: 1rem 0; border-radius: 6px; font-size: .92rem; }
.open-q strong { color: #7a5e00; }
.constraint { background: #f1edff; border: 1px solid #c8bce5; border-left: 4px solid #5a3fad; padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
.constraint strong { color: #4a2faa; }
</style>
</head>
<body>
<nav class="nav">
<a href="#tldr">Status</a>
<a href="#goal">Goal</a>
<a href="#constraints">Constraints</a>
<a href="#sot">Source of truth</a>
<a href="#arch">Architecture</a>
<a href="#phase1">Phase 1: livecd</a>
<a href="#phase2">Phase 2: launchd</a>
<a href="#phase3">Phase 3: polish</a>
<a href="#cleanup">Cleanup</a>
<a href="#loginwindow">LoginWindow</a>
<a href="#files">Files</a>
<a href="#open">Open questions</a>
</nav>
<h1>gershwin-on-freebsd: livecd rework + launchd port</h1>
<p class="subtitle">Plan to integrate the <a href="https://github.com/pkgdemon/freebsd-launchd">freebsd-launchd</a> work (livecd architecture + launchd PID 1 + kmodloader + configd stub) into the <a href="https://github.com/gershwin-desktop/gershwin-on-freebsd">gershwin-on-freebsd</a> project. Sequenced: <strong>livecd rework first</strong>, then launchd, then the Apple-shaped helper daemons. Holds the line on packages — gershwin-on-freebsd's existing <code>resources/packages/</code> lists are the source of truth and we add nothing new in either phase.</p>
<section id="tldr" class="tldr">
<h3>Status: <span class="pill ok">Phase 1 + Phase 2 landed (CI-green)</span> <span class="pill info">v3 — Phase 3 polish next</span></h3>
<ul>
<li><strong>Two repos involved:</strong>
<a href="https://github.com/pkgdemon/gershwin-on-freebsd">pkgdemon/gershwin-on-freebsd</a> (livecd build + package lists; we work in pkgdemon's fork only)
and <a href="https://github.com/gershwin-desktop/gershwin-developer">gershwin-developer</a> (GNUstep + desktop install orchestrator, <code>feat/libs-corebase</code> branch). One new dep:
<a href="https://github.com/pkgdemon/freebsd-launchd">freebsd-launchd</a> cloned into <code>/Developer/Library/Sources/freebsd-launchd/</code> in Phase 2.</li>
<li><strong>Phase 1 (livecd rework) — landed and CI-green:</strong> commits on <code>pkgdemon/gershwin-on-freebsd:main</code>:
<ol style="margin:.4rem 0">
<li><code>1d2150b</code> — pin gershwin-developer to <code>feat/libs-corebase</code> branch (one-line build.sh change).</li>
<li><code>7caefbf</code> — split CI workflow into build → boot-test → release jobs; add <code>tests/boot-test.sh</code> (qemu+OVMF+expect). Release only publishes when boot-test passes.</li>
<li><code>3b97e40</code> — <strong>architectural rework:</strong> replace per-subdir nullfs + <code>cp -R</code> model with single-root unionfs + <code>init_chroot</code> kenv pivot, mirroring <a href="https://github.com/pkgdemon/freebsd-livecd-unionfs">freebsd-livecd-unionfs</a>. <code>/init.sh</code> at cdroot top, mdconfig the uzip, mount UFS at <code>/sysroot</code>, tmpfs at <code>/upper</code>, unionfs combining them, devfs at <code>/sysroot/dev</code>, then <code>kenv init_chroot=/sysroot</code> + exit. <code>/sbin/init</code> stays PID 1 and chroots into the unionfs before multi-user. RAM saving at idle: ~15-70 MB (no physical <code>/etc</code> + <code>/var</code> tmpfs copies). Mount points: 6 instead of 19.</li>
<li><code>a7f01f4</code> — mkdir <code>${CD_ROOT}/etc</code> for <code>mkisoimages.sh</code>'s transient fstab (it writes <code>/etc/fstab</code> at line 69 and removes it at line 71; the dir needs to exist).</li>
<li><code>02e4e9a</code> — boot diagnostic settings: <code>boot_verbose="YES"</code>, <code>console="comconsole vidconsole"</code>, comment out <code>boot_mute</code> in <code>loader.mute.d/loader.conf</code>, 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.</li>
<li><code>206f8a7</code>, <code>e342e57</code> — successive attempts to make <code>init.sh</code>'s output visible: explicit <code>exec >/dev/console 2>&1</code>, then progress markers and fail-fast guards on each mount.</li>
<li><code>04c7bc5</code> — root cause for the silent boot, identified by reading <code>init.c</code> + diff'ing against freebsd-livecd-unionfs: (a) init mounts devfs <em>after</em> running <code>init_script</code> (init.c:343-389 is post-326-336), so init's <code>open_console()</code> falls back to <code>/dev/null</code> for the child's stdio. Fix: <code>init.sh</code> mounts devfs at <code>/dev</code> first, then re-execs stdio. (b) <code>boot_serial="YES"</code> was missing — userspace stdio needs it to route to comconsole; without it everything goes to vidconsole (invisible under QEMU <code>-display none</code>).</li>
<li><code>6ff5abe</code> — final boot blockers: <code>root_rw_mount="NO"</code> in <code>/sysroot/etc/rc.conf</code> (otherwise <code>/etc/rc.d/root</code> tries <code>mount -uw /</code> which dispatches to mount_cd9660 and aborts the entire boot). Plus <code>kld_list</code> reassignment instead of <code>+=</code> (FreeBSD sh doesn't parse <code>var+=</code> in sourced rc.conf). Plus /rescue-compat for the SMBIOS hostname / EFI MonkeyPatch / clear calls (xargs, grep, clear missing from /rescue).</li>
</ol>
<strong>Result:</strong> CI run 25495932941 is green — build (12-13m) → boot-test (~3m, real multi-user reach) → release publish.
</li>
<li><strong>Phase 2 (launchd) — landed and CI-green:</strong> three commits on <code>pkgdemon/gershwin-on-freebsd:main</code>:
<ol style="margin:.4rem 0">
<li><code>fc6cbce</code> — cutover commit. New <code>build_launchd</code> stage (host-clone freebsd-launchd, run <code>make-launchd.sh</code> in chroot, install bedrock plists + getty wrapper, copy gershwin overlays). 3 gershwin plists authored (<code>org.gnustep.gdomap</code>, <code>org.gershwin.dshelper</code>, <code>org.gershwin.loginwindow</code>). <code>init.sh</code> tail swapped to <code>exec chroot /sysroot /sbin/launchd</code>. <code>loader.conf</code> flipped to Option D (<code>init_path="/init.sh"</code>). Packages: added <code>dhcpcd</code> + <code>wpa_supplicant</code>; removed <code>FreeBSD-dhclient</code> + <code>FreeBSD-wpa</code>. <code>configure_system</code>: stripped the two replaced <code>_enable=YES</code> lines.</li>
<li><code>8da5b4d</code> — round 1 fixes from first CI. Dropped <code>vidconsole</code> from <code>console=</code> (dual-console claimed ttyu0 as kernel console-session, blocking getty). Added <code>/System/Library/Libraries</code> to <code>ldconfig -m</code> so dshelper finds <code>libdispatch.so</code>. Added <code>-f</code> to gdomap's ProgramArguments to stop it from forking-and-detaching.</li>
<li><code>ebe7320</code> — round 2 fixes. Dropped getty plists (<code>console</code> + <code>vty0</code>) entirely — gershwin uses LoginWindow as the login UI; getty was redundant and looping on <code>login_tty: Operation not permitted</code>. Updated boot-test markers to LoginWindow- and dhcpcd-derived signals (<code>LoginWindow[</code>, <code>Successfully registered</code>, <code>em0: leased</code>). The "no getty" decision matches macOS shape (loginwindow.app on macOS isn't paired with text getty either).</li>
</ol>
<strong>Result:</strong> CI run 25504336947 — build (16m38s) → boot-test (~3m, real LoginWindow reach) → release publish.
<br><br>
<strong>Known non-blocking issues to clean up later:</strong>
<ul>
<li><code>dshelper</code>: first instance runs fine; subsequent KeepAlive respawns log <em>"Already running, PID 103"</em> 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).</li>
<li><code>gdomap</code>: <em>"I can't find the loopback interface"</em>. lo0 isn't auto-up. Need a one-shot plist or init.sh step to <code>ifconfig lo0 up</code> before gdomap probes. Not blocking the boot today.</li>
<li>Single-user mode gap: documented further down.</li>
</ul>
</li>
<li><strong>Phase 3 (polish):</strong> bring in kmodloader, configd stub, ASL when each lands and earns its keep.</li>
<li><strong>Post-launchd cleanup (deferred):</strong> once Phase 2's launchd + loginwindow plist are stable, restore the polished boot UX (re-enable <code>boot_mute</code>, drop the verbose / dual-console diagnostics) and retire the CI boot-test gate (collapse the workflow back to a single build → publish job, delete <code>tests/boot-test.sh</code>). See <a href="#cleanup">§Cleanup</a>.</li>
<li><strong>Packages constraint:</strong> <code>resources/packages/{base,vital-base,gershwin,drivers,vital-gershwin}</code> are the source of truth, and the existing entries are <strong>not re-audited</strong>. We don't re-derive what gershwin's pkg-base list should contain — that work is done. <strong>No new kernel modules / GPU drivers</strong> (no <code>nvidia-drm-kmod</code>, etc.). Phase 2 swaps the DHCP/WPA stacks: <code>dhcpcd</code> and <code>wpa_supplicant</code> from ports get added to <code>resources/packages/gershwin</code>; <code>FreeBSD-dhclient</code> and <code>FreeBSD-wpa</code> get removed from <code>resources/packages/base</code> (and <code>vital-base</code>) in the same commit. The ISO ships exactly one DHCP client and one supplicant.</li>
<li><strong>Base system:</strong> already pkg-base in gershwin-on-freebsd — no change. freebsd-launchd's <code>base.txz</code>+<code>tar -xJf</code> approach is dropped on the floor when the work moves over.</li>
<li><strong>LoginWindow:</strong> no port of Apple's <code>loginwindow.app</code>. Phase 2 ships a launchd plist that starts gershwin's existing greeter binary (or wraps the existing <code>loginwindow_enable=YES</code> 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.</li>
</ul>
</section>
<h2 id="goal">Goal</h2>
<p>Take what we've learned building <a href="https://github.com/pkgdemon/freebsd-launchd">freebsd-launchd</a> — the livecd boot pattern (cd9660 → mkuzip → tmpfs → unionfs cascade), the <code>/boot/firmware</code> 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.</p>
<p>Gershwin-on-freebsd already does most of the work. It already uses pkg-base. It already builds a hybrid EFI/BIOS ISO via <code>mkisoimages.sh</code>. It already does the cd9660+uzip+tmpfs+unionfs cascade. It already builds the GNUstep stack into <code>/System/Library/</code>. 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).</p>
<h2 id="constraints">Constraints (hard)</h2>
<div class="constraint">
<strong>Packages.</strong> <code>gershwin-on-freebsd/resources/packages/</code> is the canonical package set, and <strong>we do not re-audit it</strong>. Don't redo the work of figuring out what pkg-base entries gershwin needs — that's been settled. <strong>No new kernel modules / GPU drivers</strong> (no <code>nvidia-drm-kmod</code>; existing <code>drm-kmod</code> in <code>drivers</code> stays as-is). The <strong>only</strong> package edits across this whole plan are four lines, all in Phase 2:
<ul>
<li>add <code>dhcpcd</code> to <code>resources/packages/gershwin</code></li>
<li>add <code>wpa_supplicant</code> to <code>resources/packages/gershwin</code></li>
<li>remove <code>FreeBSD-dhclient</code> from <code>resources/packages/{base,vital-base}</code></li>
<li>remove <code>FreeBSD-wpa</code> from <code>resources/packages/{base,vital-base}</code></li>
</ul>
Nothing else changes package-wise. No package list reorganization, no other strip-outs, no other adds, regardless of whether some other entry "looks redundant" under launchd.
</div>
<div class="constraint">
<strong>Gershwin install orchestration.</strong> <a href="https://github.com/gershwin-desktop/gershwin-developer">gershwin-developer</a> on the <code>feat/libs-corebase</code> branch is the canonical installer for the GNUstep system domain plus the desktop. <code>build.sh</code> already invokes it; the only change is the branch flag (<code>git clone --branch feat/libs-corebase --depth 1 …</code>). We do not vendor or fork the GNUstep build steps into gershwin-on-freebsd.
</div>
<div class="constraint">
<strong>Sequencing.</strong> Livecd rework first; launchd after that's stable. <strong>Phase 1 done</strong> — including the <code>init_script</code> → <code>/init.sh</code> 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.
</div>
<div class="constraint">
<strong>freebsd-launchd source location.</strong> Cloned into <code>/Developer/Library/Sources/freebsd-launchd/</code> via <code>chroot git clone</code> from inside <code>build.sh</code>, matching gershwin's existing pattern (<code>build.sh:281</code> 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.
</div>
<h2 id="sot">Source-of-truth split</h2>
<table>
<thead>
<tr><th>Repo</th><th>Owns</th><th>Branch</th></tr>
</thead>
<tbody>
<tr>
<td><strong>gershwin-on-freebsd</strong></td>
<td>Package lists (<code>base</code>, <code>vital-base</code>, <code>gershwin</code>, <code>drivers</code>, <code>vital-gershwin</code>); livecd build script (<code>build.sh</code>); <code>init_script</code> / <code>/init.sh</code>; loader config; ISO mastering; CI; LaunchDaemon plists in Phase 2.</td>
<td><code>main</code></td>
</tr>
<tr>
<td><strong>gershwin-developer</strong></td>
<td>GNUstep system-domain build (libdispatch → tools-make → libobjc2 → libs-base → libs-corebase → libs-gui → libs-back); desktop apps build; clones into <code>/Developer/Library/Sources/</code>; <code>Install-System-Domain.sh</code> drives <code>make install</code>.</td>
<td><code>feat/libs-corebase</code></td>
</tr>
<tr>
<td><strong>freebsd-launchd</strong></td>
<td>launchd PID 1 binary + LaunchDaemon plists; kmodloader; netconfigd stub. Cloned by gershwin-on-freebsd's <code>build.sh</code> in Phase 2 into <code>/Developer/Library/Sources/freebsd-launchd/</code>.</td>
<td><code>main</code></td>
</tr>
</tbody>
</table>
<p>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.</p>
<h2 id="arch">Architecture (target end state)</h2>
<div class="ascii-diagram" style="font-family:'SF Mono',Menlo,Consolas,monospace;font-size:.82rem;line-height:1.3;white-space:pre;background:var(--code-bg);padding:1rem;border-radius:6px;overflow-x:auto;">
Phase 1 (landed): Phase 2 (pending):
livecd ISO boot flow same flow, last line of init.sh
swapped + loader.conf flips to Option D
+--------------------------+
| cd9660 (kernel root) | init.sh today (Phase 1):
| /boot/{loader,kernel,…}| kenv init_chroot=/sysroot
| /rootfs.uzip | exit 0
| /init.sh ◄ kernel reads init_script |
| /sysroot/ /upper/ /dev/ from loader.conf, forks /rescue/sh /init.sh
+--------------------------+ |
| v
/sbin/init runs from cd9660 (PID 1) /sbin/init reads init_chroot kenv
| chroots into /sysroot
reads init_script kenv = /init.sh continues normal multi-user
| |
/init.sh: v
mdconfig /rootfs.uzip rc.d (Phase 1) / launchd (Phase 2)
mount UFS at /sysroot (lower) |
mount tmpfs at /upper (writable) v
mount unionfs /upper /sysroot login: prompt
mount devfs /sysroot/dev |
gershwin live tweaks (rc.conf, (Phase 2 swap)
LoginWindow.plist, hostname, init.sh's last line becomes:
VirtualBox detect) exec chroot /sysroot /sbin/launchd
kenv init_chroot=/sysroot loader.conf drops init_script/init_shell
exit and sets init_path="/init.sh" (Option D)
Source-of-truth split:
+--------------------------+ +--------------------------+
| pkgdemon/gershwin-on- | | gershwin-developer |
| freebsd build.sh: | | feat/libs-corebase |
| setup_workspace | | Library/Scripts/ |
| install_base_system ───┼─pkg-base→ Bootstrap.sh |
| install_gershwin_software│ | Checkout.sh |
| build_gershwin_components│ | Install-System- |
| └─ git clone --branch ─┼──→ Domain.sh |
| feat/libs-corebase | | builds GNUstep stack |
| gershwin-developer | | + desktop apps |
| build_launchd (Phase 2) ─┼──→ /Developer/Library/ |
| └─ rsync freebsd- | | Sources/freebsd- |
| launchd into | | launchd/ (Phase 2) |
| /Developer/Library/ | | builds launchd, plists, |
| Sources/ | | kmodloader (Phase 3) |
+--------------------------+ +--------------------------+
</div>
<p>Phase 1 (landed) keeps stock <code>/sbin/init</code>+rc.d but introduces the unionfs+chroot architecture. Phase 2 changes the last line of <code>init.sh</code> and the loader knobs to swap stock <code>init</code> for launchd; the rest of the cascade body is reused unchanged. Phase 3 fills in the Apple-shaped helpers.</p>
<h2 id="phase1">Phase 1: livecd rework <span class="pill ok">landed</span></h2>
<p>Three commits on <code>pkgdemon/gershwin-on-freebsd:main</code>. Init stays stock; rc.d stays the service manager — but the boot architecture now mirrors freebsd-livecd-unionfs (single uzip rootfs + tmpfs upper + unionfs + <code>init_chroot</code> kenv pivot), making Phase 2's launchd swap a single-line change.</p>
<h3>1.1 Switch gershwin-developer to <code>feat/libs-corebase</code> <span class="pill ok">done — 1d2150b</span></h3>
<p>One-line change at <code>build.sh:281</code>:</p>
<pre class="shell"><code>git clone --branch feat/libs-corebase --depth 1 \
https://github.com/gershwin-desktop/gershwin-developer "${RELEASE_DIR}/Developer"</code></pre>
<p>Diff between <code>main</code> and <code>feat/libs-corebase</code> 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 <code>gnustep/libs-corebase</code> HEAD with a vanilla configure invocation. Once feat/libs-corebase merges to main upstream, drop the <code>--branch</code> flag — but until then, the user's open upstream PR (gershwin-developer#32) is the source of truth and we leave it alone.</p>
<p>This enables CoreFoundation-shaped APIs (<code>libgnustep-corebase.so</code>) for everything downstream — including freebsd-launchd in Phase 2, which links against it for plist parsing.</p>
<h3>1.2 <code>/boot/firmware</code> symlink — <span class="pill info">not needed</span></h3>
<div class="resolved">
<strong>Resolved (skip):</strong> the symlink trick fixes a kernel-namei vs. userspace-chroot namespace split that exists in freebsd-launchd's <em>chroot-to-/sysroot</em> model. Gershwin's Phase 1 architecture also chroots to <code>/sysroot</code>, but does so via <code>/sbin/init</code>'s <code>init_chroot</code> kenv — the kernel's view of <code>/sysroot</code> is a real mount stack (uzip + tmpfs upper + unionfs), and <code>/sysroot/boot/firmware</code> 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.
</div>
<h3>1.3 Live-mount cascade rework: single-root unionfs + <code>init_chroot</code> pivot <span class="pill ok">done — 3b97e40</span></h3>
<p>This was originally underscoped as "tighten <code>init_script</code> (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:</p>
<p><strong>Before:</strong> <code>resources/overlays/boot/init_script</code> ran a 14-mount cascade (per-subdir nullfs of <code>/Developer</code>, <code>/System</code>, <code>/Local</code>, <code>/bin</code>, <code>/lib</code>, <code>/libexec</code>, <code>/sbin</code>, <code>/usr</code>, <code>/boot</code>, <code>/root</code>, plus tmpfs at <code>/nvidia</code>, <code>/compat</code>, <code>/tmp</code>, <code>/media</code>) plus 5 unionfs mounts on top, plus <code>cp -R /media/.uzip/var → /var</code> (~10–50 MB physical copy) and <code>cp -R /media/.uzip/etc → /tmp; nullfs /tmp/etc /etc</code> (~5–20 MB physical copy). No chroot — kernel root stayed cd9660. Mount points to track: 19.</p>
<p><strong>After:</strong> <code>resources/overlays/init.sh</code> at cdroot top-level. Cascade:</p>
<pre class="shell"><code>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</code></pre>
<p><code>/sbin/init</code> (still PID 1, real FreeBSD binary from /rescue/init since /sbin/init isn't on the cd9660) reads <code>init_chroot</code> kenv at init.c:333 and chroots into <code>/sysroot</code> before continuing multi-user. Kernel root stays cd9660; userland sees the unionfs as <code>/</code>. Mount points: 6.</p>
<p>Loader.conf:</p>
<pre class="shell"><code># Phase 1 additions:
unionfs_load="YES"
init_shell="/rescue/sh"
init_script="/init.sh" # was /boot/init_script</code></pre>
<p>Build.sh changes:</p>
<ul>
<li><code>prepare_boot_env</code>: dropped the 30+ mountpoint mkdir list (now only <code>/sysroot</code>, <code>/upper</code>, <code>/dev</code>); dropped the <code>/etc/login.conf</code> workaround (chroot makes it unnecessary); added <code>unionfs.ko</code> to the kept-modules list; <code>cp init.sh</code> from <code>overlays/</code> top-level instead of <code>overlays/boot/init_script</code>.</li>
<li><code>generate_iso</code>: <code>rootfs.uzip</code> now lives at cdroot top-level (not <code>/boot/rootfs.uzip</code>) so <code>/init.sh</code> can <code>mdconfig -f /rootfs.uzip</code> directly.</li>
</ul>
<p>Live-mode tweaks preserved (rcorder surgery, SMBIOS hostname, VirtualBox detect, sendmail/linux/dbus rc.conf overrides) — paths retargeted to <code>/sysroot/…</code> since they run before the chroot.</p>
<p><strong>Footprint impact:</strong> disk unchanged; RAM at idle ~15–70 MB lower; boot speed faster (two recursive <code>cp -R</code> passes go away); cognitive load much lower (one mount stack instead of nineteen).</p>
<h3>1.4 CI boot-test gate <span class="pill ok">done — 7caefbf</span></h3>
<p>Workflow split into three jobs: <code>build</code> → <code>test</code> → <code>release</code>. <code>release</code> only fires on push to main and only when both prior jobs pass.</p>
<p><code>tests/boot-test.sh</code> (lifted from freebsd-launchd, simplified to single stage): runs <code>qemu-system-x86_64</code> with OVMF, KVM if available else TCG single-thread, <code>-display none -serial stdio</code>. 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.</p>
<p>The test job runs on <code>ubuntu-latest</code> (not the freebsd-vm) and pulls qemu+expect+ovmf via <code>apt-get</code> — same pattern as freebsd-launchd's CI.</p>
<h3>1.5 Phase 1 acceptance criteria</h3>
<ul>
<li><span class="pill ok">met</span> ISO builds in CI without changes to package lists.</li>
<li><span class="pill ok">met</span> ISO boots in QEMU past <code>/etc/rc.d/root</code> within 10 minutes (CI run 25495932941, commit 6ff5abe).</li>
<li><span class="pill ok">met</span> Post-rework architecture confirmed working under chroot — <code>init.sh</code> cascade clean, /etc/rc reaches multi-user.</li>
<li><span class="pill warn">deferred</span> Boot on the Lenovo test laptop and verify WiFi firmware loads. The architecture is structurally identical to the QEMU-validated path; hardware test before final Phase-2 ship is sufficient.</li>
<li><span class="pill ok">met</span> The continuous GH release publishes per push, gated by boot-test that verifies real multi-user reach (not the prior 1m5s loader-banner false positive).</li>
</ul>
<h2 id="phase2">Phase 2: launchd port <span class="pill warn">starts only after Phase 1 ships</span></h2>
<p>Goal: replace stock <code>/sbin/init</code>+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.</p>
<h3>2.1 Build launchd in the chroot</h3>
<p>Add a new build stage in <code>build.sh</code>, between <code>build_gershwin_components</code> and <code>configure_system</code>. Cloned <em>inside</em> the chroot to match gershwin's existing pattern (<code>build.sh:281</code> already clones gershwin-developer the same way).</p>
<pre class="shell"><code>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"
}</code></pre>
<p>Wired into <code>main</code> at <code>build.sh:441</code>:</p>
<pre class="shell"><code>build_gershwin_components
build_launchd # NEW
configure_system</code></pre>
<h3>2.2 Swap to Option D + launchd PID 1</h3>
<p>Phase 1 already moved <code>init.sh</code> to cdroot top-level, restructured to the <code>/sysroot</code> unionfs model, and delivered the live-mount cascade body. Phase 2 only needs to change <em>how the kernel exec's <code>init.sh</code></em> (init_script kenv → Option D shebang) and <em>what <code>init.sh</code> does at the end</em> (set <code>init_chroot</code> kenv → exec launchd in the chroot).</p>
<p>Concrete migration:</p>
<ol>
<li><strong>Update <code>loader.conf</code>:</strong> remove <code>init_script="/init.sh"</code> and <code>init_shell="/rescue/sh"</code>; add <code>init_path="/init.sh"</code>. <em>(The <code>#!/rescue/sh</code> shebang on <code>init.sh</code> is already there from Phase 1.)</em> With Option D, the kernel's <code>imgact_shell</code> resolves the shebang and exec's <code>/init.sh</code> as PID 1 directly — no <code>/sbin/init</code>, no <code>init_chroot</code> kenv path.</li>
<li><strong>Update <code>init.sh</code> tail:</strong> replace
<pre class="shell"><code>kenv init_chroot=/sysroot
kenv -u init_script
kenv -u init_shell
exit 0</code></pre>
with
<pre class="shell"><code>exec chroot /sysroot /sbin/launchd</code></pre>
The cascade body above (mdconfig + ufs + tmpfs + unionfs + devfs + gershwin live tweaks) stays unchanged.</li>
<li><strong>Delete the rcorder surgery</strong> from <code>init.sh</code>. launchd reads its own dependency graph from plist <code>RunAtLoad</code>/<code>KeepAlive</code>/socket-activation keys; it doesn't care about <code># REQUIRE:</code> lines in <code>/etc/rc.d/*</code>.</li>
</ol>
<p>Reference: freebsd-launchd's <a href="freebsd-launchd-plan.html">plan §boot</a> walks through the Option D mechanics. The diff against Phase 1's <code>init.sh</code> is small (delete the rcorder block, swap the last 4 lines for one).</p>
<h3>2.3 LaunchDaemon plist set <span class="pill info">narrowed</span></h3>
<p>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.</p>
<table>
<thead>
<tr><th>Plist</th><th>Source</th><th>Notes</th></tr>
</thead>
<tbody>
<tr>
<td><code>org.freebsd.varrun</code><br><code>org.freebsd.syslogd</code><br><code>org.freebsd.cron</code><br><code>org.freebsd.getty</code><br><code>org.freebsd.dhcpcd</code><br><code>org.freebsd.kmodloader</code></td>
<td>freebsd-launchd</td>
<td>Bedrock plists; install via <code>gmake install</code> in Phase 2's <code>build_launchd</code> stage. Already authored.</td>
</tr>
<tr>
<td><code>org.gershwin.dshelper</code></td>
<td>NEW (gershwin-specific)</td>
<td>Replaces <code>dshelper_enable=YES</code>. Authored against the rc.d source for dshelper (user will share when we're authoring).</td>
</tr>
<tr>
<td><code>org.gershwin.loginwindow</code></td>
<td>NEW (gershwin-specific)</td>
<td>Replaces <code>loginwindow_enable=YES</code>. Authored against the rc.d source for loginwindow.</td>
</tr>
</tbody>
</table>
<p><strong>Everything else in <code>configure_system</code> stays as-is.</strong> 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 <code>/sbin/init</code>, no <code>/etc/rc</code>, and so the <code>_enable=YES</code> lines for those services don't fire — they sit harmlessly in <code>rc.conf</code>. Gershwin boots to login with just the bedrock + dshelper + loginwindow + getty + dhcpcd; the desktop reaches a usable login screen without dbus or the rest.</p>
<p><strong>Phase 3 follow-ups</strong> (one at a time, each on user signal):</p>
<ul>
<li><code>org.freedesktop.dbus.plist</code> — likely first, enables IPC for desktop components. User flagged this 2026-05-07.</li>
<li>cupsd, avahi, ntpd, smartd — added when each earns its keep.</li>
<li>moused, webcamd — need device-event triggering (Phase 3+'s kmodloader / configd surface).</li>
<li>dsbdriverd, initgfx — overlap with kmodloader; revisit after kmodloader is integrated.</li>
</ul>
<h3>2.4 configure_system rc.conf cleanup</h3>
<p>Two new lines added to <code>configure_system</code>'s sysrc block, two implicitly removed:</p>
<ul>
<li><strong>Add</strong>: <code>root_rw_mount="NO"</code> 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.</li>
<li><strong>Remove</strong>: <code>dshelper_enable="YES"</code> and <code>loginwindow_enable="YES"</code> — replaced by their plists. Removing prevents rc.d (which isn't running) from being a phantom dependency in future audits.</li>
<li><strong>Leave alone</strong>: every other <code>_enable=YES</code> line. Dead weight under launchd PID 1, but harmless.</li>
</ul>
<h3>2.5 Phase 2 acceptance criteria</h3>
<ul>
<li><span class="pill ok">met</span> launchd is PID 1 — verified by reaching LoginWindow under the launchd path.</li>
<li><span class="pill ok">met</span> Boot-test reaches the gershwin GUI greeter — LoginWindow comes up, Eau theme bridge registers via DO. (Replaces the original "getty <code>login:</code> prompt" criterion — getty isn't shipped under Phase 2's gershwin shape.)</li>
<li><span class="pill ok">met</span> The gershwin plists (<code>org.gershwin.dshelper</code>, <code>org.gershwin.loginwindow</code>, <code>org.gnustep.gdomap</code>) load — dshelper PID 103 confirmed running in CI run 25504336947.</li>
<li><span class="pill ok">met</span> <code>dhcpcd</code> leases an IP on the test interface (em0: leased 10.0.2.15 from QEMU's NAT).</li>
<li><span class="pill warn">deferred</span> Lenovo hardware test. Architecture is structurally validated under QEMU; hardware boot to LoginWindow before final ship.</li>
<li><span class="pill ok">met</span> QEMU boot-test in CI gates releases.</li>
</ul>
<p><strong>Out of scope for Phase 2:</strong> 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.</p>
<p><strong>Known gap — single-user mode (deferred):</strong> <code>boot -s</code> at the loader sets <code>RB_SINGLE</code> in <code>kern.boothowto</code>, but our <code>init.sh</code> 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 <code>init.sh</code> after the cascade — if <code>boothowto & 0x10</code>, <code>exec chroot /sysroot /bin/sh -i</code>; else <code>exec chroot /sysroot /sbin/launchd</code>. 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."</p>
<h2 id="phase3">Phase 3: polish (deferred) <span class="pill info">post-launchd</span></h2>
<p>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:</p>
<table>
<thead><tr><th>Component</th><th>Replaces</th><th>Trigger to land</th></tr></thead>
<tbody>
<tr><td><a href="freebsd-kmodloader-plan.html">kmodloader</a></td><td><code>initgfx</code> + <code>dsbdriverd</code> + manual <code>kld_list=</code> entries</td><td>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.</td></tr>
<tr><td><a href="nextbsd-configd-plan.html">netconfigd</a> stub</td><td>Nothing yet — it's foundational</td><td>When the desktop needs a programmable view of network state (Network preference pane, WiFi switcher). Phase 1 stub from freebsd-launchd ships as-is.</td></tr>
<tr><td><a href="freebsd-asl-plan.html">ASL</a></td><td><code>FreeBSD-syslogd</code></td><td>When console-noise filtering becomes worth months of work. Today: dhclient and dhcpcd-style spam goes to <code>/var/log/messages</code>; ASL's structured filtering would cleanly suppress per-Sender at the daemon side. Major port; defer.</td></tr>
<tr><td><a href="freebsd-mdnsresponder-plan.html">mDNSResponder</a></td><td>Avahi</td><td>When .local resolution against Avahi is unreliable. Apple's mDNSPosix layer is portable; the swap is mostly configuration. Could be small.</td></tr>
<tr><td><a href="freebsd-notifyd-plan.html">notifyd</a></td><td>Nothing yet</td><td>When a desktop component wants Apple's lightweight pub/sub event bus.</td></tr>
<tr><td><a href="freebsd-disk-arbitration-plan.html">DiskArbitration</a></td><td>Workspace File Viewer's ad-hoc kqueue polling</td><td>When the Devices sidebar needs proper attach/detach events.</td></tr>
</tbody>
</table>
<h2 id="cleanup">Post-launchd cleanup <span class="pill info">deferred</span></h2>
<p>Once Phase 2 lands and gershwin boots reliably with launchd as PID 1 + a working <code>org.gershwin.loginwindow.plist</code>, do this single follow-up commit on <code>pkgdemon/gershwin-on-freebsd:main</code>. End-user UX takes precedence over development diagnostics from this point.</p>
<h3>Revert the Phase 1 boot diagnostics (commit 02e4e9a)</h3>
<ul>
<li><code>resources/overlays/boot/loader.mute.d/loader.conf</code> — uncomment <code>boot_mute="YES"</code> (restore the polished animation).</li>
<li><code>resources/overlays/boot/loader.conf</code> — drop the diagnostics block: <code>boot_verbose="YES"</code>, <code>console="comconsole vidconsole"</code>, <code>comconsole_speed="115200"</code>.</li>
</ul>
<h3>Retire the CI boot-test gate (commit 7caefbf)</h3>
<ul>
<li><code>.github/workflows/build.yml</code> — collapse the build → test → release split back to the original single-job build + publish flow. Drop the <code>test</code> job and its <code>needs:</code> wiring on <code>release</code>.</li>
<li><code>tests/boot-test.sh</code> — delete.</li>
<li><code>tests/</code> — delete the directory if no other tests live there.</li>
</ul>
<p><strong>Why deferred, not skipped:</strong> 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.</p>
<p><strong>What does NOT need reverting:</strong> the <code>exec >/dev/console 2>&1</code> in <code>init.sh</code> (commit 206f8a7). It lives inside the <code>else</code> branch of the <code>boot_mute</code> check — when <code>boot_mute="YES"</code> is restored, the silencing branch fires and the explicit redirect is dead code. Leave it; it gives future debugging cycles visible output for free.</p>
<p><strong>Single commit suggested title:</strong> "post-Phase-2 cleanup: restore quiet boot, retire CI boot-test gate" with reference to commits 02e4e9a + 7caefbf in the body.</p>
<h2 id="loginwindow">LoginWindow plist</h2>
<p>Three things share the name "LoginWindow"; keep them straight.</p>
<table>
<thead><tr><th>Artifact</th><th>What it is</th><th>Phase</th></tr></thead>
<tbody>
<tr>
<td><code>/Local/Library/Preferences/LoginWindow.plist</code></td>
<td>Existing gershwin <em>state file</em>. Tracks last-logged-in user and last session script. Written by <code>init_script</code> today (lines 133-139). Two-key dictionary, no launchd semantics.</td>
<td>Already shipped. Keep as-is.</td>
</tr>
<tr>
<td><code>/System/Library/LaunchDaemons/org.gershwin.loginwindow.plist</code></td>
<td>NEW launchd job to be authored in Phase 2. Replaces the rc.d <code>loginwindow_enable=YES</code>. Starts whatever binary today's gershwin <code>loginwindow</code> rc script starts (likely a SLiM-style greeter or a Gershwin-native equivalent — needs identification before authoring).</td>
<td>Phase 2.</td>
</tr>
<tr>
<td>Apple's <code>loginwindow.app</code></td>
<td>Closed-source macOS daemon. Per the LoginWindow research: <strong>not portable</strong> — 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.</td>
<td>Phase 4+, research only.</td>
</tr>
</tbody>
</table>
<p>Phase 2's <code>org.gershwin.loginwindow.plist</code> is the small, concrete, useful thing. Sketch:</p>
<pre class="plist"><code><?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></code></pre>
<p>Authored once we read the actual rc script in the post-install image — the <code>ProgramArguments</code> path is a guess until then. <code>StandardOutPath</code>/<code>StandardErrorPath</code> are recorded as not-yet-implemented in freebsd-launchd's plist parser; they're written for the eventual implementation, ignored harmlessly today.</p>
<h2 id="files">File-by-file change inventory</h2>
<h3>gershwin-on-freebsd</h3>
<table>
<thead><tr><th>File</th><th>Phase 1</th><th>Phase 2</th></tr></thead>
<tbody>
<tr><td><code>build.sh:281</code></td><td><span class="pill ok">done — 1d2150b</span> <code>--branch feat/libs-corebase</code> added.</td><td>Insert <code>build_launchd</code> stage between <code>build_gershwin_components</code> and <code>configure_system</code>.</td></tr>
<tr><td><code>build.sh:224-257</code> (configure_system rc.conf block)</td><td>Untouched.</td><td>Strip <code>_enable=YES</code> lines for converted services; keep tunables.</td></tr>
<tr><td><code>build.sh:319-389</code> (prepare_boot_env)</td><td><span class="pill ok">done — 3b97e40</span> dropped 30+ mountpoint mkdir; only <code>/sysroot</code>, <code>/upper</code>, <code>/dev</code>; dropped <code>/etc/login.conf</code> workaround; added <code>unionfs.ko</code> to keep list; <code>cp init.sh</code> from overlays top-level.</td><td>Untouched (init.sh tail change is in the overlay, not build.sh).</td></tr>
<tr><td><code>build.sh</code> generate_iso</td><td><span class="pill ok">done — 3b97e40</span> uzip moved from <code>/boot/rootfs.uzip</code> to <code>/rootfs.uzip</code>.</td><td>Untouched.</td></tr>
<tr><td><code>resources/overlays/boot/init_script</code> (old)</td><td><span class="pill ok">done — 3b97e40</span> deleted; replaced by <code>resources/overlays/init.sh</code>.</td><td>—</td></tr>
<tr><td><code>resources/overlays/init.sh</code> (new)</td><td><span class="pill ok">done — 3b97e40</span> single-root unionfs cascade + gershwin live tweaks against <code>/sysroot/…</code> + <code>kenv init_chroot=/sysroot</code> + exit.</td><td>Delete rcorder surgery block; replace last 4 lines (kenv init_chroot + cleanup + exit 0) with single line <code>exec chroot /sysroot /sbin/launchd</code>.</td></tr>
<tr><td><code>resources/overlays/boot/loader.conf</code></td><td><span class="pill ok">done — 3b97e40</span> <code>init_script="/init.sh"</code>, <code>init_shell="/rescue/sh"</code>, <code>unionfs_load="YES"</code>.</td><td>Drop <code>init_script</code> + <code>init_shell</code>; add <code>init_path="/init.sh"</code>.</td></tr>
<tr><td><code>resources/overlays/Local/Library/Preferences/LoginWindow.plist</code></td><td>Untouched.</td><td>Untouched (this is the state file, not the launchd plist).</td></tr>
<tr><td><code>resources/overlays/System/Library/LaunchDaemons/*.plist</code></td><td>—</td><td><strong>NEW directory</strong>; ~15 plists land here over the rollout.</td></tr>
<tr><td><code>resources/packages/gershwin</code></td><td>Untouched.</td><td>Add <code>dhcpcd</code> and <code>wpa_supplicant</code>.</td></tr>
<tr><td><code>resources/packages/base</code></td><td>Untouched.</td><td>Remove <code>FreeBSD-dhclient</code> and <code>FreeBSD-wpa</code>.</td></tr>
<tr><td><code>resources/packages/vital-base</code></td><td>Untouched.</td><td>Remove <code>FreeBSD-dhclient</code> and <code>FreeBSD-wpa</code> if present.</td></tr>
<tr><td><code>resources/packages/{drivers,vital-gershwin}</code></td><td>Untouched.</td><td>Untouched — settled work; not re-audited.</td></tr>
<tr><td><code>tests/boot-test.sh</code></td><td><span class="pill ok">done — 7caefbf</span> qemu+OVMF+expect; multi-marker.</td><td>Add a second grep for <code>launchd: PID 1 ready</code>.</td></tr>
<tr><td><code>.github/workflows/build.yml</code></td><td><span class="pill ok">done — 7caefbf</span> split into build → test → release; release gated by boot-test.</td><td>Untouched (boot-test detects launchd via grep).</td></tr>
</tbody>
</table>
<h3>gershwin-developer</h3>
<table>
<thead><tr><th>File</th><th>Change</th></tr></thead>
<tbody>
<tr><td><em>(none required)</em></td><td>We consume the <code>feat/libs-corebase</code> branch as-is. If the branch needs to merge to main and pick up an upstream PR before we can drop the <code>--branch</code> flag, that's a separate gershwin-developer task.</td></tr>
</tbody>
</table>
<h3>freebsd-launchd</h3>
<table>
<thead><tr><th>File</th><th>Change</th></tr></thead>
<tbody>
<tr><td><code>build.sh</code> (the freebsd-launchd one)</td><td>Untouched. freebsd-launchd remains independently buildable as a standalone livecd. Gershwin-on-freebsd just consumes the launchd binary and plists, not the build.sh.</td></tr>
<tr><td><code>kmodloader/</code>, <code>configd/</code></td><td>Untouched in Phase 2; potentially adopted in Phase 3.</td></tr>
</tbody>
</table>
<h2 id="open">Open questions</h2>
<div class="resolved">
<strong>11.1 — Firmware symlink — resolved (skip).</strong> Phase 1's <code>init_chroot</code> chroot operates against a real mount stack (uzip + tmpfs + unionfs at <code>/sysroot</code>); 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.
</div>
<div class="open-q">
<strong>11.2 — Identity of the existing loginwindow binary.</strong> The <code>loginwindow_enable=YES</code> sysrc points at an rc.d script in <code>/usr/local/etc/rc.d/loginwindow</code> that lands during <code>install_gershwin_software</code>. Need to read that script after a fresh build to identify the <code>command_args</code> + executable path — drives the Phase 2 launchd plist's <code>ProgramArguments</code>. Likely shipped by one of the <code>gershwin-*</code> packages (gershwin-system or gershwin-workspace).
</div>
<div class="open-q">
<strong>11.3 — DSBSD <code>dsbdriverd</code> overlap with kmodloader.</strong> 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.
</div>
<div class="open-q">
<strong>11.4 — initgfx vs kmodloader for GPU.</strong> Same pattern as 11.3. initgfx (GhostBSD) configures Xorg too, not just KLD load. kmodloader only loads kmods. If we keep Xorg config in initgfx and let kmodloader handle bind, both can coexist. Or rip out initgfx entirely and have a separate gershwin component own Xorg config.
</div>
<div class="open-q">
<strong>11.5 — pkg-base fingerprints across versions.</strong> Per the pkg-base research, the keys live in <code>/usr/share/keys/pkgbase-15</code> — version-suffixed. When gershwin-on-freebsd bumps from 15 to 16, this path moves. <code>build.sh</code> currently doesn't reference it (host's pkg knows; the chroot inherits the repo conf). Worth a comment somewhere, no immediate change.
</div>
<div class="open-q">
<strong>11.6 — Boot-test on real hardware in CI.</strong> QEMU boot-test catches a lot but not everything (firmware loading on the Lenovo wouldn't have been caught in QEMU). Worth investigating a real-hardware boot test runner — overkill for now, log it as a future improvement.
</div>
<h2>Decisions log</h2>
<ul>
<li><strong>Sequencing:</strong> livecd first, launchd second. Per user direction 2026-05-06.</li>
<li><strong>Packages:</strong> gershwin-on-freebsd's existing lists are the source of truth; no additions. Per user direction 2026-05-06.</li>
<li><strong>gershwin-developer branch:</strong> <code>feat/libs-corebase</code>. Per user direction 2026-05-06.</li>
<li><strong>freebsd-launchd source location:</strong> <code>/Developer/Library/Sources/freebsd-launchd/</code>, matching gershwin-developer's <code>$SCRIPT_DIR/../Sources</code> convention. Per user direction 2026-05-06.</li>
<li><strong>LoginWindow:</strong> launchd plist in Phase 2; no port of Apple's loginwindow.app. Research-confirmed unportable as of 2026-05-06.</li>
<li><strong>dhclient vs dhcpcd:</strong> Phase 2 adds <code>dhcpcd</code> from ports and removes <code>FreeBSD-dhclient</code> from the base list in the same commit. Lifts freebsd-launchd's existing <code>org.freebsd.dhcpcd.plist</code> verbatim. Per user direction 2026-05-06.</li>
<li><strong>wpa_supplicant:</strong> Phase 2 adds <code>wpa_supplicant</code> from ports and removes <code>FreeBSD-wpa</code> from the base list in the same commit. Per user direction 2026-05-06.</li>
<li><strong>No other package edits:</strong> the four-line dhcpcd / wpa_supplicant swap is the entire scope of package changes. Existing <code>resources/packages/{base,vital-base,gershwin,drivers,vital-gershwin}</code> 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.</li>
<li><strong>Phase 1 underscoping correction:</strong> the original v0 plan called §1.3 "tighten init_script (cosmetic)" and parked the architectural rework in Phase 2. After conversation 2026-05-06 with the user noting that gershwin still does per-subdir nullfs + cp -R, scope was corrected — Phase 1 owns the architectural rework to single-root unionfs + <code>init_chroot</code> kenv pivot (mirroring freebsd-livecd-unionfs). Phase 2 then becomes a single-line tail change to <code>init.sh</code> + loader.conf knob swap.</li>
<li><strong>Mechanism for Phase 1 vs Phase 2 pivot:</strong> Phase 1 uses <code>init_chroot</code> kenv (read by stock <code>/sbin/init</code> after <code>init_script</code> exits). Phase 2 swaps to Option D shebang (kernel exec's <code>/init.sh</code> directly as PID 1, replacing init entirely). Both are documented mechanisms in FreeBSD; Option D is freebsd-launchd-specific because launchd replaces <code>/sbin/init</code>. Per agent research 2026-05-06.</li>
<li><strong>Firmware symlink:</strong> not adopted in Phase 1. Gershwin's <code>init_chroot</code> chroot operates against a real mount tree, so kernel-namei resolves <code>/sysroot/boot/firmware</code> 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.</li>
<li><strong>init_script cosmetic tightening:</strong> not done. Risk/value didn't justify it for a load-bearing shell script. The full architectural rework subsumed any "cleanup" intent. Decision 2026-05-06.</li>
<li><strong>chroot stays git-free — exception for gershwin integration:</strong> the freebsd-launchd standalone repo follows the rule (host-side clone + rsync into chroot). gershwin-on-freebsd's existing pattern is the opposite — <code>build.sh:281</code> clones gershwin-developer inside the chroot, and Checkout.sh (running in chroot) clones each upstream into <code>/Developer/Library/Sources/</code>. Phase 2's <code>build_launchd</code> follows gershwin's pattern (<code>chroot git clone</code>) to keep one consistent integration shape. Per user direction 2026-05-07.</li>
<li><strong>Fork-only workflow:</strong> all gershwin work lands in <code>pkgdemon/gershwin-on-freebsd</code> (the user's fork) directly on main; no PRs to gershwin-desktop, no topic branches. Per user direction 2026-05-06.</li>
<li><strong>Post-rework debugging trail (2026-05-07):</strong>
<ul>
<li>Initial CI on the rework looked silent on serial after kernel handoff (only kernel printf messages visible). Spent several iterations chasing this.</li>
<li>Two coupled root causes identified by parallel agents reading <code>init.c</code> + diff'ing freebsd-livecd-unionfs:
(a) <code>/sbin/init</code> mounts devfs <em>after</em> running <code>init_script</code>, so init's <code>open_console()</code> for the child's stdio falls back to <code>/dev/null</code>. Fix: <code>init.sh</code> mounts devfs first, then re-execs stdio onto <code>/dev/console</code>.
(b) <code>boot_serial="YES"</code> was missing in loader.conf — userspace stdio routed to vidconsole (invisible under QEMU <code>-display none</code>). Working repo had it; we didn't.
</li>
<li>Once visibility was restored, the actual blocker surfaced: <code>/etc/rc.d/root</code> aborts the boot trying <code>mount -uw /</code> (mount(8) inspects the global kernel mount table, which still reports / as cd9660 even after the chroot). Fix: <code>root_rw_mount="NO"</code> in <code>/sysroot/etc/rc.conf</code>, same as freebsd-livecd-unionfs.</li>
<li>Earlier "green" CI runs were 1m5s false positives — boot-test was matching the FreeBSD loader's beastie banner ("Welcome to FreeBSD") before the kernel even started. Tightened markers to multi-user-only signals: <code>login:</code>, <code>Starting local daemons</code>, <code>Setting hostname</code>, <code>Mounting local filesystems</code>.</li>
</ul>
</li>
</ul>
<p class="footnote">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.</p>
</body>
</html>