Kernel toolchains#
Building the kernel needs a toolchain that satisfies the kernel’s own
requirements, and on Nix that takes more care than passing a single flag. Two
toolchains have non-obvious wiring: Clang/LLVM, where the nixpkgs cc-wrapper
fights the kernel’s include handling, and Rust, where the supported version
window moves with the kernel version. This page records the durable recipe for
each. The Clang/LLVM recipe is implemented in the build-kernel devShell of
vendor/nixos-flake and consumed by f/kernel/build_flags.py; the Rust
requirements drive both that devShell and any per-kernel toolchain selection.
Clang and LLVM#
The kernel build selects an LLVM toolchain with make LLVM=1, which puts
clang and the llvm-* binutils on the command line. With the nixpkgs
clang toolchain, LLVM=1 on its own does not build the kernel: the wrapper
that nixpkgs places around clang injects flags the kernel rejects, and the
unwrapped compiler underneath it loses the include paths the kernel needs. The
recipe below sets each tool explicitly so both target objects and host tools
build cleanly.
Why LLVM=1 alone is not enough#
The nixpkgs cc-wrapper exists so that a bare clang redirects to Nix’s own
libc headers. It does this by adding -nostdlibinc to every invocation. The
kernel, however, compiles with -nostdinc and
-Werror=unused-command-line-argument, so the wrapper’s -nostdlibinc is
flagged as unused and becomes a hard error on the very first object
(scripts/mod/empty.o). This hits both the target compiler (CC) and the
host compiler (HOSTCC):
clang: error: argument unused during compilation: '-nostdlibinc'
[-Werror,-Wunused-command-line-argument]
Switching to the unwrapped clang (llvmPackages.clang-unwrapped) removes
the injected -nostdlibinc, so target objects compile, but it trades one
failure for two more:
Host tools built with the unwrapped compiler link against the wrong libc and fail. For example
tools/objtoollinkslibelfand the link aborts withundefined reference to '__isoc23_strtol@GLIBC_2.38', because the unwrappedclangdoes not wire in Nix’s glibc.The kernel’s
-nostdincstrips the unwrapped compiler’s own resource directory, so the target build can no longer findstdarg.h,stddef.h, and the other builtin headers, which the wrapper would normally re-add.
Demoting the warning (for example with
KCFLAGS=-Wno-unused-command-line-argument) is whack-a-mole: silencing
-nostdlibinc only surfaces the next unused argument
(-Wa,--compress-debug-sections), and never reaches a clean build. The fix is
to set each tool to the right variant rather than to suppress diagnostics.
The proven recipe#
This mirrors what the nixpkgs kernel build itself does in
pkgs/os-specific/linux/kernel/common-flags.nix, which notably does not use
LLVM=1 and instead sets each tool by hand:
CCis the unwrappedclang. Dropping the wrapper drops the-nostdlibincthe kernel rejects.HOSTCCandHOSTCXXare the wrappedclang(theLLVM=1default), so host tools still link against Nix’s glibc.CFLAGS_KERNELandCFLAGS_MODULEeach carry-I$("$CC" -print-resource-dir)/includeto restore the builtin headers that-nostdincstrips. On the unwrappedclang,-print-resource-diralready returns the populatedliboutput (containingstdarg.hand the rest), so there is no need to computelib.getLibpaths by hand. Using the compiler’s own-print-resource-diravoids the trap of pointing at the emptyoutresource directory.LDis the rawld.lld, andAR,NM,OBJCOPYand the rest of the binutils are thellvm-*tools. These come for free fromLLVM=1because the toolchain already puts the rawlldandllvmbinaries onPATH.
The resulting target invocation is:
make ... LLVM=1 CC=<unwrapped-clang> \
CFLAGS_KERNEL=-I<resource>/include \
CFLAGS_MODULE=-I<resource>/include
A full defconfig builds clean under this recipe, and make rustavailable
reports that Rust is available (see Rust). Building Rust under Clang needs
one extra flag:
BINDGEN_EXTRA_CLANG_ARGS=-Wno-unused-command-line-argument
bindgen probes the wrapped clang on PATH for its default include paths
and captures its -nostdlibinc; libclang then rejects that as an unused
argument, and because bindgen treats any clang diagnostic as fatal the build
dies at the RUSTC/bindgen step. Silencing that one warning for bindgen alone
fixes it.
How the devShell exports it#
The store paths above are Nix-internal, so the unwrapped compiler and its
resource directory cannot be computed by code that runs outside the devShell.
The build-kernel devShell in vendor/nixos-flake therefore exports them
as environment variables (KERNEL_CLANG_CC for the unwrapped clang and
KERNEL_CLANG_RESOURCE for its resource-include directory), computed in Nix
from llvmPackages.clang-unwrapped.
The f/kernel/build_flags.py step reads those two variables through the
devShell when compiler=clang and splices LLVM=1 together with the
unwrapped CC and the CFLAGS_KERNEL/CFLAGS_MODULE resource includes
into the single make-flags string that every make step (configure, compile,
devtools, install) consumes. The kernel requires the same LLVM= value on
each make invocation when configuring and building through separate commands
(Documentation/kbuild/llvm.rst), so producing the flags once keeps the
toolchain consistent across steps. ccache composes naturally:
CC="ccache <unwrapped-clang>" for the target, leaving HOSTCC wrapped.
QEMU under Clang#
QEMU is the easy case and needs no special recipe. It is ordinary userspace and
does not use -nostdinc, so the wrapped clang is the correct choice: it
redirects to Nix’s libc, which QEMU links against, and none of the kernel’s
three blockers apply. f/qemu/configure.py pins
--cc=clang --cxx=clang++ with -Qunused-arguments to drop the GCC-only
-Wa,--compress-debug-sections that clang sees as unused on link.
Rust#
Building a kernel with CONFIG_RUST=y needs a Rust toolchain inside the
kernel’s supported version window, and that window moves with the kernel
version. This chapter records where the requirements are stated, how to
cross-match a kernel to a toolchain, what the build-kernel devShell
provides, and how to pin an older toolchain, so that a “bump the kernel” or
“bump the toolchain” task does not have to re-derive any of it.
Where the requirements are stated#
Check these for the exact kernel ref you intend to build; they differ between versions. All but the last are in the kernel source tree, in rough priority order:
scripts/min-tool-version.shThe canonical, machine-readable source. It echoes the enforced minimum
rustc,bindgen,llvm(and GCC) versions per tool, and is what the build actually checks.scripts/rust_is_available.shThe gate behind
make rustavailable. It readsmin-tool-version.sh, probes the toolchain, and exits non-zero (*** Rust compiler '...' is too old) if a tool is below the minimum, which makes Kconfig setRUST_IS_AVAILABLE=n.Documentation/process/changes.rstThe human-readable “Minimal requirements” table (Rust, bindgen, GNU C, Clang/LLVM), a mirror of the enforced minimums.
Documentation/rust/quick-start.rstThe setup guide: install instructions plus the recommended toolchain and the
RUST_LIB_SRCwiring.
The Rust for Linux rust-version-policy is the why behind the moving window:
the minimum tracks Debian Stable’s rustc and advances roughly per Debian
release, while there is no hard maximum, since new releases are CI-tested and
have worked with every version since the minimum. bindgen is CI-tested with no
separate written policy; rustfmt is not version-gated, as it is optional
formatting and effectively tracks the rustc version.
Reading it for a given ref, in a kernel checkout:
scripts/min-tool-version.sh rustc # -> e.g. 1.85.0
scripts/min-tool-version.sh bindgen # -> e.g. 0.71.1
scripts/min-tool-version.sh llvm # -> e.g. 15.0.0
That minimum, plus “no hard maximum but newer is CI-tested”, is the whole requirement.
Cross-matching a kernel to a toolchain#
The minimums below come from scripts/min-tool-version.sh at each ref. The
“custom-target gate” is whether the core Rust build path passes
-Zunstable-options, which rustc 1.85 and newer need to load the kernel’s
custom target JSON (older rustc loaded it under RUSTC_BOOTSTRAP=1
alone). That single fact decides whether a newer rustc works.
Kernel |
min rustc |
min bindgen |
-Zunstable? |
works with 1.95? |
|---|---|---|---|---|
v6.11 - v6.18 |
1.78.0 |
0.65.1 |
no |
no |
v7.1+ |
1.85.0 |
0.71.1 |
yes |
yes |
The consequences are that no single rustc serves both ends: v6.18 needs
rustc in roughly the [1.78, 1.84] range, before rustc tightened
custom-target loading, while v7.1 and newer need 1.85 or later and tolerate
1.95. A toolchain good for one silently fails the other. The minimum bump is the
trigger: rustc minimum went 1.78 to 1.85 at v7.1 (the Debian Stable cadence)
and bindgen minimum went 0.65.1 to 0.71.1, so always re-read
min-tool-version.sh at the new ref before bumping the kernel.
To extend the table for a new ref, read its min-tool-version.sh (rustc
plus bindgen) and check rust/Makefile and scripts/Makefile.build
cmd_rustc_library for -Zunstable-options on the core path.
A too-old toolchain builds without Rust, silently#
If rustc is below the kernel’s minimum, rust_is_available.sh fails,
Kconfig sets RUST_IS_AVAILABLE=n, and any preset with CONFIG_RUST=y is
silently downgraded: alldefconfig and olddefconfig drop it and the build
succeeds without Rust. Confirm that Rust actually built by checking that the
resulting config still carries CONFIG_RUST=y:
grep -E '^CONFIG_RUST(_IS_AVAILABLE)?=' <build_dir>/.config
What the devShell provides#
The build-kernel devShell in vendor/nixos-flake (its matrixExtras)
ships nixpkgs’ own rustc, rust-bindgen, and rustfmt, plus
rustPlatform.rustLibSrc exported as RUST_LIB_SRC so the kernel builds
core and alloc from source. On nixos-26.05 that is rustc 1.95.0 with
bindgen 0.72.1, which is inside the v7.1 and newer window, so it suits modern
kernels (v7.1 and later).
Pinning an older toolchain#
To build a pre-v7.1 kernel with Rust, pin an older rustc (with matching
rust-src and a compatible bindgen) into the build-kernel devShell. The
proven approach is a rust-overlay input:
# flake.nix
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
# apply rust-overlay.overlays.default in the pkgs import, then:
# lib/toolchain.nix
rustForKernel = pkgs.rust-bin.stable."1.80.0".default.override {
extensions = [ "rust-src" ];
};
# use rustForKernel in matrixExtras; RUST_LIB_SRC =
# "${rustForKernel}/lib/rustlib/src/rust/library";
This was in place briefly for kernel 6.18 before the project moved to modern kernels. Selecting the pin automatically from the kernel version range, rather than editing the flake by hand, is the goal of the planned per-kernel Rust requirements work.