NextBSD builds its entire base from source on GitHub Actions free-tier runners — no pkgbase base packages at all. Four repos cross-build the kernel, modules, and the full base userland and publish them as artifacts; a fifth repo (nextbsd) ingests those, mixes in the Apple-shaped base (Mach + launchd + the Apple system libraries), and assembles an ISO. No self-hosted runners, no open ports, no paid plans.
The pipeline is live end-to-end and has been refactored around CI-gated rolling continuous releases. The section-by-section design below is the original blueprint and its embedded workflow YAML predates the changes summarized here — the five repos themselves are now the source of truth. What changed since the blueprint:
continuous GitHub release on every green main build, and consumers ingest from those releases instead of scraping gh run list --branch main --status success --limit 1. kernel → kernel-obj-<arch>.tar.gz + nextbsd-kernel-<arch>.tar.gz; modules → nextbsd-modules-<arch>.tar.gz; compat → nextbsd-base-<arch>.tar.gz. The toolchain's GHCR :<arch>-latest tag is its continuous pointer (no release needed).nextbsd CI is now wired (the table in §2 below still says “not yet wired” for historical reference): it ingests base/kernel/modules from their continuous releases and publishes the ISO to its own rolling continuous release on push-to-main or an upstream cascade.push: false). Core invariant: a PR never updates continuous anywhere — every publish/release job requires github.ref == 'refs/heads/main' (a PR's ref is refs/pull/<N>/merge), so nothing downstream can ingest in-flight PR work.nextbsd-kernel has a PR-only boot smoke test: it drops the PR's freshly-built kernel into the latest continuous NextBSD ISO (via a FreeBSD VM, since the root is UFS), boots it under qemu, and runs nextbsd's boot-test.sh marker suite. Runs only on pull_request; does not gate publish or the cascade.nextbsd-freebsd-compat is now a {amd64, arm64} matrix like the others, so all four producers emit per-arch continuous assets. The nextbsd ISO stays amd64-only (vmactions limitation); the arm64 assets are ready for a future arm64 ISO runner.repository_dispatch from the toolchain flows toolchain → {kernel, compat} → {modules, nextbsd}, each trigger gated on main and fired from the publish job (after continuous is refreshed, so consumers ingest this run's output, not the prior one).upload-artifact strips the exec bit, which left bmake at 0644 in the kernel obj and crashed the modules build (make.py PermissionError) until fixed.The four upstream repos are artifact producers; nextbsd is the assembler. The result is a minimal FreeBSD base built entirely from source, mixed with an Apple-shaped userland, packaged as an ISO. This eliminates srclist-fbsdglue.txt and buildpkgs-base.txt (no curated FreeBSD subset, no pkgbase base); buildpkgs.txt (non-base build deps) and pkglist.txt (kmods) remain.
| Repo | Purpose | Triggers |
|---|---|---|
freebsd-src | GitHub fork of freebsd/freebsd-src, all branches | Scheduled daily sync |
nextbsd-kernel-toolchain | Dockerfile + workflow to build cross-compilation toolchain containers for amd64 and aarch64 | repository_dispatch from freebsd-src sync, or manual |
nextbsd-kernel | Kernel patches (patches/), custom kernel config (config/NEXTBSD), build workflow | repository_dispatch from nextbsd-kernel-toolchain, push to patches/** or config/**, or manual |
nextbsd-kernel-modules | Module build workflow, consumes kernel obj artifact | repository_dispatch from nextbsd-kernel |
nextbsd-freebsd-compat | Full base userland — buildworld WITHOUT_TOOLCHAIN (libc + all base libs/bins, no compiler). Replaces all base packages and the old fbsdglue subset. | repository_dispatch from nextbsd-kernel-toolchain, or manual |
nextbsd | Assembler — ingests the four producers' artifacts, mixes the Apple-shaped base, builds the ISO. Cross-repo consumer (needs DISPATCH_TOKEN to pull the producers' continuous releases). | Wired (see §0) — repository_dispatch (base-updated) from compat, push to main, or manual |
releng/15.0?pkg are built against 15.0-RELEASE ABIpkg install works for everything in userlandmake buildworld / make buildkernel is self-hosting by design — the host OS version doesn't matter, the build bootstraps its own toolchain from the source treeKernel modules are linked against the exact kernel config and source they're built with. A custom kernel (even with small patches like increasing the Mach syscall limit) means stock kmod packages won't load reliably. Modules must be built from the same patched source.
Rather than re-cloning freebsd-src in every downstream job (drift between stages) or shipping a ~300–400 MB source tarball as a GitHub Actions artifact (which would nearly fill the 500 MB/repo free cap and fight the kernel-obj artifact for space), the toolchain Dockerfile bakes the source into the image and pins it to the exact change-detected commit via the FREEBSD_SHA build-arg. The image is stored in GHCR (free and unlimited for public repos). Both the kernel and module jobs run inside that image, so they inherit the identical baked /usr/src — zero re-cloning, and the exact same source flows end-to-end. The pinned tag amd64-fbsd-<sha> is forwarded down the dispatch chain so a re-run rebuilds byte-identical source.
These are different axes. --depth 1 (shallow) drops git history — fine, buildkernel doesn't need it. git sparse-checkout set sys (sparse) materializes only sys/ — this breaks the build, because kernel-toolchain/buildkernel need share/mk, tools/build, usr.bin/, gnu/, lib/, etc. The clone is therefore shallow but full-tree (not sparse), pulled from the fork.
tools/build/make.pyOn Ubuntu, make is GNU make, which cannot parse BSD makefiles, and a bare bmake lacks the bootstrap environment. FreeBSD's supported way to cross-build on Linux — the “proven path” referenced in §3.2 — is ./tools/build/make.py, which bootstraps bmake and sets up the cross toolchain. All build steps (Dockerfile, kernel, modules) use it. The toolchain container also installs gh + curl so the in-container kernel/module jobs can fire repository_dispatch.
The freebsd-src fork inherits upstream's GitHub Actions (checklist.yml, style.yml, and crucially cross-bootstrap-tools.yml, which triggers a heavy matrix on every push to main). To stop these burning Actions minutes, the three upstream workflows are removed from main and replaced with only sync-fork.yml. main may diverge freely because the pipeline only ever syncs releng/15.0 (kept a clean upstream mirror so gh repo sync stays a fast-forward). A scheduled workflow must live on the default branch to fire, so sync-fork.yml lives on main but its job operates solely on releng/15.0. No upstream workflow listens to releng/15.0 events, so the daily sync triggers nothing else.
iwlwifi uses __builtin_popcountg, a Clang 19 builtin). Ubuntu 24.04 only ships clang-18, so the toolchain pulls clang-19/lld-19 from apt.llvm.org and points --cross-bindir at it. Building FreeBSD's in-tree Clang instead (dropping --cross-bindir) is a dead end on Linux: make.py errors Could not infer value for $XCC — it requires an external toolchain. The in-tree clang would be a throwaway build tool anyway, so external stock Clang 19 (same version) is the right call: fast, lean, builds every module.{amd64, arm64}; each leg pulls its per-arch image and uploads per-arch artifacts. Native arm64 runners are only needed to run/boot/test aarch64 artifacts, not to build them.--cross-bindir="$CROSS_BINDIR", and CROSS_BINDIR is baked into the image ENV. The image drives the compiler, so bumping clang means rebuilding only the toolchain image — no downstream workflow changes.:<arch>-<branch>) and never touch production; only main builds publish :<arch>-latest + :<arch>-fbsd-<sha> and cascade downstream. Since docker/build-push-action pushes only on success and there is no pull_request trigger, opening a PR never builds or touches images. So merging a PR (then the main build) is what promotes a change to production.NextBSD does not install pkgbase base packages. The kernel, modules, and the entire base userland are built from source by the four producer repos and shipped as artifacts. nextbsd-freebsd-compat runs a buildworld WITHOUT_TOOLCHAIN — this builds libc and every base library/binary but deliberately omits the compiler (clang/lld/lldb). That keeps the base lean and out of the “build a whole LLVM into base” trap; anyone who wants to compile on-device runs pkg install llvm19 from ports. Clang only ever exists as the cross build compiler inside the toolchain image — never in the shipped base.
This replaces two things from the old nextbsd build: srclist-fbsdglue.txt (the curated “irreducibly-FreeBSD-only” subset — now we just build the whole base) and buildpkgs-base.txt (pkgbase base packages — now from our artifacts). buildpkgs.txt survives for non-base build-time dependencies, and pkglist.txt for kmods and other things not worth rebuilding in CI. nextbsd then mixes this minimal from-source base with the Apple-shaped base (Mach + launchd + the Apple system libraries) and builds the ISO.
Why this matters for the pipeline: nextbsd-freebsd-compat is a full buildworld — the heaviest compile in the whole system. Fast iteration on it depends entirely on the caching strategy below. It cross-builds exactly like the kernel/modules (same container, --cross-bindir, WITHOUT_* trims) — the only difference is that userland links against the target libc, so the build stages _includes/_libraries first.
Two different caches, matched to two different build shapes:
docker build. With docker/setup-buildx-action + cache-to/from: type=gha (per-arch scope), an identical-source rebuild reuses the whole kernel-toolchain layer. Measured: ~6.5 min cold → ~13 sec warm (~30×). Helps same-source re-runs; a new FREEBSD_SHA still rebuilds (keeps the cached apt layer).container: jobs, so actions/cache persists CCACHE_DIR cleanly. Catch: FreeBSD's WITH_CCACHE_BUILD does not wrap an external XCC (the cross clang from --cross-bindir) — it leaves the cache empty. Fix: the toolchain image ships a ccache wrapper bindir (/opt/ccache-cross, exposed as CCACHE_CROSS_BINDIR) that mirrors llvm-19/bin but routes clang/clang++ through ccache <real>. Consumers set --cross-bindir=$CCACHE_CROSS_BINDIR + persist CCACHE_DIR. A 1-file (or no-op) rebuild then mostly cache-hits; a compiler bump invalidates everything (different key).Net: the layer cache makes toolchain re-runs near-instant, and ccache makes the kernel/module/world compiles cheap when little changed — which is what makes iterating on the full base-world build (and the Mach + Apple work that feeds the ISO) practical.
nextbsd-kernel-toolchainDockerfile.amd64; Dockerfile.arm64 is identical bar ARG defaults TARGET=arm64 TARGET_ARCH=aarch64)
FROM ubuntu:24.04
ARG TARGET=amd64
ARG TARGET_ARCH=amd64
ARG FREEBSD_BRANCH=releng/15.0
# Exact upstream commit to bake. Empty => branch tip (bootstrap/manual).
ARG FREEBSD_SHA=
# MAKEOBJDIRPREFIX must be in the ENV (the build refuses it as a make arg).
# CROSS_BINDIR points make.py at the EXTERNAL Clang 19 cross toolchain (make.py
# requires an external toolchain on Linux). Clang 19 == FreeBSD 15.0's own
# compiler, so every module builds (e.g. iwlwifi needs the Clang 19 builtin
# __builtin_popcountg). Both inherited by the downstream kernel/module jobs.
ENV MAKEOBJDIRPREFIX=/usr/obj
ENV CROSS_BINDIR=/usr/lib/llvm-19/bin
# A minimal ubuntu base lacks host deps the GitHub runner ships implicitly.
# Empirically required to bootstrap bmake + build kernel-toolchain on releng/15.0:
# build-essential (host cc), bc + flex + bison (bmake tests / kbdcontrol),
# time (host-symlinks), libssl-dev (certctl), libarchive-dev, bmake,
# git/python3 (make.py), gh/curl (in-container repository_dispatch).
# Clang 19 / lld 19 from apt.llvm.org (Ubuntu 24.04 only ships clang-18).
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl gnupg \
&& curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/llvm.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/llvm.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main" \
> /etc/apt/sources.list.d/llvm.list \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| tee /usr/share/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends \
build-essential bc time flex bison bmake \
libarchive-dev libssl-dev clang-19 lld-19 git python3 gh \
&& rm -rf /var/lib/apt/lists/*
# Shallow (--depth 1) but FULL-TREE clone from the NextBSD fork (NOT sparse:
# buildkernel needs share/mk, tools/build, usr.bin/, gnu/, lib/ ...).
# FREEBSD_SHA pins the baked source to the change-detected commit.
RUN git clone --depth 1 --branch "${FREEBSD_BRANCH}" \
https://github.com/nextbsd-redux/freebsd-src.git /usr/src \
&& if [ -n "${FREEBSD_SHA}" ]; then \
cd /usr/src \
&& git fetch --depth 1 origin "${FREEBSD_SHA}" \
&& git checkout -q "${FREEBSD_SHA}"; \
fi
# Build ONLY the kernel toolchain, using the external Clang 19 via --cross-bindir
# so make.py doesn't bootstrap its own LLVM (fast + lean). NB: dropping
# --cross-bindir to build FreeBSD's in-tree clang FAILS on Linux ("Could not
# infer value for $XCC") - make.py requires an external toolchain.
RUN mkdir -p "${MAKEOBJDIRPREFIX}" \
&& cd /usr/src && ./tools/build/make.py \
--cross-bindir="${CROSS_BINDIR}" \
TARGET="${TARGET}" TARGET_ARCH="${TARGET_ARCH}" \
kernel-toolchain -j"$(nproc)"
.github/workflows/build-toolchain.yml
name: Build Toolchain Containers
on:
repository_dispatch:
types: [upstream-updated]
workflow_dispatch:
schedule:
- cron: '0 6 * * 1' # weekly fallback
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/nextbsd-redux/nextbsd-kernel-toolchain
FREEBSD_BRANCH: releng/15.0
jobs:
build:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- target: amd64
target_arch: amd64
- target: arm64
target_arch: aarch64
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- name: Resolve image tags
id: src
run: |
SHA="${{ github.event.client_payload.sha }}"
BRANCH="${{ github.ref_name }}"
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
if [ "$BRANCH" = "main" ]; then
echo "image_tag=${{ matrix.target }}-fbsd-${SHA:-tip}" >> "$GITHUB_OUTPUT"
else
echo "image_tag=${{ matrix.target }}-${BRANCH}" >> "$GITHUB_OUTPUT"
fi
# BRANCH builds publish ONLY a branch-scoped tag (never touch prod);
# only main publishes :latest + :fbsd-.
{
echo "tags<> "$GITHUB_OUTPUT"
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.${{ matrix.target }}
push: true
build-args: |
TARGET=${{ matrix.target }}
TARGET_ARCH=${{ matrix.target_arch }}
FREEBSD_BRANCH=${{ env.FREEBSD_BRANCH }}
FREEBSD_SHA=${{ github.event.client_payload.sha }}
tags: ${{ steps.src.outputs.tags }}
# Fire once (amd64 leg), and only on main - branch/PR builds are for
# testing and must not cascade or touch prod. fbsd_sha lets the kernel
# derive its per-arch image tag (:-fbsd-).
- name: Trigger downstream kernel build (amd64, main only)
if: matrix.target == 'amd64' && github.ref_name == 'main'
run: |
gh api repos/nextbsd-redux/nextbsd-kernel/dispatches \
-f event_type=toolchain-updated \
-f "client_payload[fbsd_sha]=${{ steps.src.outputs.sha }}" \
-f "client_payload[target]=${{ matrix.target }}"
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
freebsd-src (fork sync).github/workflows/sync-fork.ymlThis is the only workflow on the fork (see §3.8). It lives on main (required for schedule) but its job operates solely on releng/15.0. The upstream checklist.yml, style.yml, and cross-bootstrap-tools.yml are deleted from main so they never run.
name: Sync Fork
on:
schedule:
- cron: '0 6 * * *' # daily 6am UTC
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: releng/15.0
fetch-depth: 1
- name: Get current SHA
id: before
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Sync from upstream
run: gh repo sync ${{ github.repository }} --branch releng/15.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check if changed
id: after
run: |
git fetch origin releng/15.0
NEW_SHA=$(git rev-parse origin/releng/15.0)
echo "sha=$NEW_SHA" >> $GITHUB_OUTPUT
if [ "${{ steps.before.outputs.sha }}" != "$NEW_SHA" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Trigger toolchain rebuild
if: steps.after.outputs.changed == 'true'
run: |
gh api repos/nextbsd-redux/nextbsd-kernel-toolchain/dispatches \
-f event_type=upstream-updated \
-f "client_payload[sha]=${{ steps.after.outputs.sha }}" \
-f "client_payload[branch]=releng/15.0"
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
nextbsd-kernel
nextbsd-kernel/
patches/
0001-increase-mach-syscall-limit.patch
series # ordered list of patches to apply
config/
NEXTBSD # custom kernel configuration
.github/
workflows/
build.yml
Patches are plain git format-patch diffs, applied in the order listed in patches/series. To create a new patch:
# Make changes in a local FreeBSD checkout cd /usr/src # ... edit files ... git format-patch -1 -o /path/to/nextbsd-kernel/patches/ # Add to series file echo "0002-my-change.patch" >> /path/to/nextbsd-kernel/patches/series # Push -- CI runs automatically
.github/workflows/build.yml
name: Build NextBSD Kernel
on:
repository_dispatch:
types: [toolchain-updated]
push:
paths: ['patches/**', 'config/**']
workflow_dispatch:
env:
FREEBSD_BRANCH: releng/15.0
jobs:
kernel:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- target: amd64
target_arch: amd64
- target: arm64
target_arch: aarch64
container:
# Per-arch toolchain image. fbsd_sha (dispatch) -> :<arch>-fbsd-<sha>;
# patch-only push / manual run -> :<arch>-latest. Both arches CROSS-build
# on the x86_64 runner (clang cross-compiles; no emulation).
image: ghcr.io/nextbsd-redux/nextbsd-kernel-toolchain:${{ matrix.target }}-${{ github.event.client_payload.fbsd_sha && format('fbsd-{0}', github.event.client_payload.fbsd_sha) || 'latest' }}
steps:
- uses: actions/checkout@v4
- name: Apply patches
run: |
cd /usr/src
for p in $(cat "$GITHUB_WORKSPACE/patches/series"); do
echo "Applying $p"
git apply "$GITHUB_WORKSPACE/patches/$p"
done
- name: Copy kernel config
run: cp "$GITHUB_WORKSPACE/config/NEXTBSD" /usr/src/sys/${{ matrix.target }}/conf/
- name: Build kernel (no modules)
run: |
cd /usr/src
# MAKEOBJDIRPREFIX + CROSS_BINDIR inherited from the toolchain image ENV.
./tools/build/make.py \
--cross-bindir="${CROSS_BINDIR}" \
TARGET="${{ matrix.target }}" \
TARGET_ARCH="${{ matrix.target_arch }}" \
KERNCONF=NEXTBSD \
NO_MODULES=yes \
buildkernel -j"$(nproc)"
- name: Upload kernel artifact
uses: actions/upload-artifact@v4
with:
name: nextbsd-kernel-${{ matrix.target }}
path: /usr/obj/**/sys/NEXTBSD/
- name: Upload obj tree for modules
uses: actions/upload-artifact@v4
with:
name: kernel-obj-${{ matrix.target }}
path: /usr/obj/
compression-level: 6
# Fire once (amd64 leg) on a real source/toolchain change. run_id is shared
# across matrix legs, so the module job can fetch BOTH arch obj artifacts.
- name: Trigger module build
if: github.event_name == 'repository_dispatch' && matrix.target == 'amd64'
run: |
gh api repos/nextbsd-redux/nextbsd-kernel-modules/dispatches \
-f event_type=kernel-updated \
-f "client_payload[fbsd_sha]=${{ github.event.client_payload.fbsd_sha }}" \
-f "client_payload[run_id]=${{ github.run_id }}" \
-f "client_payload[sha]=${{ github.sha }}"
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
nextbsd-kernel-modules
nextbsd-kernel-modules/
.github/
workflows/
build.yml
.github/workflows/build.yml
name: Build NextBSD Modules
on:
repository_dispatch:
types: [kernel-updated]
workflow_dispatch:
inputs:
run_id:
description: 'nextbsd-kernel run ID that uploaded the kernel-obj-<arch> artifacts'
required: true
fbsd_sha:
description: 'Pinned FreeBSD SHA for the toolchain image tag (blank = latest)'
required: false
default: ''
env:
FREEBSD_BRANCH: releng/15.0
jobs:
modules:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- target: amd64
target_arch: amd64
- target: arm64
target_arch: aarch64
container:
# Same per-arch toolchain image the kernel was built in. Cross-builds on
# the x86_64 runner (no emulation). repository_dispatch OR manual input.
image: ghcr.io/nextbsd-redux/nextbsd-kernel-toolchain:${{ matrix.target }}-${{ (github.event.client_payload.fbsd_sha || inputs.fbsd_sha) && format('fbsd-{0}', github.event.client_payload.fbsd_sha || inputs.fbsd_sha) || 'latest' }}
steps:
- name: Download kernel obj tree
uses: actions/download-artifact@v4
with:
name: kernel-obj-${{ matrix.target }}
path: /usr/obj/
# The run lives in the kernel repo, not here; without repository:
# download-artifact looks in the current repo and 404s.
repository: nextbsd-redux/nextbsd-kernel
run-id: ${{ github.event.client_payload.run_id || inputs.run_id }}
github-token: ${{ secrets.DISPATCH_TOKEN }}
- name: Apply patches to source
run: |
cd /usr/src
git clone --depth 1 https://github.com/nextbsd-redux/nextbsd-kernel.git /tmp/nk
for p in $(cat /tmp/nk/patches/series); do
git apply /tmp/nk/patches/$p
done
cp /tmp/nk/config/NEXTBSD /usr/src/sys/${{ matrix.target }}/conf/
- name: Build modules
run: |
cd /usr/src
# NO_KERNEL=yes builds modules against the already-built kernel obj.
./tools/build/make.py \
--cross-bindir="${CROSS_BINDIR}" \
TARGET="${{ matrix.target }}" \
TARGET_ARCH="${{ matrix.target_arch }}" \
KERNCONF=NEXTBSD \
NO_KERNEL=yes \
buildkernel -j"$(nproc)"
- name: Upload modules artifact
uses: actions/upload-artifact@v4
with:
name: nextbsd-modules-${{ matrix.target }}
path: /usr/obj/**/modules/
- name: Create release
if: github.event.client_payload.release == 'true'
run: |
cd /usr/obj
tar czf /tmp/nextbsd-modules-${{ matrix.target }}.tar.gz $(find . -type d -name modules)
gh release create "v$(date +%Y%m%d)-modules-${{ matrix.target }}" \
/tmp/nextbsd-modules-${{ matrix.target }}.tar.gz \
--repo nextbsd-redux/nextbsd-kernel-modules \
--title "NextBSD Modules ${{ matrix.target }} $(date +%Y-%m-%d)" \
--notes "Built against FreeBSD ${{ env.FREEBSD_BRANCH }}"
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
nextbsd-freebsd-compat (the base userland)Produces the entire base userland from source as an artifact — the replacement for all pkgbase base packages and the old srclist-fbsdglue.txt. It runs inside the same toolchain image as the kernel/modules and cross-builds the same way; it is simply pointed at buildworld instead of buildkernel.
cd /usr/src
# Cross-build the base WITHOUT the compiler toolchain (no clang/lld in base).
# Trim everything not needed to run launchd + the Apple components.
./tools/build/make.py \
--cross-bindir="${CCACHE_CROSS_BINDIR}" \ # ccache-wrapped cross clang
TARGET="${TARGET}" TARGET_ARCH="${TARGET_ARCH}" \
WITHOUT_TOOLCHAIN=yes \ # no clang/lld/lldb in base -> pkg install llvm19
WITHOUT_TESTS=yes WITHOUT_DEBUG_FILES=yes \ # trims (extend as needed)
buildworld -j"$(nproc)"
# Stage into a chroot-able tree and tar it as the base artifact:
make.py installworld distribution DESTDIR=/stage ... # -> nextbsd-base-<arch>.tar
--cross-bindir + WITHOUT_*. The one extra cost: userland links against the target libc, so the build stages _includes + _libraries before the binaries (the kernel links nothing, which is why it was simpler).WITHOUT_TOOLCHAIN) keeps the artifact lean; pkg install llvm19 covers on-device compilation. Clang stays only in the toolchain image.CCACHE_CROSS_BINDIR + persisted CCACHE_DIR — essential here, since buildworld is the heaviest compile in the pipeline.nextbsd-base-<arch> artifacts for nextbsd to ingest.nextbsd (the assembler)Not a producer — the integrator. It ingests the four producers' artifacts and builds the ISO. Roughly:
nextbsd-base-<arch> + kernel + modules (cross-repo artifacts — needs DISPATCH_TOKEN + repository:, same pattern the module job uses for kernel-obj).buildpkgs.txt).mach.ko + launchd, libxpc, configd, notifyd, asl, etc. (today built in nextbsd's build.sh; could become their own producer repos later).pkglist.txt (kmods and things not worth rebuilding in CI).Its own work is mostly assembly/packaging, so its speedup is artifact reuse (skip a component whose artifact didn't change), not ccache — the compile caching lives in the producers.
| Stage | Measured (both arches, external Clang 19) |
|---|---|
| Fork sync + change detection | ~1 min |
| Toolchain container build (amd64 + arm64, external clang) | ~6-8 min; skipped when no upstream change |
Kernel build, NO_MODULES (per arch) | ~6-7 min |
| Module build, layered (per arch, full module tree) | ~17-20 min |
Measured on standard ubuntu-24.04 runners. The toolchain stays ~6-8 min because we use external clang-19 (no in-tree LLVM build). arm64 legs run in parallel with amd64 at the same speed (cross-compile, no emulation).
When upstream hasn't changed and no patches are pushed, nothing runs. Zero wasted minutes.
freebsd/freebsd-src into nextbsd-redux/freebsd-src (all branches)main: delete the upstream workflows (checklist.yml, style.yml, cross-bootstrap-tools.yml) and add only sync-fork.yml (see §3.8). releng/15.0 stays an untouched mirror.nextbsd-redux/nextbsd-kernel-toolchain with Dockerfile.amd64, Dockerfile.arm64 + workflownextbsd-redux/nextbsd-kernel with patches (empty series to start), config (NEXTBSD), workflownextbsd-redux/nextbsd-kernel-modules with workflowrepo scope for cross-repo repository_dispatchDISPATCH_TOKEN secret in all four repos (the freebsd-src fork + the three NextBSD repos)nextbsd-kernel-toolchain workflow manually to bootstrap the first toolchain containernextbsd-kernel-toolchain visibility to public so the kernel/module container: jobs can pull it without credentialsnextbsd-kernel, push to trigger first buildNote on cross-building: all build steps use ./tools/build/make.py (not bare make) — FreeBSD's supported Linux cross-build entry point, which bootstraps bmake. See §3.7.
| Resource | Limit | Impact |
|---|---|---|
| GitHub Actions minutes | 2,000/month (private) / unlimited (public) | Public repos recommended |
| Concurrent jobs | 20 (Linux) / 5 (macOS) | Plenty for this pipeline |
| Artifact storage | 500 MB per repo | Compress obj trees; use Releases for permanent storage |
| GHCR (container registry) | Free for public repos | Toolchain containers stored here |
| Job timeout | 6 hours | Well within limits |
{amd64, aarch64}, cross-built on the x86_64 runner (see §3.9).cache-to/from: type=gha so an unchanged-source toolchain rebuild is instant, and WITH_CCACHE_BUILD (with a persisted ccache) to speed partial-source rebuilds.obj artifact to stay KBI-safe; pkg kmods load as long as NEXTBSD stays KBI-compatible with stock 15.0.nextbsd-libc repo consuming a world-builder container