NextBSD · Monorepo · Feasibility & setup

One tree to rule them all: consolidating NextBSD into a single freebsd-src monorepo

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.

Investigated 2026-07-17 by six parallel source-grounded agents: current-state recon (read from your live checkouts + GitHub orgs), upstream-sync strategy, buildworld integration, CI/CD design, Apple ASL vs FreeBSD syslog, kld-vs-kext, and containerized layer caching. Planning only — no repos created, no code written.

Draft · not final Working draft — several topics are deliberately unsettled and slated for their own breakout docs. In particular the IOKit / kld-vs-kext position (§4) is a provisional stance pending a dedicated sidebar; 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.

Verdict

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.

0 · What you actually have today 1 · The sync model & why 2 · Phase 1 — stand up the repo 3 · Tree layout & ownership rules 4 · Phase 2 — Mach, unionfs, kld/kext 5 · Userland into buildworld (drop cmake) 6 · PKGBASE shape (compose, don't strip) 7 · Replacing Apple syslog/ASL 8 · Selective & incremental builds 9 · Containerized layer caching 10 · CI/CD topology & patch checks 11 · Packages, ISOs & releases 12 · Roadmap & open decisions

0What you actually have today

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.

Two recon findings that revise your premises 1. Userland is already mostly bsdmake. Of the ~23 components in 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.

2. You already carry the full Darwin stack. Mach (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.
Stale memory pointer, corrected The path ~/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.

1The sync model — and why merge, not patches

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.

ModelHowFit
Merge from upstream remoteAdd 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 patchesKeep 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 branchPristine 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 subtreeEmbed freebsd-src under a prefix.Wrong tool Subtree is for upstream living in a subdir. Here freebsd-src is the repo root.
What the real downstreams teach HardenedBSD = merge-based & automated (your template). GhostBSD tracks a stable branch and keeps invasive work in separate repos — minimize in-tree edits. MidnightBSD & the original NextBSD are the cautionary tales: both synced rarely, drifted, and decayed into de-facto hard forks (old NextBSD's Mach layer became un-rebaseable against CURRENT). Cadence dominates cost — weekly small merges are dramatically cheaper than quarterly mega-merges. Surface area dominates conflict count — every upstream file you edit in place is a recurring tax; every new file you add is free forever.

2Phase 1 — stand up pkgdemon/nextbsd

Concrete 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.

2.1 — Seed the tree (single branch, no deep history)

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.

The one-time cost of a snapshot seed Because your root commit is a squashed snapshot sharing no ancestry with 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.

2.2 — The sync-via-PR workflow

Do NOT use the popular fork-sync recipe The stock 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."

3Tree layout & the one ownership rule

Clean upstream merges and obvious "what's ours" are the same goal, served by one rule.

The rule New code goes in new files/directories. Edits to upstream files are rare, minimal, and marked. New files can never conflict on a merge; every line you change inside an existing FreeBSD file is a conflict waiting to recur on every future sync.
WhatWhereMechanism
Your libraries (libmach, CF, libasl…)nextbsd/lib/* or lib/libnext*bsd.lib.mk + LOCAL_LIB_DIRS
Your tools & daemonsnextbsd/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 subsystemssys/next/new dir wired via minimal SUBDIR += edit
Kernel configsys/*/conf/NEXTBSDnew 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.

4Phase 2 — Mach, unionfs, and the kld/kext decision

"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.

Provisional verdict — IOKit sidebar pending This whole section is the current working position, not a settled decision — a dedicated IOKit breakout doc will revisit it. As it stands: Loading Apple's shipping .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.

4.1 — Where Mach and unionfs actually live

ComponentIn-tree sys/ patchLoadable kld
Mach coremach_msg/trap path, struct proc/struct thread extensions for tasks/threads/port-right namespaces, VM hooks for memory objectsYes — 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 layoutYes This is essentially what Darling's LKM does via /dev/mach ioctls.
unionfs and all filesystemsYes — 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.

4.2 — Where IOKit earns its keep (narrowly)

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.

5Userland into buildworld — and dropping cmake

The sanctioned hooks, the CMake→bsd.mk translation, and the decision rule for "in world vs a package."

5.1 — The 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

5.2 — Minimal Makefiles & the CMake translation

CMakebsd.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_directoriesCFLAGS+= -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.

Decision rule — in buildworld vs a package In-tree (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).

6PKGBASE shape — compose, don't strip

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.

Verdict Leave unused base components in the source tree (minimal upstream diff → clean merges) and curate at ISO-compose time via 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.

6.1 — How pkgbase slices base

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.

The single biggest concrete build-system change this implies Today 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.

6.2 — Per-component: does "just don't install it" work?

ComponentPackageCompose-out?
initinside 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.)
syslogdFreeBSD-syslogdYes — omit Separable; drop it and ship your logger (see §7).
rc / rc.dFreeBSD-rcYes — 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, libthrFreeBSD-clibsMust-have (note: libc is in -clibs, not -runtime)
An unwanted tool in the utilities catch-allFreeBSD-utilitiesRe-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.

6.3 — Minimal ISO set + derivative shape changes

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, makefsis exactly what your builders already do; only the install line's contents change.

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.

7Replacing Apple syslog/ASL with FreeBSD's

Can stock FreeBSD syslog replace Apple's ASL, and can ASL callers work against it? Yes — feasibly and at low risk.

Verdict Replace Apple's 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 typeActionEffort
Plain syslog(3)/openlog()Links FreeBSD libc, lands in syslogd(8). Just works.Zero
GNUstep NSLogAlready uses stderr/plain syslog() (never ASL). Build with HAVE_SYSLOG.Trivial
asl_*() write callsThin 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/queryStub 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.

8Selective & incremental builds

Only rebuild the kernel, only rebuild world, and make both fast — the real flag names.

7.1 — Partial builds

# 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

7.2 — Fast incremental: meta mode + ccache

META_MODE + filemon is THE fast-incremental feature 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).

7.3 — Cross-building on Linux

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 supportedmake 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)
Linux caveat 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.

