← Back

NextBSD CI Pipeline Plan

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.

0. Implementation status — June 2026

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:

1. Architecture Overview

nextbsd-redux/freebsd-src (fork, auto-synced daily) | changed? no --> stop. changed? yes --> repository_dispatch v nextbsd-kernel-toolchain (cross-toolchain image: baked source + clang-19) | bakes the EXACT change-detected source (FREEBSD_SHA) into the image | publishes ghcr.io/.../nextbsd-kernel-toolchain:{amd64,arm64}-{latest,fbsd-} | the image IS the source artifact; every consumer runs INSIDE it | +--> nextbsd-kernel --> kernel + kernel-obj artifacts (both arches) +--> nextbsd-kernel-modules --> module .ko artifacts (layered on kernel-obj) +--> nextbsd-freebsd-compat --> FULL base userland artifact | (buildworld WITHOUT_TOOLCHAIN: libc + everything, | NO compiler in base -> replaces ALL base packages | AND the old srclist-fbsdglue.txt) v nextbsd (the ASSEMBLER -- ingests the 4 artifacts above) | 1. lay down the from-source base (kernel + modules + base userland) = zero base packages | 2. + a few NON-base build deps (buildpkgs.txt) | 3. + Apple-shaped base (mach.ko + launchd/libxpc/configd/notifyd/asl/...) | 4. + pkglist.txt (kmods etc. not worth rebuilding in CI) v ISO

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.

2. Repos

RepoPurposeTriggers
freebsd-srcGitHub fork of freebsd/freebsd-src, all branchesScheduled daily sync
nextbsd-kernel-toolchainDockerfile + workflow to build cross-compilation toolchain containers for amd64 and aarch64repository_dispatch from freebsd-src sync, or manual
nextbsd-kernelKernel patches (patches/), custom kernel config (config/NEXTBSD), build workflowrepository_dispatch from nextbsd-kernel-toolchain, push to patches/** or config/**, or manual
nextbsd-kernel-modulesModule build workflow, consumes kernel obj artifactrepository_dispatch from nextbsd-kernel
nextbsd-freebsd-compatFull base userlandbuildworld 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
nextbsdAssembler — 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

3. Design Decisions

3.1 Why releng/15.0?

3.2 Why Linux runners for cross-building?

3.3 Why kernel modules must be built (not from pkg)?

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

3.4 Why separate repos instead of one monorepo?

3.5 Source pinning: the toolchain image is the source artifact

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.

3.6 Shallow vs. sparse (why the clone is full-tree)

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.

3.7 Cross-build entry point: tools/build/make.py

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

3.8 Fork hygiene: only our workflow runs

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.

3.9 Compiler (Clang 19), arm64 parity, and tag-gating

3.10 Base from source: no base packages

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.

3.11 Caching strategy (per build shape)

Two different caches, matched to two different build shapes:

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.

4. Repo: nextbsd-kernel-toolchain

4.1 Dockerfile (Dockerfile.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)"

4.2 Workflow: .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 }}

5. Repo: freebsd-src (fork sync)

5.1 Workflow: .github/workflows/sync-fork.yml

This 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 }}

6. Repo: nextbsd-kernel

6.1 Directory structure

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

6.2 Patch maintenance

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

6.3 Workflow: .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 }}

7. Repo: nextbsd-kernel-modules

7.0 Directory structure

nextbsd-kernel-modules/
  .github/
    workflows/
      build.yml

7.1 Workflow: .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 }}

8. Repo: 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.

8.1 Build shape

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

9. Repo: nextbsd (the assembler)

Not a producer — the integrator. It ingests the four producers' artifacts and builds the ISO. Roughly:

  1. Download the from-source base: nextbsd-base-<arch> + kernel + modules (cross-repo artifacts — needs DISPATCH_TOKEN + repository:, same pattern the module job uses for kernel-obj).
  2. Lay them into a chroot — zero base packages.
  3. Install a few non-base build-time dependencies (buildpkgs.txt).
  4. Mix in the Apple-shaped basemach.ko + launchd, libxpc, configd, notifyd, asl, etc. (today built in nextbsd's build.sh; could become their own producer repos later).
  5. Install pkglist.txt (kmods and things not worth rebuilding in CI).
  6. Build the ISO.

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.

10. Estimated Build Times

StageMeasured (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.

11. Setup Checklist

  1. Fork freebsd/freebsd-src into nextbsd-redux/freebsd-src (all branches)
  2. On the fork's 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.
  3. Create nextbsd-redux/nextbsd-kernel-toolchain with Dockerfile.amd64, Dockerfile.arm64 + workflow
  4. Create nextbsd-redux/nextbsd-kernel with patches (empty series to start), config (NEXTBSD), workflow
  5. Create nextbsd-redux/nextbsd-kernel-modules with workflow
  6. Create a Personal Access Token with repo scope for cross-repo repository_dispatch
  7. Add the token as DISPATCH_TOKEN secret in all four repos (the freebsd-src fork + the three NextBSD repos)
  8. Run nextbsd-kernel-toolchain workflow manually to bootstrap the first toolchain container
  9. After the first toolchain build, set the GHCR package nextbsd-kernel-toolchain visibility to public so the kernel/module container: jobs can pull it without credentials
  10. Add initial patches and kernel config to nextbsd-kernel, push to trigger first build

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

12. GitHub Free Plan Limits

ResourceLimitImpact
GitHub Actions minutes2,000/month (private) / unlimited (public)Public repos recommended
Concurrent jobs20 (Linux) / 5 (macOS)Plenty for this pipeline
Artifact storage500 MB per repoCompress obj trees; use Releases for permanent storage
GHCR (container registry)Free for public reposToolchain containers stored here
Job timeout6 hoursWell within limits

13. Future Enhancements