Re: [PATCH v8] kbuild: host: use single executable for rustc -C linker

From: Nicolas Schier

Date: Wed Jun 17 2026 - 15:44:15 EST


On Wed, Jun 10, 2026 at 03:18:54PM +0200, Miguel Ojeda wrote:
> On Sat, May 9, 2026 at 12:20 PM Mohamad Alsadhan <mo@xxxxxxx> wrote:
> >
> > rustc's -C linker= option expects a single executable path. When
> > HOSTCC contains a wrapper (e.g. "ccache gcc"), passing
> > `-Clinker=$(HOSTCC)` results in the shell splitting the value into
> > multiple words, and rustc interprets the additional word as an
> > input filename:
>
> I have been taking a look at this, and considered applying it since
> Kbuild is OK with it (thanks a lot for all the work during the
> different versions), but I am not sure if the following bits are all
> intended:
>
> - Shouldn't `HOSTRUSTC_LD` be documented in `Documentation/kbuild/kbuild.rst`?

Good catch, thanks. Yes.


> - Why do we do both `clean-files` and `CLEAN_FILES`?
>
> + In fact, should we do it on `clean` or `mrproper`? Nicolas
> originally suggested `MRPROPER_FILES`, but this is on `CLEAN_FILES`.
> But more on that below, since I guess it depends on how we treat
> out-of-tree modules...
>
> - Was this tested with an out-of-tree module? I am asking because:
>
> + It does create an unused wrapper in a `scripts/` folder in the
> out-of-tree module directory (i.e. the one used is the in-tree one) --
> is that intended?
>
> + If we remove the wrapper during `clean` as the patch currently
> does, then it means we cannot build Rust host programs in an
> out-of-tree module (because it uses the in-tree one). Should it be in
> `mrproper` instead, or should we generate a per-out-of-tree-module
> one?
>
> While I think the kernel generally expects that the same
> toolchain is used for both the main build and out-of-tree modules (it
> may happen to work otherwise, but as a policy it is not supposed to be
> supported, or at least that is what I recall I was told), I am not
> sure if it applies to host programs. I guess someone may want to use a
> different host toolchain vs. the one used to build the main kernel,
> and I guess things would generally work.
>
> - The `filechk` could fail if we use Rust host programs in more
> folders later on, i.e. if two submakes run it at the same time, and
> one at the end deletes the (shared) temporary, then the other will
> fail if it was in the middle of updating it.


Uh, thanks for all those good questions. I didn't even think about rust
host progs being built in out-of-tree module trees. And the concurrent
filechk calls are really no good.


Iff HOSTRUSTC_LD shall be kept stable for external kernel modules, a
semi-simple solution might be to move rustc-wrapper rules to
scripts/basic/ and use cmd_* instead of filechk. Regarding the
queations above, this would mean:


* ${KBUILD_OUTPUT}/scripts/basic/rustc-wrapper will be purged during
'mrproper'

* It will be generated only once, no matter who many in-tree folder
use it.


Only roughly tested:


