A Nix-based universal Linux kernel cross-compiler: build essentially any kernel version (2.6 → current 6.x) across ~12 architectures (arm/armhf/arm64, mips/mipsel/mips64 big+little endian, powerpc/ppcle/ppc64/ppc64le, x86_64), with the right toolchain selected automatically per kernel version.
This is the foundation under nixifying the rehosting linux_builder and igloo_driver
flows, and a successor to the hand-maintained embedded-toolchains Docker image (a pile
of unpinned wgets of musl.cc / panda.re prebuilt toolchains).
kernel version ──resolve──► era ──► (arch, gcc-band) cell ──► toolchain
"3.18.140" k3 gcc 6.5 / mipsel cross-mipsel-...-gcc
The "single environment" is not a baked image holding every toolchain — it's a resolver + dev shell. Nix laziness realizes only the cell you target; a binary cache makes any cell you've built before an instant substitution. So you flip between kernel versions as build targets without rebuilding anything you've seen.
| File | Role |
|---|---|
flake.nix |
inputs + outputs: the era×arch toolchain matrix, buildKernel, toolchainFor, a dev shell with kbuild <ver> <arch> |
resolve.nix |
kernelVersion → era via version bands (the gcc-floor dispatch) — sourcing-agnostic |
matrix.nix |
the data: kernel eras (gcc/binutils/musl bundles) × 12 arch triples + per-arch quirks |
mk-cross-toolchain.nix |
builds one (arch, versions) musl toolchain (userland; and the k3+ kernel bands) |
mk-kernel-toolchain.nix |
builds one kernel-only, no-libc period toolchain (the k2.6 kernel band — see the reframe below) |
sources.nix |
pinned component / toolchain inputs |
kernel.nix |
buildKernel { version, arch, src, config } — auto-resolves the toolchain (kernel toolchain for k2.6, else musl); emits vmlinux + the arch's bootable image (ARM zImage, x86 bzImage, arm64 Image.gz) + kernel-devel |
nix build .#bootlin-all builds every pinned Bootlin cell (25); each runs a -dumpmachine
install-check, and spot-compiles produce correct per-arch ELF (verified ARM/AArch64/PPC64LE/MIPS).
toolchainFor/buildKernel now resolve to the Bootlin cell where pinned, else the
musl-cross-make fallback — verified: 5.10/mipsel→Bootlin k4, 2.6.31/armel→musl-cross-make k2.6.
Pinned Bootlin cells (gcc per the release inside, not the release year):
| Band (kernel) | gcc | arches covered by Bootlin |
|---|---|---|
| k6 (6.x) | 13.3.0 (2024.05) | all 9 |
| k4 (4.x/5.x) | 9.3.0 (2020.08); x86_64 = 10.3.0 (2021.11, no older musl) | all 9 |
| k3 (3.x) | 6.4.0 (2018.02) | 7 — powerpc, x86_64 deferred (no gcc-6 musl: their musl starts 2020.08 / 2021.11) |
From-source (musl-cross-make) path PROVEN: nix build .#spike builds a k2.6 armel
toolchain (gcc 5.3.0) that compiles static + dynamic-musl ARM ELF. Findings from the spike:
- gcc 4.2.1 / 4.7.4 fail — mcm-master's
litecrosspassesAR_FOR_TARGET=…as configure args, which pre-modern autoconf rejects. True gcc-4.x needs a stable/older mcm pin (TODO). - mcm master ships 2026 musl CVE patches that corrupt musl-1.1.24
qsort.c;mk-cross-toolchain.nixstripspatches/musl-*/cve-*.diff. - gcc 5.3.0 is the oldest gcc that builds here; above the era-ideal for true 2.6 (upper-bound risk) — now validated: builds a real 2.6.31 ARM kernel with three documented era quirks (below).
From-source matrix FILLED (2026-06-29):
nix build .#k26-all— all 12 k2.6 cells build under gcc 5.3.0 (the band Bootlin can't supply). Spot-compiles confirm correct per-arch ELF, including the 3 arches Bootlin lacks at any band: powerpcle (32-bit PPC-LE), mips64eb, mips64el.nix build .#fromsource-extra— the Bootlin-uncovered modern cells: mips64eb/el + powerpcle at k3/k4/k6, plus powerpc/x86_64 at k3 (11 cells). gcc 6.5.0 (k3) / 9.4.0 (k4) / 13.3.0 (k6).- All mcm component versions are reconciled to musl-cross-make's blessed
hashes/set (it refuses to build a version it has no.sha1for). So the from-source band now has zero placeholder hashes.
Decision (2026-06-29): VALIDATED — gcc 5.3.0 is viable for k2.6. nix-build validate-k26.nix builds a stock kernel.org 2.6.31 ARM vmlinux
end-to-end via buildKernel, with the gcc 5.3.0 toolchain auto-resolved from the version.
Output: ELF 32-bit LSB executable, ARM, statically linked (3.8 MB). True gcc-4.x / a
stable-mcm pin is not needed — the gcc-5-on-2.6 breakage is all known, shimmable upper-bound
stuff, now handled in kernel.nix's k2.6 eraQuirks/postPatch:
compiler-gcc5.h: No such file— 2.6.x ships onlycompiler-gcc{3,4}.hbutcompiler-gcc.hdoes#include compiler-gcc<__GNUC__>.h. Shim: copy the gcc4 header tocompiler-gcc<major>.h(pure dispatch, no codegen change).multiple definition of pin_inotify_watch— gcc 5 defaults to C99 (gnu11) inline; 2.6.x assumes gnu89, so plain-inlineheader funcs emit duplicate external defs. Fix:-fgnu89-inline(sed into the top Makefile's-fno-commonline).timeconst.pl: defined(@array)— modern host Perl (≥5.22) removed that syntax; sed it out.
buildKernel VALIDATED ACROSS ALL BANDS (2026-06-30): nix build -f validate-bands.nix all
builds a stock kernel per modern era to a correct-arch vmlinux, exercising both sourcing paths:
- k3-mipsel 3.18.140
malta_defconfig— Bootlin k3 (gcc 6.4) → MIPS32 rel2 ELF - k3-x86_64 3.18.140
x86_64_defconfig— from-source mcm gcc 6.5.0 → x86-64 ELF - k4-arm64 5.10
defconfig— Bootlin k4 (gcc 9.3) → AArch64 PIE ELF - k6-x86_64 6.6
x86_64_defconfig— Bootlin k6 (gcc 13.3) → x86-64 ELF
(plus k2.6 2.6.31/armel from validate-k26.nix.) Two general buildKernel fixes fell out:
patchShebangs scripts tools (5.10's scripts/ld-version.sh has a #!/usr/bin/awk shebang absent
in the sandbox) and adding rsync to nativeBuildInputs (kernels ≥5.3 shell out to it in
headers_install).
FULL BAND×ARCH KERNEL SWEEP (2026-07-02): nix build -f validate-sweep.nix all builds one
stock kernel per band across every kernel-capable arch (42 cells) to a vmlinux, using
endianness/width-definite defconfigs (SGI Indy/Origin, Loongson, PowerMac) so a pass certifies
the ABI. Result: 32/42.
| Band | Pass | Fails (root cause, not flag-noise) |
|---|---|---|
| k6 (6.6) | 11/11 | — |
| k4 (5.10) | 10/11 | x86_64: objtool under tools/ has its own -Werror (gcc-13 use-after-free), unreachable by HOSTCFLAGS |
| k3 (3.18) | 10/11 | powerpc64le: vdso32 sub-build invokes host gcc. powerpc64 was the ELFv1/ELFv2 ABI clash (Bootlin gcc is elfv2-default, BE kernel wants -mcall-aixdesc/elfv1) — fixed with a dedicated elfv1-default kernel gcc (matrix.k3PpcKernel); it now builds and boots |
| k2.6 (2.6.31) | 2/9 → 3/9 | see the TRUE gcc-4.x section below — moving k2.6 to gcc 4.9.4 removed the hard errors and recovered powerpc |
Two kernel.nix eraQuirks fixes landed (recovered the 3 k3 dtc cells, no regressions):
KCFLAGS=-Wno-error (cross warnings) and HOSTCFLAGS -fcommon/-Wno-error (host dtc/objtool).
The sweep's headline finding: the residual failures are characterized root causes, not noise, and they cluster by era. Modern kernels (k4/k6) are essentially clean. The frontier is k2.6, which the next section tackles.
The k2.6 sweep above ran under gcc 5.3.0 and hit errors that pointed to the compiler being too new
for a 2009 tree (x86 vDSO -m elf_x86_64; gcc-5 C++/inline behavior), so we built a genuinely
era-appropriate gcc 4.9.4 cross toolchain from source (musl-cross-make). The feared "modern host
can't compile 2010s gcc" wall did not materialize; it was five small, individually-gated
(gccVer < 5) fixes in mk-cross-toolchain.nix:
- refresh the pre-musl
config.sub/config.guessfrom nixpkgsgnu-config(old ones reject*-linux-musl*triples); - use gcc 4.9.4 (4.7.4's in-tree
mpccan't detect a modernmpfr); - synthesize the mcm
hashes/entry from the staged tarball (4.9.4 isn't in mcm's blessed set); touchstaged tarballs newer than those hashes, else make fires an offlinewget;- build gcc's own C++03-era source with
-std=gnu++03(host gcc 13 defaults to C++17), and build the k2.6 bandlanguages = "c"(its libstdc++ predates musl's_IS*ctype macros).
Result: k2.6 → 3/9 (armel, armhf, powerpc), verified: gcc 4.9.4 produces correct ARM ELF
(static + dynamic musl). A kernel.nix k2.6 quirk (strip standalone -Werror from the old tree's
Makefiles — it lands after KCFLAGS and wins on precedence) recovered powerpc.
A separate mk-cross-toolchain.nix fix repairs the MIPS toolchain: gcc 4.9's MIPS libgcc
unwinder #include <asm/unistd.h>, which mcm otherwise stages into the build sysroot too late
(after libgcc). Adding a libgcc → kernel-headers-in-obj_sysroot dependency (gated to gccVer<5)
fixes it, so .#k26-all builds all 12 k2.6 toolchains — important for MIPS module and
other-version kernel builds even though it doesn't fix the 2.6.31 kernel (below).
Honest status — the k2.6 non-ARM tail is layered, and mostly NOT a toolchain problem. gcc 4.9.4 removed the host/build hard errors (vDSO, C++17 source, libstdc++, config.sub) and all 12 toolchains build, but the remaining kernel failures are kernel-source/arch issues that persist under gcc 4.9.4:
- mips ×4:
arch/mips/mm/page.caliases a function to a variable — rejected by gcc 4.9 too (not a gcc-5-only error, as first assumed), plusr4k_switch.Suses FPU opcodes (mtc1 $f30/$f31) the assembler rejects for the configured ISA. Both need kernel source patches. - powerpc64:
arch/powerpc/Makefile:171(bootwrapper_install %.dtb:) trips modern GNU make ≥4.3's "mixed implicit and normal rules" (plus the ELFv1/v2 ABI underneath). - x86_64: a
percpu_to_opinline-asm macro error under gcc 4.9.
So gcc 4.9.4 is necessary but not sufficient: it (plus the MIPS toolchain fix) gets the whole k2.6 toolchain row green, and gets ARM + powerpc kernels building, but the mips/ppc64/x86 2.6.31 kernels need per-arch source patches, not toolchain work. ARM (the primary firmware target) is solid. Prioritize the rest by firmware need. Fallback if a given arch's from-source path stays painful: kernel.org crosstool ships prebuilt gcc 4.9 (kernel-build-only, glibc).
The open question was whether an even more era-matched gcc 4.4.7 (what 2.6.31 was literally built
with) would clear the residual kernel-source failures (page.c alias, etc.) with fewer shims than
4.9.4. Tested directly (mipsel, from source, binutils held at 2.27 to isolate the gcc variable). Two
things came out of it, one expected and one decisive:
- The compiler proper builds fine — the "ancient gcc won't build under a 2026 host" fear is again
false. It needed only one new shim beyond 4.9.4's: build gcc's own C with
CC="gcc -std=gnu89 -fgnu89-inline"(gcc <4.8 is written in gnu89 C; host gcc's gnu11 inline madefloor_log2/exact_log2intoplev.credefinitions).all-gcccompleted clean. - But it can't consume our pinned musl 1.1.24. First its MIPS
libgccunwinder failed (config/mips/linux-unwind.h:struct siginfo— a named tag glibc has but musl doesn't; fixable with the same one-linestruct siginfo→siginfo_tpatch mcm ships for blessed versions). Clearing that only exposed the real wall: musl 1.1.24's own headers don't parse under gcc 4.4.7 —bits/alltypes.hfails withduplicate 'unsigned'/two or more data types. Same sysroot gcc 4.9.4 parsed cleanly; only the compiler version changed.
Conclusion (scoped to a musl toolchain): musl-cross-make blesses a specific set of gcc versions precisely because each needs a maintained (gcc × musl) compatibility patch set. gcc 4.9.4 is the oldest gcc that cleanly consumes modern musl 1.1.24 — going below it doesn't reduce shims, it leaves the supported envelope and trades a few kernel-source patches for an open-ended musl-header porting burden. For a musl toolchain, 4.9.4 is the practical period floor.
The wall above was a musl anachronism, not a gcc-age wall — musl 1.1.24 (2019) is a decade newer than
2.6.31 (2009), and the kernel is built -nostdinc -ffreestanding: it needs no target libc at all
(libgcc ships inside gcc). musl only matters for userland (busybox, guest utils). So the k2.6
toolchain splits by purpose:
- Kernel band: build period gcc
--without-headers --with-newlib(no musl). Tested for mipsel: gcc 4.4.7 builds clean on a 2026 host (one incidental snag — modern texinfo can't render gcc 4.4 docs, soMAKEINFO=true), and then compiles stock 2.6.31malta_defconfigto a 5 MBvmlinuxwith zero k2.6 source shims — no tree-wide-Werrorstrip, no-fgnu89-inline, noKCFLAGS=-Wno-error. The era-correct compiler doesn't need placating; those shims only ever existed to appease a compiler newer than the tree. - Userland band: keep gcc 4.9.4 + musl 1.1.24 (the floor established above). Userland needn't be period-correct — it just has to run on the emulated kernel.
This turns a red cell green, not merely prettier. In-session control on the same tree/defconfig:
gcc 4.9.4 (current k2.6 toolchain, with all shims) fails — arch/mips/mm/page.c:108: error: 'copy_page' alias in between function and variable is not supported (a hard error, so -Wno-error
can't touch it) — while period gcc 4.4.7 (no shims) succeeds. Corrects a doc bug: matrix.nix blamed
gcc ≥5 for the page.c alias, but 4.9.4 (<5) rejects it too; the idiom's ceiling is ≤4.4. So the recurring
"true gcc-4.x for k2.6" NEXT item is confirmed, not rejected — scoped to a kernel-only (no-libc)
toolchain, now landed as mk-kernel-toolchain.nix (period gcc 4.4.7) + the matrix.k26Kernel band;
buildKernel auto-uses it for 2.6.x kernels while toolchainFor keeps the musl userland toolchain.
Build them all with nix build .#k26-kernel-all.
Band sweep — all k2.6 arches, period gcc 4.4.7 (kernel-only), bare = every k2.6 source shim
stripped, shims = the existing -Wno-error/frame-size net kept:
| arch | toolchain | bare | +shims | verdict |
|---|---|---|---|---|
| armel | ✅ | ✅ 3.9 MB | ✅ | green either way |
| armhf | ✅ | ✅ 7.0 MB | ✅ | green either way |
| mipsel | ✅ | ✅ 5.0 MB | ✅ | was RED on 4.9.4 (page.c hard error) → period gcc fixes it |
| mipseb | ✅ | ✅ 4.0 MB | ✅ | bare, no shims |
| mips64eb | ✅ | ✅ 6.0 MB | ✅ | added in the completeness pass (ip27) |
| mips64el | ✅ | ✅ 5.8 MB | ✅ | added in the completeness pass (fuloong2e) |
| powerpc (32) | ✅ | ❌ | ✅ 7.6 MB | frame-size warning→error (gcc 4.4 bigger frames, a too-old artifact); -Wno-error net demotes it |
| powerpc64 | ✅ | ❌ | ✅ 13.3 MB | pseries_defconfig + FTRACE off (kernel.nix kernelConfigDisable): the function-graph tracer's return trampoline is the only R_PPC64_ADDR16_HI overflow (gcc 4.4 predates ppc64's medium code model); dropping that debug feature links clean |
| x86_64 | ✅ (native) | — | ✅ 17.6 MB | built as a genuine native gcc (target==host): a cross-with-newlib hits gcc's native-detection trap and an -elf triple omits the __linux__/OS predefines 2.6.31's x86 .S needs. Native gcc 4.4 + 3 sandbox fixes (fixincludes header dir, struct ucontext, CPATH) → clean |
Two corrections this surfaced: (1) ppc64 is not blocked by the make "mixed implicit and normal rules" error — testing a period make (gnumake42 = 4.2.1) proved those messages are non-fatal (the
fatal form is make ≥3.82; truly-period 3.81 is the hard-to-build-on-a-modern-host one), and the build
dies later at the linker regardless of make version; (2) the ppc64 reloc overflow is not iSeries-
specific (pseries_defconfig hits it too). Also, the bare column refined the thesis: the -Wno-error
net is not purely about appeasing a too-new gcc — ppc32's frame-size error is a too-old artifact —
so it's a general "this isn't the exact gcc the tree was tuned for" absorber, warranted for any gcc.
Net: band-wide period gcc 4.4.7 + the shim net is green for all 9 kernel-capable arches — arm×2,
mips×4 (32+64, both endians), ppc32, ppc64 (pseries + FTRACE off), and x86_64 (native gcc). The
non-negotiable win is mips, where period gcc removes a hard error (page.c) that no warning-demotion
can touch; the counterintuitive one is x86_64 — "host==target" makes it the awkward case for a
cross-build system, and the fix was to stop fighting it as a cross and build a genuine native gcc.
- #6 mips64eb/el [DONE] — added to the kernel band; both build
vmlinuxvia the real entrypoint (6.0/5.8 MB). MIPS is now complete (4 cells). - #7
-Wno-errorcompiler wrapper [DONE] —kernel.nix'sccShimwraps the cross gcc and the host cc so-Wno-errorlands last on every compile, defeating-Werrorwherever the tree injects it. This replaced three fragile knobs:KCFLAGS=-Wno-error, the tree-wide-Werrorstrip sed, and the-Wno-errorinsideHOSTCFLAGS(whose override-vs-append semantics flip at 4.19 — now irrelevant). Only the real host quirk-fcommon(k3 dtc) stays inHOSTCFLAGS. Revalidated: k2.6-mipsel/powerpc, k4-mipsel, k6-mipsel all green (k3-mipsel blocked only by a transient v3.x source-download failure). - #5 ppc64 [DONE], x86_64 [DONE] — both landed. ppc64: the
R_PPC64_ADDR16_HIlink overflow came only from the function-graph tracer's return trampoline (gcc 4.4 predates ppc64's medium code model);pseries_defconfig+FTRACEoff (kernel.nixkernelConfigDisable) → 13.3 MBvmlinux. Disproved two red herrings: not the make mixed-rules error (periodgnumake42proved those non-fatal) nor iSeries-specific. x86_64: the-elfdetour was the trap — it omits__linux__, so 2.6.31's x86.Smis-preprocesses. Built a genuine native gcc 4.4 instead (mk-kernel-toolchain.nixnativemode), clearing three sandbox walls a native ancient-gcc build assumes away — fixincludes'/usr/include(patchNATIVE_SYSTEM_HEADER_DIR),struct ucontext(glibc ≥2.26), and the in-buildxgcc's header search for target-libgcc's CPP check (CPATH). → 17.6 MBvmlinuxvia the real entrypoint.
Boot-validated across eras, and automated — nix build -f boot.nix all. The kernels don't just
compile, they boot, and that's a build target: boot.nix builds each (band, arch) system on a
qemu-friendly defconfig + fragments, then runs qemu headless in a sandboxed derivation and asserts the boot
reached the root-fs stage (no rootfs supplied → a VFS: Unable to mount root fs panic is the success
marker). A cell's derivation only succeeds if its kernel boots. Per band: nix build -f boot.nix k4;
one cell: tests.k4-powerpc64; interactive: nix run -f boot.nix runners.k4-arm64.
nix build -f boot.nix all boots 40 cells across four eras (2.6.31 → 6.6):
| band (kernel / gcc) | booting | not booting (still built) |
|---|---|---|
| k2.6 (2.6.31 / 4.4.7) | 8 | powerpc64 — 2.6.31 can't hand off to modern SLOF |
| k3 (3.18.140 / 6.5) | 10 | powerpc64le (3.18 vdso32 -mlittle-endian toolchain) |
| k4 (5.10.229 / 9.x) | 11 | — |
| k6 (6.6 / 13.x) | 11 | — |
k4 and k6 boot all 11 arches; the modern bands clear every k2.6 gap (powerpc64/powerpc64le on
-M pseries, armhf/arm64 on the DT-driven -M virt). Fixes the sweep surfaced — each a real
era×toolchain constraint now encoded in the table:
- 5.10 must be a late
.y(pinned 5.10.229): binutils 2.36 omits the symtab from empty objects (x86_64thunk_64.ounderx86_64_defconfig), which 5.10.0's objtool rejects. - armhf
multi_v7(k4/k6):GCC_PLUGINSoff — the ARM ssp-per-task plugin's ABI ≠ the Bootlin cross gcc. - powerpc64le:
COMPAToff (k4/k6) — its pure-64-bit LE toolchain can't build the 32-bit vdso. - 64-bit malta (mips64el/eb):
-cpu MIPS64R2-genericon k4/k6, but-cpu 5KEcon k3 (3.18's malta is silent on the generic core); the default malta CPU is 32-bit → a 64-bit kernel never starts. - k3 powerpc64: an ELFv1-default kernel gcc, not ABI-forcing. 3.18's BE Makefile emits
-mcall-aixdescand assumes an elfv1-default compiler; the Bootlin buildroot gcc defaults to elfv2 (-mcall-aixdesc incompatible with -mabi=elfv2), and a forced-mabi=elfv1produces a mixed-ABI vmlinux SLOF traps on.buildKernelroutes k3/powerpc64 to a kernel-only gcc 6.5.0 built aspowerpc64-linux(elfv1-default, asm-goto-capable) — seematrix.k3PpcKernel. Boots on-M pseries -cpu POWER8. - versatile: DT-only on k4/k6 → needs a
-dtb(buildKernel gained adtbsparam →$out/dtbs/); ATAG-based on k3 → needs none. powerpc/pmac32 needs the escc console built in on k2.6 + k3 (not k4/k6).
k2.6 (2.6.31, period gcc 4.4.7): 8 of the 9 kernel-capable arches boot (banner
Linux version 2.6.31 (gcc version 4.4.7)):
| cell | qemu | defconfig (+ fragments) | image |
|---|---|---|---|
mipsel |
-M malta |
malta_defconfig |
vmlinux |
mipseb |
-M malta (BE) |
malta_defconfig +CPU_BIG_ENDIAN −CPU_LITTLE_ENDIAN |
vmlinux |
mips64el |
-M fuloong2e |
fulong_defconfig |
vmlinux |
mips64eb |
-M malta (mips64) |
malta_defconfig →64-bit BE (+64BIT+CPU_MIPS64_R1+CPU_BIG_ENDIAN) |
vmlinux |
armel |
-M versatilepb |
versatile_defconfig |
zImage |
armhf |
-M realview-pb-a8 |
realview_defconfig →PBA8/ARMv7-only (needs the ARMv7 toolchain) |
zImage |
powerpc |
-M g3beige |
pmac32_defconfig +SERIAL_PMACZILOG[_CONSOLE] |
vmlinux |
x86_64 |
-M pc |
x86_64_defconfig |
bzImage |
nix run -f boot.nix runners.k26-<arch> boots any cell interactively. This is DISTINCT from
validate-sweep.nix, which picks endianness/width-definite boards (ip22/ip27/omap3430) that build but
have no qemu machine — those prove codegen, boot.nix proves boot; for the hard arches the win was
picking the period-correct model qemu does emulate (64-bit BE malta for mips64eb, a Cortex-A8
realview-pb-a8 for armhf) instead of the endianness-definite board. Four subtleties surfaced by making
it run, not just eyeball it: (1) MIPS endianness/width are Kconfig choices — enabling the wanted symbol
without disabling the default leaves the old -EL/32-bit image; (2) 2.6.31 PowerMac goes silent at
turn off boot console udbg0 unless the escc console is built in (its =m default is a module, and
keep_bootcon postdates 2.6.31); (3) armhf needs its own --with-arch=armv7-a toolchain (else gas
rejects the ARMv7 isb/dsb in cache-v7.S) and a PBA8-only config (realview_defconfig is
multi-board; the v6 boards keep CPU_V6 on and arch/arm/Makefile lets the v6 -march override v7).
The k3 ppc64 gap was a toolchain bug, not a qemu-firmware wall — now closed. Earlier this looked like a
pre-4.x pseries firmware limit. A reference-kernel isolation test disproved that: a known-good Debian jessie
vmlinux-3.16.0-4-powerpc64 (BE, ELF V1, built by gcc 4.8.4) boots cleanly on our exact pinned qemu 8.2.7
with the documented recipe (-M pseries -cpu POWER8, console=hvc0, raw vmlinux) — SLOF loads it,
prom_init runs, reaches the root-fs stage. So qemu/SLOF is fine; the fault was on our build side. The A/B:
our own pseries_defconfig (a) wouldn't compile naturally (-mcall-aixdesc incompatible with -mabi=elfv2),
and (b) with a forced KCFLAGS=-mabi=elfv1 compiled but SLOF-loaded-then-invalid-opcoded. Root cause:
powerpc64-buildroot-linux-musl-gcc -Q --help=target shows -mabi=elfv2 enabled by default — but 3.18's
BE Makefile emits -mcall-aixdesc and assumes an elfv1-default compiler. Fix: a kernel-only gcc 6.5.0 built
as powerpc64-linux (elfv1-default, asm-goto-capable — the ancient k2.6 gcc 4.4.7 can't assemble 3.18's
jump-label/KVM real-mode asm). Now k3-powerpc64 boots full-config on -M pseries -cpu POWER8. ppc64 boots
on k3, k4, and k6.
Two ppc64 items remain, both narrow:
k2.6-powerpc64(build-only,kernels.k26-powerpc64): 2.6.31 predates modern SLOF/PAPR expectations — it can't hand off on-M pseries(and-M mac99/OpenBIOS fails the device-treeclaim). A genuine 2.6.31-vintage limit, distinct from the (now-solved) k3 ABI issue; its codegen is cross-validated by the bootingpowerpc(ppc32) cell.k3-powerpc64le: 3.18 always builds the 32-bit vdso32, whose Makefile passes-mlittle-endian(the Bootlin ppc64le gcc wants-mlittle), and 3.18 has no COMPAT/VDSO32 knob to drop it — a 3.18-era toolchain mismatch. Boots fine on k4/k6.
The sweep drove the boot-image outputs emitted by kernel.nix: ARM's ELF vmlinux entry is a
virtual address that qemu/bootloaders can't jump to before the MMU is on (MIPS boots vmlinux directly
via KSEG0), so each arch also ships the image it actually boots (zImage/bzImage/Image.gz).
Next: (a) the two remaining ppc64 items (above) are narrow — k2.6-powerpc64 is a real 2.6.31-vintage SLOF
handoff limit; k3-powerpc64le is a 3.18 vdso32 toolchain mismatch. Both boot on k4/k6; low priority;
(b) build against a real firmware kernel config (needs the rehosting linux branch + linux_builder);
(c) a buildModule entrypoint to compile out-of-tree modules (e.g. igloo_driver) against a prebuilt
kernel-devel; (d) mirror host + base.
The boot sweep proves specific board defconfigs; this proves buildKernel handles any
.config, not just curated ones. buildKernel gained the machinery a real vendor/firmware
config needs, all general (not tuned to one config):
- Full
.config+ validation — supply a complete config viaconfig; it's normalized (oldconfig/olddefconfig) with cross-version safety nets, since kbuild itself warns that normalizing an old config against a different tree "won't necessarily produce a working kernel" (new and renamed symbols). Every supplied config getsmake listnewconfig(missing new-in-tree symbols),KCONFIG_WARN_UNKNOWN_SYMBOLS(stale/renamed/dropped), and a before/after.configdiff logged;configStrict = truefails the build on symbols unknown to the tree. - Hermetic/reproducible —
KBUILD_BUILD_{USER,HOST,VERSION}fixed +KBUILD_BUILD_TIMESTAMPfromSOURCE_DATE_EPOCH, so/proc/versionand embedded strings are deterministic. capabilityDisable(default[ "GCC_PLUGINS" "SAMPLES" ]) — options a cross/musl kernel-build toolchain fundamentally can't satisfy are auto-dropped if a config enables them, each logged, so a maximal config degrades gracefully instead of dying. Neither is the kernel proper:GCC_PLUGINSare host.sos loaded by the crosscc1(prebuilt cross gcc ≠ host g++ → plugin ABI init failure);SAMPLESbuilds userspace demos that assume glibc headers musl lacks. Caller-overridable.- Config-driven host tools in the build env:
pahole(BTF),zstd/lz4/lzop(kernel/module/ initramfs compression),kmod(depmod → module deps),hexdump(arm64 EFI zboot),mkimage(MIPS FIT images) — inert unless a config selects them.
The goal is not that any maximal config builds — it's that any config that is legitimately
buildable for a given (arch, era) builds. allmodconfig is used as a diagnostic stress test
(every driver/subsystem the arch supports, as modules) to flush out missing host tools and
capability gaps — not as the acceptance bar. Where it doesn't fully build, the cause is that the
request itself isn't buildable with that toolchain's width or that era's source, not a hole in the
capability:
| era (kernel / gcc) | allmodconfig builds | notes |
|---|---|---|
| k6 (6.6 / 13.x) | 11/12 | all but powerpc64le |
| k4 (5.10 / 9.x) | 11/12 | all but powerpc64le (modules install .ko.gz) |
| k3 (3.18 / 6.5) | 10/12 | powerpc64le + x86_64 (a 3.18-era Documentation/vDSO + headers_check quirk) |
| k2.6 (2.6.31 / 4.4) | n/a | allmodconfig isn't meaningful here — building 2.6.31's entire driver surface hits era bugs (e.g. advansys.c dma_cache_sync on ARM, Xen .size asm). Real 2.6.31 configs are small; the boot sweep proves 8/9 arches boot. |
The diagnostic surfaced three categories of finding, each addressed generally: a host tool to add
(hexdump, mkimage), a capability to disable (GCC_PLUGINS, SAMPLES), or an inherent
toolchain/width limit. The last two allmodconfig "misses" fall in the third bucket — they are
unbuildable requests, not failures against the real goal:
allmodconfigis per-kernel-ARCH and width/endian-agnostic — forARCH=powerpcit maximizes toPPC64=y(64-bit). It maps cleanly only to the arch key whose (width, endianness) matches that choice: the 64-bitpowerpc64key passes; the 32-bitpowerpc/powerpclekeys needPPC64=nto test a 32-bit max config (they build fine then — the 32-bit toolchain isn't the problem, and real 32-bit ppc configs build + boot).powerpc64leis the one persistent hole: the pure-64-bit-LE Bootlin toolchain has no 32-bit multilib, soCOMPAT/vdso32and the powerpc boot wrapper (arch/powerpc/boot/crt0.o) can't build. A real toolchain limitation, not a kernelsmith defect — and rehosting uses ppc64levmlinuxdirectly on pseries (no zImage, no 32-bit compat), so it doesn't block the real use case.
Every pinned tarball (mcm components in sources.nix, Bootlin SDKs in bootlin-sources.nix)
is fetched mirror-first, upstream-fallback via mirror.nix: fetchurl { urls = [ <mirror> <upstream> ]; }. The sha256 guarantees identical bytes whichever wins, so the mirror needs no
separate trust. Upstream pins are not "reproducible forever" (GNU prunes old point releases,
Bootlin rotates its download area) — the mirror is the durable copy.
Currently inert: base = null in mirror.nix, so fetches are upstream-only (verified: a
toolchain's store hash is unchanged by the refactor). To turn the mirror on:
MIRROR_DEST=<target> ./mirror-upload.sh— builds.#mirror-tarballs(all 42 tarballs, named by upstream basename) and uploads them. Supportss3://…,user@host:/path, local dirs.- Set
baseinmirror.nixto the HTTP(S) URL serving that directory.
The mirror must serve plain HTTP(S) GETs by basename (S3/MinIO, nginx, release assets) — Harbor's OCI registry can't serve plain tarballs, so use an object store / file host, not the registry API.
Verified gcc floors the resolver clamps to: mainline 8.1 (binutils 2.30); ≥5.1 from Linux 5.15; 4.9 global + 5.1 for arm64 pre-5.15; old kernels practically ≥4.1 (4.3 on several arches; the documented 3.2 is folklore).
Sourcing strategy: vendor-and-pin prebuilt toolchains, do NOT build ancient gcc from source. Building old gcc under modern nixpkgs is fragile (gcc 3.x vs modern glibc).
- Primary: Bootlin prebuilt musl toolchains (open Buildroot recipes),
fetchurl- sha256, mirrored to our own store — upstream pinning is not "reproducible forever".
- Fallback:
musl-cross-make-in-Nix (the currentmk-cross-toolchain.nix) only for bands Bootlin doesn't cover old enough. - kernel.org/crosstool is kernel-build-only (can't build guest userland) → not the sole source.
Caveat — upper bounds are unverified: the cases where a new gcc breaks an old
kernel (asm-goto, PIE defaults, -Werror) are folklore, not cited. Validate each cell's
upper bound empirically; treat the band table as data to correct, not as ground truth.
Bootlin coverage (resolved 2026-06-29): Bootlin archives every release back to 2017.05, all libc flavors incl. musl, for our arches. Oldest gcc = 5.4.0; no gcc 4.x. Modern end reaches gcc 15.1 (2025.08). So:
| Band | Source |
|---|---|
| k6 / k4 / k3 (gcc ≥5.4) | vendor Bootlin musl prebuilt (covers the majority of cells) |
| k2.6 (needs gcc 4.x) | musl-cross-make fallback — Bootlin has nothing below 5.4 |
2.6 is a real target (2.6.31 ARM firmware in the wild), so the fallback band is needed, not hypothetical.
Per-arch Bootlin coverage (k6 band, found while pinning): 9 of 12 arches map cleanly to a modern Bootlin musl SDK (armel, armhf, arm64, mipseb, mipsel, powerpc, powerpc64, powerpc64le, x86_64). Three need musl-cross-make even for modern bands:
powerpcle— Bootlin has no 32-bit powerpc little-endian variant at all.mips64el— Bootlin only ships it as n32 ABI (mips64el-n32), not the n64 our target wants.mips64eb— themips64dir has musl only at 2017.05 (gcc 5.4); newer releases dropped musl.
So musl-cross-make owns: all of k2.6 (12 arches) + the modern bands of those 3 arches.
Verify Bootlin's gcc-version coverage for the old (2.6/3.x) bands.✅ done above.- Pivot
sources.nix→ Bootlinfetchurlentries (mirror to Harbor) for k3/k4/k6;mk-cross-toolchain.nix→ unpack +patchelfderivation. Keep the musl-cross-make path for k2.6 only. - Prove one cell end-to-end (the
spike), then widen the matrix.