NextBSD · Driver delivery · build model

One build system, three targets, three tiers

How NextBSD produces the compiled-in kernel, the loadable klds, and the kexts from one tree without ever patching a stock FreeBSD build file. buildkernel NO_MODULES makes tier 1, buildextensions makes tiers 2–3 from in-tree modules, buildgraphics makes the out-of-tree GPU/nvidia kexts, and buildworld is untouched.

Drafted 2026-07-17 from the build-system research (grounded in the actual freebsd-src Makefile.inc1 / kern.post.mk / kmod.mk) and the naming manifest. Provisional — sub of the driver-delivery sidebar. One load-bearing decision (Pure vs Mixed) is still open. Planning only, no code written.

The target map

TargetOwnerProducesTier
make buildkernel NO_MODULES=yesstock (flag only)kernel image, no modules1 · compiled-in
make buildextensionsNextBSDin-tree sys/modules → plain klds + firmware kexts2 + 3
make buildgraphicsNextBSDout-of-tree drm-kmod + nvidia → custom-named kexts3 (GPU)
make buildworldstock (untouched)userland

Naming style tracks the target: buildextensions emits FreeBSD-name kexts (if_iwlwifi.kext, mechanical); buildgraphics emits the 12 custom names (IntelGraphics.kext, NVIDIAGraphics<NNN>). See the naming manifest — it's the KEXTS table these targets read.

make buildkernel NO_MODULES=yes ─▶ /boot/kernel/kernel (tier 1: compiled-in) │ leaves configured obj tree (opt_*.h, machine/, kernel-toolchain) ▼ make buildextensions ───────────▶ sys/modules → plain klds ─┐ (tier 2) │ (needs kernel config output) + kext-wrap ─┼─▶ /System/Library/Extensions make buildgraphics ─────────────▶ drm-kmod + nvidia (fetch) ─┘ (tier 3) └ shared bsd.kext.mk wrap ──▶ + firmware in Resources/ make buildworld ────────────────▶ userland (independent, any order)

1buildkernel NO_MODULES — tier 1, stays stock

Stock make buildkernel's all target is kernel-all + modules-all (in sys/conf/kern.post.mk). Passing NO_MODULES=yes (a flag — not a Makefile edit) drops the modules half, leaving just the kernel image. installkernel NO_MODULES=yes installs only the kernel to /boot/kernel.

make buildkernel   KERNCONF=NEXTBSD NO_MODULES=yes
make installkernel KERNCONF=NEXTBSD NO_MODULES=yes DESTDIR=${STAGE}

