NextBSD · Monorepo · Feasibility & setup
Should the ~10 nextbsd-redux component repos collapse into one pkgdemon/nextbsd tree that starts as fresh FreeBSD main with your changes committed on top, tracks upstream by merge-via-PR, integrates the userland into buildworld, and drives selective kernel/world/pkg/ISO builds from one CI system? Yes — and every piece is proven precedent. This is the feasibility verdict, the phased setup, and the honest caveats.
make release targets (§11) and pkgbase package naming (§6) are also still open. See §12 · Open decisions for the live list of things to discuss next.The monorepo is feasible, well-precedented, and a genuine upgrade over the current 10-repo repository_dispatch cascade — with two hard constraints to design around from day one.
freebsd/freebsd-src main as a plain git remote and merge it into your main (never rebase, never patch files, never an in-network fork). This is exactly HardenedBSD's proven model. Seed from a squashed single-branch snapshot (not FreeBSD's multi-GB history) → repo stays ~1–1.5 GB. First merge needs --allow-unrelated-histories; all later ones are cheap. A scheduled Action opens the sync PR; you resolve conflicts by hand.LOCAL_DIRS / LOCAL_LIB_DIRS / LOCAL_MODULES hooks (set in src.conf) are the sanctioned way to add your tools to world without patching base. Good news from recon: most of nextbsd-userland is already bsdmake — only libdispatch and swift-foundation-icu are CMake — so "drop cmake" is a small, targeted job, not a rewrite.syslogd+syslog(3); satisfy any asl_* callers with a ~few-hundred-line libasl write-shim (the mirror image of Darwin's own syslog→ASL adapter). Drop the ASL binary store/query side — your GNUstep layer never touched it..kext binaries on a FreeBSD kernel is permanently infeasible (ABI, C++/OSMetaClass, versioned KPI, signing) — even the original NextBSD skipped IOKit. Go kld-primary; reserve a source-level IOKit-compat shim for the few subsystems that reuse IOKit-shaped Darwin source (GPU is the one real candidate). Mach core is in-tree sys/ patches, not a module.filemon(4) does not exist → -DNO_FILEMON is mandatory and meta-mode incrementality degrades to command-line detection; ccache/sccache must compensate. Real ISOs / make release need a FreeBSD host. This maps cleanly to your "cross-build continuously on Linux, cut real releases on a NextBSD box" plan.Grounding the plan in the recon agent's reading of your live checkouts and the nextbsd-redux / pkgdemon orgs — including two facts that change the plan.
The current architecture is ~10 repos wired into a cascading repository_dispatch pipeline. freebsd-src (a publish-only mirror fork, ~3.36 GB) syncs daily on releng/15.1, which fans out to a cross-toolchain build, then kernel → kernel-modules on one path and freebsd-compat (buildworld base) → userland → pkg → ISO on the other. Each repo carries its own .github/workflows; PR runs are build-only and non-cascading.
nextbsd-userland/src/, only libdispatch and swift-foundation-icu use CMake (the rest use .include <bsd.prog.mk> driven by a make.py buildenv). "Drop cmake" is therefore a 2-component job plus the top-level driver — not the tree-wide conversion the framing implied.libmach, mach_kmod, bootstrap_cmds/MIG), IOKit (libIOKit, kext_tools, plus ko2kext.sh/personalities in nextbsd-kernel-modules), XPC/configd, and Apple's actual ASL (src/syslog/ = libsystem_asl.tproj, syslogd.tproj, aslmanager, with APPLE_LICENSE). So §4 and §6 are about relocating and re-deciding existing code, not greenfield work.~/gershwin-developer/Library/Sources/ does not exist on this host. Live checkouts are directly under ~/ (~/nextbsd, ~/nextbsd-userland, ~/nextbsd-freebsd-compat, ~/gershwin-on-nextbsd) and under ~/Documents/ (nextbsd-kernel, -kernel-modules, -pkg, -ci, and a full launchd/ working area). The same repos are cloned in multiple places today — a mess the monorepo directly fixes.The through-line: the monorepo isn't inventing your dependency graph — it's re-encoding the cascade you already hand-wired across repos as ordinary in-tree build ordering plus (optionally) BuildKit stage edges, where cache invalidation is computed from content digests instead of repository_dispatch plumbing.
Your instinct ("changes as code on the tree, sync often via a PR, not patch files, not an in-network fork") is correct. Here is the exact model and the evidence.
| Model | How | Fit |
|---|---|---|
| Merge from upstream remote | Add freebsd remote; git merge freebsd/main into your main periodically. Your commits + upstream coexist in one DAG. | Best Conflicts only in files both sides touched. Merge commits timestamp each sync. rerere replays past resolutions. This is HardenedBSD's model. |
| Rebase your patches | Keep a linear series, rebase onto each new upstream. | No Rewrites your hashes every sync → breaks clones, open branches, PR continuity; re-resolve the same conflict on every commit, every sync. |
| Vendor branch | Pristine upstream on vendor/freebsd, merge into main. | Redundant Identical to the remote model — an upstream remote's main is your vendor branch. Skip the ceremony. |
| git subtree | Embed freebsd-src under a prefix. | Wrong tool Subtree is for upstream living in a subdir. Here freebsd-src is the repo root. |
pkgdemon/nextbsdConcrete steps: create the empty repo, seed it from a snapshot of FreeBSD main, wire the upstream remote, and land the sync-via-PR workflow. Nothing here touches your existing repos — it's additive and reversible.
Importing FreeBSD's full history (~2.6 GB) would put you near GitHub's 5 GB discomfort line before your first commit and tax every CI clone forever. Take a snapshot instead:
# 1. Create the empty repo first (gh or the web UI):
gh repo create pkgdemon/nextbsd --private --description "NextBSD monorepo"
# 2. Grab main only, shallow, into a scratch dir
git clone --branch main --single-branch --depth 1 \
https://github.com/freebsd/freebsd-src.git seed
cd seed
# 3. Re-root history at this snapshot: first commit = "Import FreeBSD main @HASH"
UP=$(git rev-parse HEAD)
rm -rf .git
git init -b main
git add -A
git commit -m "Import FreeBSD-src main @ ${UP:0:12}"
# 4. Push to your monorepo
git remote add origin git@github.com:pkgdemon/nextbsd.git
git push -u origin main
# 5. Add upstream for future syncs
git remote add freebsd https://github.com/freebsd/freebsd-src.git
git config remote.freebsd.tagOpt --no-tags
git config rerere.enabled true # remember conflict resolutions
git fetch freebsd main
Working-tree size lands ~1–1.5 GB — comfortably inside GitHub's recommended range and fast to clone in CI. It grows only as you add code plus the deltas of upstream commits you actually merge.
freebsd/main, your first sync needs git merge --allow-unrelated-histories freebsd/main (the same step HardenedBSD used at migration). That first merge stitches the histories; every subsequent git merge freebsd/main is an ordinary cheap incremental merge.peter-evans/create-pull-request "keep a fork updated" example runs git reset --hard upstream/main, which deletes all your NextBSD commits and replaces the tree with pristine upstream — the exact opposite of what you want. Use the git CLI + gh pr create, which preserves history and lands real conflict markers in the PR branch for you to resolve.# .github/workflows/upstream-sync.yml
name: Upstream FreeBSD sync
on:
schedule: [{ cron: '0 6 * * 1' }] # Mondays 06:00 UTC
workflow_dispatch: {} # the "click to sync" button you asked for
permissions: { contents: write, pull-requests: write }
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Merge upstream (allow conflicts)
id: merge
run: |
git config user.name "nextbsd-sync[bot]"
git config user.email "sync@nextbsd.invalid"
git remote add freebsd https://github.com/freebsd/freebsd-src.git
git fetch --no-tags freebsd main
BR="sync/$(date +%Y-%m-%d)"; echo "branch=$BR" >>"$GITHUB_OUTPUT"
git switch -c "$BR"
if git merge --no-ff --no-edit freebsd/main; then
echo "conflicts=false" >>"$GITHUB_OUTPUT"
else
git add -A
git commit --no-edit -m "MERGE CONFLICTS - resolve before merging"
echo "conflicts=true" >>"$GITHUB_OUTPUT"
fi
git push -u origin "$BR"
- name: Open PR
env: { GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
run: |
gh pr create --base main --head "${{ steps.merge.outputs.branch }}" \
--title "Sync upstream FreeBSD main ($(date +%F))" \
--body "Automated merge. Conflicts: ${{ steps.merge.outputs.conflicts }}. Merge with a MERGE COMMIT — never squash."
main — history is append-only; that's the whole point of choosing merge over rebase..github/workflows/**, the default GITHUB_TOKEN can't push it — use a PAT with workflow scope.rerere is on) after checking out the sync branch — CI can't easily share the rr-cache.Clean upstream merges and obvious "what's ours" are the same goal, served by one rule.
| What | Where | Mechanism |
|---|---|---|
| Your libraries (libmach, CF, libasl…) | nextbsd/lib/* or lib/libnext* | bsd.lib.mk + LOCAL_LIB_DIRS |
| Your tools & daemons | nextbsd/usr.bin/*, usr.sbin/*, libexec/* | bsd.prog.mk + LOCAL_DIRS |
| Your kernel modules (kld) | sys/modules/next* or sys/dev/next* | bsd.kmod.mk + LOCAL_MODULES |
| Net-new kernel subsystems | sys/next/ | new dir wired via minimal SUBDIR += edit |
| Kernel config | sys/*/conf/NEXTBSD | new file — never edit GENERIC |
| Mach core (task/thread/trap patches) | in-place in sys/kern, sys/sys | /* NEXTBSD-BEGIN … END */ markers (see §4) |
When you must modify an upstream file in place, wrap it: /* NEXTBSD-BEGIN: reason */ … /* NEXTBSD-END */. Then grep -rn NEXTBSD enumerates your entire in-place footprint — your ownership map and conflict-risk register in one command. Wire new subdirs in with the smallest possible edit (one line in a SUBDIR list). Never reformat or relocate upstream files "while you're in there" — churn manufactures conflicts against every future upstream commit.
Ownership becomes trivially answerable: under a next* path → ours; grep NEXTBSD hits it → ours-modified-upstream; otherwise → pristine upstream.
"Integrate mach & unionfs patches as code in the branch, not as patch files." Here's what becomes an in-tree patch vs a loadable module — and the verdict on kexts.
.kext binaries on a FreeBSD kernel is permanently infeasible — barred independently by ABI, the C++/OSMetaClass runtime, versioned OSBundleLibraries KPI linkage, Mach/IOKit-internal symbol expectations, and code signing. No project ever did it; the original NextBSD added Mach IPC but deliberately omitted IOKit/kext loading, and Darling's IOKit is userspace-only. Treat "kext" as a programming model you re-host in source, never a binary format you load.| Component | In-tree sys/ patch | Loadable kld |
|---|---|---|
Mach core — mach_msg/trap path, struct proc/struct thread extensions for tasks/threads/port-right namespaces, VM hooks for memory objects | Yes — required NextBSD implemented tasks/threads as extensions to existing process/thread structs. This is where your "mach as code" genuinely becomes tree modifications. | — |
Mach glue/service — /dev/mach device, bootstrap/host-port surface, IPC routing that doesn't touch task/thread layout | — | Yes This is essentially what Darling's LKM does via /dev/mach ioctls. |
| unionfs and all filesystems | — | Yes — preferred VFS is a mature C kld surface; unionfs belongs as a normal sys/fs kld. IOKit adds nothing. |
| Device drivers (net/storage/HID/audio) | — | Yes newbus DRIVER_MODULE, native FreeBSD. |
A source-compatible IOKit framework (vendored libkern/OSObject/IOService/IORegistry compiled into .kos) is feasible but expensive — a multi-engineer-quarter effort per family, buying source compatibility, never binary. Rule of thumb: emulate IOKit only where you're reusing IOKit-shaped Darwin source. Filesystems, Mach glue, and most drivers should be native kld. GPU/graphics is the one place IOKit source reuse (IOGraphicsFamily/accelerators) can genuinely pay off; otherwise use drm-kmod. HID/audio: native evdev/sound(4) is usually less work than IOHID/IOAudio.
Document loading Apple binary kexts as an explicit non-goal. Your existing ko2kext.sh + personality generators produce kext packaging/metadata around FreeBSD .kos — that's the pragmatic middle ground and stays; it is not the same as running Apple's binaries.
buildworld — and dropping cmakeThe sanctioned hooks, the CMake→bsd.mk translation, and the decision rule for "in world vs a package."
LOCAL_* hooks (the official answer)You do not patch base tree Makefiles. build(7) ships three hooks; set them once in /etc/src.conf and they apply to every buildworld/installworld/buildenv/cross-build/make release:
# /etc/src.conf
LOCAL_LIB_DIRS= nextbsd/lib/libCoreFoundation nextbsd/lib/libmach nextbsd/lib/libasl
LOCAL_DIRS= nextbsd/usr.sbin/launchd nextbsd/usr.sbin/configd nextbsd/usr.bin/...
LOCAL_MODULES= nextunionfs nextmach # dirs under sys/modules
LOCAL_LIB_DIRS builds during the libraries phase (early), so your programs can link them.LOCAL_DIRS builds during the everything phase.LOCAL_MODULES builds/installs as part of buildkernel/installkernel./usr/src/local auto-discovery — the mechanism is explicit. Keep everything under one nextbsd/ subtree pointed at by the hooks.| CMake | bsd.mk |
|---|---|
add_executable(foo a.c b.c) | PROG=foo / SRCS=a.c b.c |
add_library(foo SHARED …) | LIB=foo + SHLIB_MAJOR |
target_link_libraries(foo util kvm) | LIBADD=util kvm (preferred — sets -l + DPADD) |
target_include_directories | CFLAGS+= -I… |
add_custom_command (codegen, MIG) | explicit make rule feeding SRCS + CLEANFILES |
find_package(OpenSSL) | link base libssl/libcrypto, or keep as a port |
A typical program Makefile is 3–10 lines. Objective-C/GNUstep components compile .m fine but need explicit flags (-fobjc-runtime=gnustep-2.0 -fblocks, GNUstep header/lib paths) and usually WARNS?=0 for imported code. This works only if the GNUstep/libobjc2 stack they link is present — per your stack-provenance notes it's a source build into /System/Library/Libraries, so either give those their own LOCAL_LIB_DIRS entries or keep them as a package the world links against.
LOCAL_DIRS) when all hold: you own/vendor the source; it's part of base identity (boot-critical or version-locked to kernel/libc); its build is expressible in bsd.mk; its license permits base redistribution. Otherwise a package/port: optional/replaceable, own release cadence, large third-party build, heavy external dep chain, or uncertain provenance. Keep llvm19 and the ImageMagick dep chain as packages — dragging them into base is exactly your ISO-bloat problem. Your two CMake holdouts (libdispatch, swift-foundation-icu) are the judgment calls: ICU is a large third-party build (lean package/port); libdispatch is small and core (convert to bsd.lib.mk for base).Your two mid-flight points: think about pkg base shape changes, and — you don't need to drop init or other unused base bits from the tree; let them ride in FreeBSD-runtime and compose ISOs from a minimal pkgbase set plus your unique NextBSD packages. Confirmed correct — with one nuance that's per-component.
pkg install, not with WITHOUT_* knobs. This works cleanly for anything that is its own pkgbase package. The catch: whether "just don't install it" works depends entirely on package granularity — a component bundled into a must-have package can't be cherry-picked out.make packages (a.k.a. packagesystem) reads the METALOG that installworld/installkernel emit — every file stamped with a package= tag (set declaratively in Makefiles via PACKAGE=, defaulting to utilities) — groups files by tag, and emits one .pkg per group via pkg create. Per-package metadata comes from UCL templates in src/release/packages/*.ucl (rendered by generate-ucl.lua). It's the same pkg(8) as ports, just a separate FreeBSD-*-namespaced repo (base_latest). A full base is ~200–290 packages.
nextbsd-freebsd-compat builds the FreeBSD half with installworld → /stage → tar (guarded by your WITHOUT_TOOLCHAIN/TESTS/LIB32/DEBUG_FILES flags) and ships a nextbsd-base-<arch>.tar.gz. Moving to pkgbase means switching that step to make packages → a flat FreeBSD-* repo, and your image builders pkg install real base packages instead of untarring a tarball. Everything downstream of the pkg install line in your nextbsd/gershwin-on-nextbsd builders is unchanged.| Component | Package | Compose-out? |
|---|---|---|
init | inside FreeBSD-runtime (must-have; also carries /bin/sh, /sbin/mount, loader, ldconfig) | No — let it ride You can't exclude one file from a package, and there is no WITHOUT_INIT. If launchd is PID 1, just leave the dead /sbin/init on disk — a few KB, harmless. (Only re-tag it into its own PACKAGE= if you ever truly need it omittable.) |
syslogd | FreeBSD-syslogd | Yes — omit Separable; drop it and ship your logger (see §7). |
rc / rc.d | FreeBSD-rc | Yes — omit If launchd owns service management, omit the whole rc framework. |
| cron, at, ssh, pf, ipfw, bhyve, nfs… | each its own FreeBSD-* | Yes — omit freely |
| libc, rtld, libthr | FreeBSD-clibs | Must-have (note: libc is in -clibs, not -runtime) |
An unwanted tool in the utilities catch-all | FreeBSD-utilities | Re-tag If you must drop one file from the catch-all, add a one-line PACKAGE= in your tree to make it separable — cleaner than WITHOUT_*. |
Rule: reserve WITHOUT_* for genuine build-cost wins (toolchain, tests, lib32, debug — exactly what your compat flags already do), not for curation. Curation is a compose-time pkg install decision. This is the pkgbase realization of §5's in-base-vs-package rule.
Minimal bootable set to compose with your NextBSD-* packages: FreeBSD-clibs + FreeBSD-runtime + FreeBSD-kernel-generic (+ FreeBSD-rc only if you keep rc) + a few utilities (FreeBSD-utilities, FreeBSD-caroot/certctl for TLS, pkg itself). vermaden booted a full pkgbase system in 300 MB RAM; the minimal set is a fraction of the 208-package base. The build flow — bootstrap pkg into an empty root, pkg -r $ROOTFS install FreeBSD-clibs FreeBSD-runtime FreeBSD-kernel-generic … NextBSD-everything, regenerate /etc DBs, purge cache, makefs — is exactly what your builders already do; only the install line's contents change.
PACKAGE=nextbsd-<component> to each LOCAL_DIRS Makefile so make packages emits clean NextBSD-* packages instead of dumping into FreeBSD-utilities.NextBSD-everything; add a slimmer NextBSD-base for headless/minimal images so "the ISO set" is a one-liner.FreeBSD-*, layer NextBSD-* on top. Renaming all base packages is churn you'd re-merge forever, and the FreeBSD-* vs NextBSD-* split cleanly mirrors your existing compat-vs-userland boundary. Fork a package's name to NextBSD-* only when you've genuinely patched its contents (e.g. a modified runtime/init), so version comparison never pulls upstream's over yours. This is exactly the GhostBSD/TrueOS-lineage pattern.REPOS_DIR isolation (you already point the client at only the NextBSD flat repo) and set explicit priority in the shipped NextBSD.conf.Uncertain and worth a grep -rl 'PACKAGE=' /usr/src on your 15.x checkout: the exact tag for init and newsyslog, and the precise per-branch membership of the individually-packaged daemons.
Can stock FreeBSD syslog replace Apple's ASL, and can ASL callers work against it? Yes — feasibly and at low risk.
syslogd/ASL stack with FreeBSD's stock syslogd(8) + libc syslog(3), and satisfy any asl_* callers with a thin libasl write-shim that translates asl_* → syslog(3). The load-bearing fact: Darwin's own syslog(3) is implemented as a shim over ASL — so your shim is just that same adapter pointed the other way, which is why it's low-risk.| Caller type | Action | Effort |
|---|---|---|
Plain syslog(3)/openlog() | Links FreeBSD libc, lands in syslogd(8). Just works. | Zero |
GNUstep NSLog | Already uses stderr/plain syslog() (never ASL). Build with HAVE_SYSLOG. | Trivial |
asl_*() write calls | Thin libasl: map facility→LOG_*, level→priority, ASL_KEY_SENDER→ident, ASL_KEY_MSG→message, extra keys→key=value text (or RFC5424 SD). | 1–2 days |
asl_search/store/query | Stub to empty/NULL. No consumer in a GNUstep desktop stack. | Hours |
Do not port Apple's syslogd wholesale (the Darling route) — it drags in libnotify/notifyd, launchd socket-activation, and the ASL DB manager to duplicate a daemon FreeBSD already ships. Going to FreeBSD syslogd severs those couplings, which is desirable: no notify coupling (and your IPC is DO-based anyway), no launchd socket handoff (syslogd opens /var/run/log itself under rc), newsyslog(8) replaces aslmanager rotation. The only lost capability is structured log querying (a Console.app-class browser) — which NextBSD doesn't ship. Explicitly exclude modern os_log/Unified Logging; your ASL-era sources predate it and it's a heavy binary-format rabbit hole. This lets you delete the entire src/syslog/ Apple tree from the userland, shrinking maintenance and licensing surface.
Only rebuild the kernel, only rebuild world, and make both fast — the real flag names.
# kernel only
make buildkernel KERNCONF=NEXTBSD # NO_MODULES=yes / MODULES_OVERRIDE="a b" to scope modules
make installkernel KERNCONF=NEXTBSD
# world only
make buildworld
# one component the cross-safe way (after a world):
make buildenv TARGET=amd64 TARGET_ARCH=amd64 # spawns a shell with the tree's toolchain
cd nextbsd/usr.bin/nbtool && make obj && make && make install DESTDIR=/staging
# rebuild just one subtree but keep libs/includes current
make buildworld SUBDIR_OVERRIDE=nextbsd/usr.bin/nbtool -DNO_CLEAN
WITH_META_MODE=yes writes a .meta per target recording the command and — via the filemon(4) device — every file the command actually opened. That gives make a true dependency graph including undeclared headers, so NO_CLEAN rebuilds become correct (a no-op world drops from ~an hour to a couple minutes). Stack WITH_CCACHE_BUILD=yes (pkg install ccache; set CCACHE_BASEDIR=/usr/src, CCACHE_COMPILERCHECK=content) on top. Meta mode decides whether to rebuild; ccache makes the rebuilds that happen cheap (clean tree, branch switch, CI restore).Verdict: world + kernel cross-build from Linux is officially supported and CI-tested via tools/build/make.py (which bootstraps bmake + the tree's tools). Host needs bmake libarchive-dev clang-NN lld-NN. Building release media (ISO/memstick) on Linux is NOT supported — make release needs a FreeBSD host. This is precisely your intended split: continuous cross-builds on Linux, real releases on a NextBSD box.
env MAKEOBJDIRPREFIX=/obj ./tools/build/make.py --bootstrap-toolchain \
TARGET=amd64 TARGET_ARCH=amd64 buildworld buildkernel KERNCONF=NEXTBSD -j$(nproc)
filemon(4) is a FreeBSD kernel device — on Linux you build with -DNO_FILEMON, so meta mode degrades to command-line-change detection (it won't notice a changed included header). ccache/sccache must compensate at the compile step, and you should cut a periodic clean build to resync. Two flag spellings (WITHOUT_CROSS_COMPILER, CROSS_TOOLCHAIN) are moderate-confidence — verify against your tree's src.opts.mk before relying on them.Your added idea — container + caching to speed obj at different layers — is feasible and a cleaner encoding of the artifact cascade you already run. BuildKit computes the invalidation you currently hand-wire.
Two mechanisms, don't conflate them: image layers (content-digest keyed, shareable, push to a registry) and cache mounts (RUN --mount=type=cache,target=/usr/obj — persistent scratch, builder-local, not part of any layer). Map the tiers to multi-stage FROMs. The subtlety that bites everyone: the FreeBSD source tree is one directory — a single COPY src/ /usr/src means any edit busts every downstream stage. Split the COPY by subtree so a sys/-only change doesn't invalidate the world-obj layer. (World legitimately reads some sys/ headers, so partitioning is imperfect — you accept some over-rebuild at the world/kernel boundary.)
-DNO_FILEMON mandatory; meta-mode incrementality inside a container is command-line-grade, not file-dependency-grade. ccache/sccache carry it; schedule periodic clean rebuilds.type=registry,mode=max (no size cap, costs storage+bandwidth); reserve type=gha for small hot layers. (Since Nov 2025 you can exceed 10 GB on pay-as-you-go, but registry cache is still the right home for multi-GB obj.)buildkit-cache-dance (which re-hits the 10 GB wall for a multi-GB tarball) or a self-hosted/persistent builder — the honest answer for the hot obj mount.WITH_CCACHE_BUILD is simpler.One orchestrator detects what changed and calls reusable per-artifact workflows conditionally. Keep two axes separate: selective (PR/push, fast, no ISO) vs full/release (manual or nightly, everything).
Use a detect-changes job with dorny/paths-filter emitting boolean outputs consumed downstream via needs + if (workflow-level paths: can't do per-job granularity). Filter sys/**→kernel, bin lib usr.bin usr.sbin contrib share/mk→world, ports nextbsd-pkg→pkg. Two gotchas: put share/mk/** and toolchain paths in the broadest filter (a Makefile change must force a full rebuild); and because a skipped upstream makes needs see result == 'skipped' (falsy), use if: always() && … and check .result explicitly, or the job silently never runs.
Three tiers of PR gate scoped to what changed:
tools/build/checkstyle9.pl in-tree (mechanical style(9)); run it against only changed files from git diff --name-only. Add git clang-format --diff and mandoc -Tlint/igor for man pages.sys/dev/foo/ → targeted sys/modules/foo build) against a warm obj tree — minutes, not hours.lint required in branch protection. Because path-filtered jobs get skipped, use the always-run-detect + conditional-build pattern (a required check that never reports is a merge deadlock).| Artifact | Best runner | Why |
|---|---|---|
| Style/lint | hosted ubuntu-latest | pure static checks, seconds |
PR buildkernel / scoped build | vmactions/freebsd-vm | real FreeBSD, no infra, fits in 6 h |
Full buildworld (nightly) | self-hosted FreeBSD | too big for hosted; wants warm obj+ccache |
make packages / pkg repo | self-hosted / poudriere host | large, long, persistent state |
make release / ISO | self-hosted or real hardware | slow, large output; matches "real releases on real NextBSD" |
/usr/obj+ccache on local disk, zero cache upload) turn a 3–4 h cold world into a ~20 min incremental. Cost: you own security — never run fork-PR code on self-hosted runners; restrict them to main/trusted. On hosted, cache ccache (capped ~4–8 GB), not the whole obj tree.Replace the N-repo duplicated pipelines with reusable workflows (on: workflow_call) per artifact + composite actions for shared setup (checkout + ccache restore + freebsd-vm/self-hosted select). Decision logic (path filters, if) lives only in the orchestrators; how-to-build lives only in wf-*.
.github/
workflows/
ci.yml # orchestrator: detect-changes + conditional per-component (PR/push)
continuous.yml # nightly/on-green full build → artifacts + snapshot pkgs
release.yml # workflow_dispatch: branch+tag → full pipeline → gh-release
upstream-sync.yml
wf-lint.yml wf-kernel.yml wf-world.yml wf-packages.yml wf-iso.yml # reusable
actions/
setup-freebsd/ ccache/ # composite
Continuous vs tagged, and the 2 GiB wall you've already hit.
main): full cross-build → upload-artifact (kernel/world tarballs; 2 GiB/artifact cap) + push snapshot pkgs to nextbsd-pkg. No tag, short retention.workflow_dispatch, manual): operator triggers release.yml with a version → creates the release branch + tag → full world→kernel→ISO on self-hosted/real hardware → softprops/action-gh-release attaches the ISO + checksums. This is exactly your "build real releases myself on a real NextBSD kernel, push continuous ones from Linux crossbuild" split.mkuzip -C 19 -s 131072 already saves ~223 MB, and keeping llvm19/ImageMagick out of base (§5) is the big win; (2) split split -b 1900m + a reassembly script; (3) external storage (S3/R2/mirror) with only a checksum+URL on the Release; (4) self-hosted release runner publishing straight to your mirror. ISO tooling (cd release && make cdrom memstick, via makefs/mkimg) is FreeBSD-host-only — do it on the NextBSD builder, not the Linux cross runner.| Phase | Goal | Exit criteria |
|---|---|---|
| 1 | Seed pkgdemon/nextbsd from FreeBSD main snapshot; land upstream-sync.yml; prove one merge-PR round-trip. | Repo ≤1.5 GB, sync button opens a PR, first --allow-unrelated-histories merge done. |
| 2a | Bring Mach core in-tree as marked sys/ patches + Mach glue kld; unionfs as a sys/fs kld. Get kernel building via tools/build/make.py on Linux CI. | buildkernel KERNCONF=NEXTBSD green on Linux; grep NEXTBSD footprint reviewed. |
| 2b | Relocate userland under nextbsd/, wire LOCAL_* hooks, delete src/syslog/ + add libasl shim, convert libdispatch to bsd.mk, keep ICU as a package. Switch the FreeBSD half from installworld+tar to make packages (pkgbase), tag in-tree tools with PACKAGE=nextbsd-*. | buildworld green with NextBSD tools installed; no cmake in the base path; a flat FreeBSD-* + NextBSD-* pkg repo publishes. |
| 3 | Selective ci.yml (detect-changes + reusable wf-*), ccache/meta-mode, GHCR layer caching, self-hosted FreeBSD runner for world/pkg. | A sys/-only PR rebuilds only kernel; warm incremental world <30 min. |
| 4 | continuous.yml (Linux crossbuild snapshots) + manual release.yml (branch+tag, ISO on NextBSD hardware, 2 GiB handling). | Continuous pkgs auto-publish; a tagged release cuts an ISO asset. |
drm-kmod)? How does this reconcile with the existing ko2kext.sh/personality tooling and the prior in-kernel-IOKit feasibility work? Needs a dedicated sidebar before anything is locked.make release targets = a whole media matrix (§11). The real scope here isn't just "an ISO" — it's a set of build targets: a live target, installer targets for UFS and ZFS, Raspberry Pi / arm64 SD-card images, and likely more (VM/disk images, memstick). This should be designed on top of the existing image-strategy survey's live / installer / disk edition framing (ZFS gated to non-live editions), not reinvented from stock release(7). Open questions for the breakout: how many of these are stock release(7) targets (cdrom/memstick) vs bespoke makefs paths you already run; how the pkgbase-compose builders produce each edition from one package set; which need a real NextBSD/arm host vs the Linux cross runner; and how the CI release.yml exposes them (one dispatch with an edition/arch matrix). Its own breakout, cross-referenced with the images survey.FreeBSD-* and layer NextBSD-* (recommended here), or rename/fork more aggressively? Which specific packages you fork the name of (only genuinely-patched ones, e.g. a modified runtime), the PACKAGE=nextbsd-* tagging scheme for in-tree tools, and metapackage shape (NextBSD-everything vs a slimmer NextBSD-base). Still to be pinned down.releng/15.1, not main. Tracking main gets you the newest tree (and the best tools/build/make.py cross support) but more churn; tracking a releng/stable branch is calmer and is GhostBSD's choice. The sync model is identical either way — just point the freebsd remote at the branch you pick.Every load-bearing claim above traces to a source-cited research pass (FreeBSD build(7)/src.conf(5)/release(7), HardenedBSD's migration, Apple/Darling ASL & IOKit sources, GitHub Actions & BuildKit docs). Two FreeBSD flag spellings (WITHOUT_CROSS_COMPILER, CROSS_TOOLCHAIN) are flagged moderate-confidence — verify against your tree before quoting. No repositories were created and no code was written in producing this document.