diff --git a/Makefile b/Makefile
index 8235b6a9b3cc..e3a82c344e69 100644
--- a/Makefile
+++ b/Makefile
@@ -661,6 +661,7 @@ export RCS_FIND_IGNORE := \( -name SCCS -o -name BitKeeper -o -name .svn -o \
PHONY += scripts_basic
scripts_basic: KBUILD_HOSTCFLAGS := $(KBUILD_HOSTCFLAGS)
scripts_basic: KBUILD_HOSTLDFLAGS := $(KBUILD_HOSTLDFLAGS)
+scripts_basic: HOSTRUSTC_LD := $(HOSTRUSTC_LD)
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic

diff --git a/rust/Makefile b/rust/Makefile
index 2fbdebb93bf2..d02002c50432 100644
--- a/rust/Makefile
+++ b/rust/Makefile
@@ -593,7 +593,7 @@ quiet_cmd_rustc_procmacro = $(if $(skip_clippy),RUSTC,$(RUSTC_OR_CLIPPY_QUIET))
cmd_rustc_procmacro = \
$(rustc_target_envs) \
$(if $(skip_clippy),$(RUSTC),$(RUSTC_OR_CLIPPY)) $(rust_common_flags) $(rustc_target_flags) \
- -Clinker-flavor=gcc -Clinker=$(HOSTCC) \
+ -Clinker-flavor=gcc -Clinker=$(objtree)/scripts/basic/rustc-wrapper \
-Clink-args='$(call escsq,$(KBUILD_PROCMACROLDFLAGS))' \
--emit=dep-info=$(depfile) --emit=link=$@ --extern proc_macro \
--crate-type proc-macro -L$(objtree)/$(obj) \
@@ -610,12 +610,14 @@ $(obj)/$(libzerocopy_derive_name): $(src)/zerocopy-derive/lib.rs $(obj)/libproc_
$(obj)/$(libmacros_name): private rustc_target_flags = \
--extern proc_macro2 --extern quote --extern syn
$(obj)/$(libmacros_name): $(src)/macros/lib.rs $(obj)/libproc_macro2.rlib \
- $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
+ $(obj)/libquote.rlib $(obj)/libsyn.rlib \
+ scripts/basic/rustc-wrapper FORCE
+$(call if_changed_dep,rustc_procmacro)

$(obj)/$(libpin_init_internal_name): private rustc_target_flags = $(pin_init_internal-flags)
$(obj)/$(libpin_init_internal_name): $(src)/pin-init/internal/src/lib.rs \
- $(obj)/libproc_macro2.rlib $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
+ $(obj)/libproc_macro2.rlib $(obj)/libquote.rlib $(obj)/libsyn.rlib \
+ scripts/basic/rustc-wrapper FORCE
+$(call if_changed_dep,rustc_procmacro)

# `rustc` requires `-Zunstable-options` to use custom target specifications
diff --git a/scripts/Makefile.host b/scripts/Makefile.host
index c1dedf646a39..0be405efa38a 100644
--- a/scripts/Makefile.host
+++ b/scripts/Makefile.host
@@ -91,7 +91,8 @@ hostcxx_flags = -Wp,-MMD,$(depfile) \
# current working directory, which may be not accessible in the out-of-tree
# modules case.
hostrust_flags = --out-dir $(dir $@) --emit=dep-info=$(depfile) \
- -Clinker-flavor=gcc -Clinker=$(HOSTCC) \
+ -Clinker-flavor=gcc \
+ -Clinker=$(objtree)/scripts/basic/rustc-wrapper \
-Clink-args='$(call escsq,$(KBUILD_HOSTLDFLAGS))' \
$(KBUILD_HOSTRUSTFLAGS) $(HOST_EXTRARUSTFLAGS) \
$(HOSTRUSTFLAGS_$(target-stem))
@@ -153,7 +154,7 @@ $(host-cxxobjs): $(obj)/%.o: $(obj)/%.cc FORCE
quiet_cmd_host-rust = HOSTRUSTC $@
cmd_host-rust = \
$(HOSTRUSTC) $(hostrust_flags) --emit=link=$@ $<
-$(host-rust): $(obj)/%: $(src)/%.rs FORCE
+$(host-rust): $(obj)/%: $(src)/%.rs scripts/basic/rustc-wrapper FORCE
+$(call if_changed_dep,host-rust)

targets += $(host-csingle) $(host-cmulti) $(host-cobjs) \
diff --git a/scripts/basic/.gitignore b/scripts/basic/.gitignore
index 07c195f605a1..d314c04fe131 100644
--- a/scripts/basic/.gitignore
+++ b/scripts/basic/.gitignore
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
/fixdep
/randstruct.seed
+/rustc-wrapper
diff --git a/scripts/basic/Makefile b/scripts/basic/Makefile
index fb8e2c38fbc7..0aec1adc199b 100644
--- a/scripts/basic/Makefile
+++ b/scripts/basic/Makefile
@@ -19,3 +19,25 @@ always-$(CONFIG_RANDSTRUCT) += randstruct.seed
$(obj)/../../include/generated/integer-wrap.h: $(srctree)/scripts/integer-wrap-ignore.scl FORCE
$(call if_changed,touch)
always-$(CONFIG_UBSAN_INTEGER_WRAP) += ../../include/generated/integer-wrap.h
+
+# rustc-wrapper: rustc's `-Clinker=` expects a single executable path, not a
+# command line. `HOSTCC` may be a multi-word command when wrapped (e.g.
+# "ccache gcc"), which would otherwise be split by the shell and mis-parsed by
+# rustc. To work around this, we generate a script that invokes `HOSTRUSTC_LD`
+# with the linker arguments appended so such commands can be used safely.
+#
+# Set `HOSTRUSTC_LD` for a different rustc linker command than `HOSTCC`
+HOSTRUSTC_LD ?= $(HOSTCC)
+
+quiet_cmd_rustc-wrapper = GEN $@
+ cmd_rustc-wrapper = \
+ printf '%s\n' '\#!/bin/sh' '$(addsuffix $(space),$(HOSTRUSTC_LD))"$$@"' >$@; \
+ chmod a+x $@
+
+$(obj)/rustc-wrapper: FORCE
+ $(call if_changed,rustc-wrapper)
+
+always-$(CONFIG_RUST) += rustc-wrapper
+ifneq ($(CONFIG_RUST),)
+targets += rustc-wrapper
+endif


--
Nicolas