Crucially this leaves behind the configured kernel obj tree — the config stage's opt_*.h, the machine/ symlinks, and the kernel-toolchain — which the next target needs. Because it's just a flag on a stock target, buildkernel stays byte-for-byte upstream (keeps the monorepo's merge-clean rule intact).

The one deliberate divergence With modules moved out, stock buildkernel && installkernel no longer yields a bootable system on its own — your compiled-in tier must genuinely cover boot-to-mountroot (root HBA, root FS, console, core buses), since the loadable tier now comes from a separate target. Document this so nobody's surprised.

2buildextensions — in-tree klds + kexts (tiers 2–3)

A NextBSD-owned target that builds sys/modules the way buildkernel used to, then wraps the kext-tier drivers. It emits a mix: the ~250 plain klds stay plain .ko, and the ~30 firmware drivers get wrapped into .kext bundles via a shared bsd.kext.mk.

# nextbsd/extensions/Makefile — invoked AFTER buildkernel (needs its obj tree)
KODIR?= ${OBJTOP}/${MACHINE}.${MACHINE_ARCH}/${SRCTOP}/${KERNCONF}/modules${SRCTOP}/sys/modules
.include "${SRCTOP}/nextbsd/share/mk/nextbsd.kexts.conf"   # the KEXTS table = the naming manifest

buildextensions: .PHONY
	cd ${SRCTOP}/sys/modules && ${MKMODULESENV} ${MAKE} all   # build every .ko (tier 2)
.for k in ${KEXTS}                                            # wrap only the firmware/multi-version ones (tier 3)
	@${MAKE} -f ${SRCTOP}/nextbsd/share/mk/bsd.kext.mk extension \
	    KEXT_NAME=${${k}_NAME} KEXT_ID=${${k}_ID} KO=${KODIR}/${${k}_KO} \
	    KEXT_PERSGEN=${SRCTOP}/nextbsd/tools/gen-${${k}_GEN}-personalities.sh \
	    KEXT_FW="${${k}_FW}" KEXT_DEPS="${${k}_DEPS}"
.endfor

bsd.kext.mk is the formalized ko2kext.sh: it takes an already-built .ko, runs the personality generator, bundles firmware into Resources/, writes Info.plist + OSBundleLibraries, and stages <Name>.kext/Contents/MacOS/<ko>. Move ko2kext.sh + gen-*-personalities.sh from the separate nextbsd-kernel-modules repo into nextbsd/tools/.

3buildgraphics — out-of-tree GPU/nvidia (tier 3)

drm-kmod (i915kms/amdgpu/radeonkms + the drm/ttm/dmabuf cores) and nvidia are ports, not sys/modules, so buildextensions can't reach them. They already need bespoke recipes (fetch, patch, per-branch), which is exactly why they get their own target — and their custom names cost nothing extra.

Build-time fetch does NOT violate base-never-requires-pkg That constraint is about the target at runtime, not the builder. Fetching nvidia/drm-kmod source on the build host and baking the resulting kexts into the image leaves the shipped system with zero pkg dependency — it just has baked .kext bundles. So fetching here is squarely allowed.

4buildworld — unchanged

Userland only; it never touched kernel modules. Completely stock, runs in any order relative to the kernel/extensions targets. Your custom userland tools ride in via the LOCAL_DIRS/LOCAL_LIB_DIRS hooks (see the monorepo plan §5), not here.

5The one open decision: Pure vs Mixed install layout

The build split above is compatible with both. What differs is where the loadable drivers install and who matches them — and it hinges on whether /boot/kernel holds loose klds.

Mixed (sidebar recommendation)Pure (clean-namespace)
/boot/kernelkernel + ~250 plain kldskernel image only
/System/Library/Extensions~38 kextsall ~343 as kexts
Matching for the ~250native devmatch (free, authoritative)your in-kernel matcher (must scale to all HW)
Personalities to generate~38343
/boot/firmwareavoided if kexts serve fw from Resources/ (D4)avoided (same)
Cost/boot/kernel has klds; least bespoke machineryuniform bundles, no loose files; but no native devmatch, matcher owns everything
Why you can't have both "empty /boot/kernel" and "native devmatch" kern.module_path defaults to /boot/kernel;/boot/modules, and kldxref builds linker.hints from those dirs — that's exactly where devmatch looks. Kexts in /System/Library/Extensions are bundle directories (the .ko is at Contents/MacOS/), which kldxref doesn't traverse — so they load via your loader/matcher, not devmatch. Keeping /boot/kernel empty of klds therefore forces everything through the kext path (Pure), which re-raises the 343-wide personality burden and makes your T420-verified matcher load-bearing for all hardware. Mixed keeps the matcher scoped to ~38 and lets devmatch carry the long tail. This is the decision to make; the build targets serve whichever you pick.

6Alternatives — fewer targets for local building

Four ordered targets with flags is great for CI (selective builds need the granularity) but heavy for a developer who just wants "build me a working system." The fix isn't fewer capabilities — it's a friendlier front door. Three options, not mutually exclusive.

OptionLocal experienceTrade-off
A · Umbrella + build.sh cheapestkeep the granular targets for CI; add a make nextbsd meta-target that chains kernel→extensions→graphics→world in order, and/or a build.sh (your existing idiom) with sane defaults. Local = one command.~none. Pure convenience layer.
B · Collapse graphics into extensions3 targets: buildkernel, buildextensions (in-tree and out-of-tree via internal sub-makes), buildworld.slightly coarser CI gating for GPU (recoverable with internal ifs).
C · Local stays stock; kexts are release-only best local ergonomicsdon't use NO_MODULES locally — plain make buildkernel buildworld builds a normal, bootable FreeBSD-shaped system (klds in /boot/kernel). The kext-wrap runs only in the release/image pipeline. Day-to-day kernel hacking is just stock FreeBSD.the kext path is exercised at release/CI, not in every local build. Pairs with the Mixed model.
The insight: the heavy target graph is mostly a Pure-model tax NO_MODULES + the strict tier split exists to keep /boot/kernel empty — which is the Pure goal (§5). Mixed doesn't need NO_MODULES at all: stock buildkernel builds kernel + klds (self-sufficient, bootable at every step), and a single make extensions/buildgraphics wrap handles just the ~38 kexts on top. So if local ergonomics matter, Mixed + Option C gives the simplest possible local story — local building is plain FreeBSD — while CI still does the selective/kext work. Pure is what forces the four-target dance. Another entry in Mixed's column.

Recommendation: granular targets for CI + a make nextbsd umbrella and build.sh for local (A); and if you adopt Mixed, Option C makes routine local builds just stock FreeBSD. For iterating on a single driver you don't need any of this — cd sys/modules/foo && make already works after one full build.

7Ordering, incremental, and selective CI

8Keep upstream clean — exact touch list

Everything that enters the tree is a NextBSD-owned new file; no stock build file is edited.

ConcernMechanism (no stock edit)
No modules from buildkernelNO_MODULES=yes — a flag
NextBSD-original kldsLOCAL_MODULES / LOCAL_MODULES_DIRnextbsd/modules/
Per-driver one-shot kext (NextBSD drivers)driver Makefile .include <bsd.kext.mk>; or the stock bsd.kmod.mk's existing .-include <local.kmod.mk> hook
The extensions/graphics targetsnew top-level nextbsd/ dir (extensions/, graphics/, share/mk/bsd.kext.mk, tools/), invoked from the release/CI driver
Kernel confignew file sys/${ARCH}/conf/NEXTBSD that include GENERIC — never edit GENERIC

Stock Makefile.inc1, kern.post.mk, kmod.mk, bsd.kmod.mk, sys/modules/Makefile, and GENERIC stay untouched — so upstream merges never conflict on the build system.

9Open decisions