9Containerized layer caching

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.

Layer 0 toolchain (clang/lld/bmake + tools/build) ── rare, expensive ── immutable GHCR image Layer 1 world-obj (make buildworld) ── freq, most exp ── registry cache + /usr/obj mount Layer 2 kernel-obj (make buildkernel KERNCONF) ── freq ── cache mount + small reg cache Layer 3 modules + Darwin userland components ── most freq ── cache mount only Layer 4 pkgbase / ISO assembly ── derived ── ephemeral; artifact out

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.)

Honest tradeoffs — read before committing

10CI/CD topology & "patch checks"

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).

push / PR ─▶ ci.yml (orchestrator) ├─ detect-changes ──▶ outputs: kernel / world / pkg / docs (dorny/paths-filter) ├─ lint ....................... always (style9, clang-format, igor) — fast PR gate ├─ kernel ── if kernel ─▶ uses: ./.github/workflows/wf-kernel.yml ├─ world ── if world ─▶ uses: ./.github/workflows/wf-world.yml └─ packages ── if pkg||world ─▶ uses: ./.github/workflows/wf-packages.yml workflow_dispatch ─▶ release.yml : branch + tag ─▶ world ─▶ kernel ─▶ iso ─▶ gh-release schedule (nightly) ─▶ continuous.yml : full green build ─▶ artifacts + push to nextbsd-pkg

9.1 — Selective builds

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.

9.2 — "Patch checks" on contribution

Three tiers of PR gate scoped to what changed:

9.3 — Where each artifact runs

ArtifactBest runnerWhy
Style/linthosted ubuntu-latestpure static checks, seconds
PR buildkernel / scoped buildvmactions/freebsd-vmreal FreeBSD, no infra, fits in 6 h
Full buildworld (nightly)self-hosted FreeBSDtoo big for hosted; wants warm obj+ccache
make packages / pkg repoself-hosted / poudriere hostlarge, long, persistent state
make release / ISOself-hosted or real hardwareslow, large output; matches "real releases on real NextBSD"
The single highest-leverage decision Hosted runners cap jobs at 6 h and share a 10 GB cache; a cold world+kernel+ISO can exceed both. Self-hosted FreeBSD runners (72 h limit, persistent /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.

9.4 — Keep it DRY

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

11Packages, ISOs & releases

Continuous vs tagged, and the 2 GiB wall you've already hit.

The 2 GiB Release-asset wall (your known pain) GitHub Release assets are capped at 2 GiB per file. Your ISOs hit ~2 GB. In order of preference: (1) shrink under 2 GiB — your measured 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.

12Suggested sequencing & open decisions

PhaseGoalExit criteria
1Seed 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.
2aBring 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.
2bRelocate 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.
3Selective 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.
4continuous.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.

Open decisions — flagged for breakout / sidebar

Explicitly unsettled — discuss next These three are called out as not decided in this draft and each likely wants its own doc:

Other open decisions


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.