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

From: Yoann Congal

Date: Fri Apr 17 2026 - 04:02:56 EST


On Thu Apr 16, 2026 at 11:15 PM CEST, Mohamad Alsadhan 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:
>
> error: multiple input filenames provided ...
>
> Generate a small wrapper script and pass it to -Clinker e.g.
>
> ```
> #!/bin/sh
> exec ccache gcc "$@"
> ```
>
> This fix should be general enough to address most if not all cases
> (incl. wrappers or subcommands) and avoids surprises of simpler fixes
> like just defaulting to gcc.
>
> This avoids passing the user command as an environment variable as
> that would be more challenging to trace and debug shell expansions.
>
> Link: https://github.com/Rust-for-Linux/linux/issues/1224
> Suggested-by: Yoann Congal <yoann.congal@xxxxxxxx>
> Signed-off-by: Mohamad Alsadhan <mo@xxxxxxx>

This does work on my ccache-enabled Yocto build, so:
Tested-by: Yoann Congal <yoann.congal@xxxxxxxx>

Thanks!

> ---
> v6 -> v7:
> - Always generate `scripts/rustc-wrapper` when `CONFIG_RUST=y`,
> including cases with no Rust host programs in `scripts/`.
> - Use `$(obj)/rustc-wrapper` for host Rust programs, fixing builds
> outside `scripts/`, e.g. `samples/rust/hostprogs/`.
> - remove leading exec (again) to allow leading env vars e.g.
> HOSTRUSTC_LD="VAR=VAL ccache gcc".
> - Track, clean and ignore generated wrapper as needed.
>
> v5 -> v6:
> - Add fix to `rust/Makefile` as well (Yoann)
> - Include script to `.gitignore` and make clean (Nicolas)
> - Add back the outer `exec` to the command
>
> v4 -> v5:
> - Fix word splitting issues
> - Remove unnecessary `exec sh -c` and simplify generated script
>
> v3 -> v4:
> - Use filechk instead of if_changed macro to regenerate script
> - Remove trailing space at EOL
>
> v2 -> v3:
> - Scrap previous hacky approaches (e.g. using lastword) and go with
> a proper fix (Gary) which turned out not that complex to
> implement.
>
> v1 -> v2:
> - Rename HOSTRUSTC_LINKER to HOSTRUSTC_LD for consistency
> - Introduce explicit HOSTRUSTC_LD override
> - Warn when falling back due to multi-argument HOSTCC
> - Error out if a user-specified HOSTRUSTC_LD is not an executable
>
> v1: https://lore.kernel.org/all/20260225102819.16553-1-mo@xxxxxxx/
> v2: https://lore.kernel.org/all/20260227132713.23106-1-mo@xxxxxxx/
> v3: https://lore.kernel.org/all/20260312002852.11292-1-mo@xxxxxxx/
> v4: https://lore.kernel.org/all/20260317112021.14353-1-mo@xxxxxxx/
> v5: https://lore.kernel.org/all/20260321150034.9915-1-mo@xxxxxxx/
> v6: https://lore.kernel.org/all/20260331000802.380-1-mo@xxxxxxx/
> ---
> Makefile | 3 ++-
> rust/Makefile | 8 +++++---
> samples/rust/hostprogs/.gitignore | 1 +
> samples/rust/hostprogs/Makefile | 2 ++
> scripts/.gitignore | 1 +
> scripts/Makefile | 3 +++
> scripts/Makefile.host | 26 +++++++++++++++++++++++---
> 7 files changed, 37 insertions(+), 7 deletions(-)
>
> diff --git a/Makefile b/Makefile
> index 1a219bf1c..5ebeef67f 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1651,7 +1651,8 @@ CLEAN_FILES += vmlinux.symvers modules-only.symvers \
> modules.builtin.ranges vmlinux.o.map vmlinux.unstripped \
> compile_commands.json rust/test \
> rust-project.json .vmlinux.objs .vmlinux.export.c \
> - .builtin-dtbs-list .builtin-dtb.S
> + .builtin-dtbs-list .builtin-dtb.S \
> + scripts/rustc-wrapper
>
> # Directories & files removed with 'make mrproper'
> MRPROPER_FILES += include/config include/generated \
> diff --git a/rust/Makefile b/rust/Makefile
> index 5eca6a817..6bc761a31 100644
> --- a/rust/Makefile
> +++ b/rust/Makefile
> @@ -565,7 +565,7 @@ $(obj)/libsyn.rlib: $(src)/syn/lib.rs $(obj)/libquote.rlib FORCE
> quiet_cmd_rustc_procmacro = $(RUSTC_OR_CLIPPY_QUIET) P $@
> cmd_rustc_procmacro = \
> $(RUSTC_OR_CLIPPY) $(rust_common_flags) $(rustc_target_flags) \
> - -Clinker-flavor=gcc -Clinker=$(HOSTCC) \
> + -Clinker-flavor=gcc -Clinker=scripts/rustc-wrapper \
> -Clink-args='$(call escsq,$(KBUILD_PROCMACROLDFLAGS))' \
> --emit=dep-info=$(depfile) --emit=link=$@ --extern proc_macro \
> --crate-type proc-macro -L$(objtree)/$(obj) \
> @@ -576,12 +576,14 @@ quiet_cmd_rustc_procmacro = $(RUSTC_OR_CLIPPY_QUIET) P $@
> $(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/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/rustc-wrapper FORCE
> +$(call if_changed_dep,rustc_procmacro)
>
> # `rustc` requires `-Zunstable-options` to use custom target specifications
> diff --git a/samples/rust/hostprogs/.gitignore b/samples/rust/hostprogs/.gitignore
> index a6c173da5..d88a75790 100644
> --- a/samples/rust/hostprogs/.gitignore
> +++ b/samples/rust/hostprogs/.gitignore
> @@ -1,3 +1,4 @@
> # SPDX-License-Identifier: GPL-2.0
>
> single
> +rustc-wrapper
> diff --git a/samples/rust/hostprogs/Makefile b/samples/rust/hostprogs/Makefile
> index 8ddcbd741..9c9663a77 100644
> --- a/samples/rust/hostprogs/Makefile
> +++ b/samples/rust/hostprogs/Makefile
> @@ -3,3 +3,5 @@
> hostprogs-always-y := single
>
> single-rust := y
> +
> +clean-files += rustc-wrapper
> diff --git a/scripts/.gitignore b/scripts/.gitignore
> index 4215c2208..b3948b148 100644
> --- a/scripts/.gitignore
> +++ b/scripts/.gitignore
> @@ -6,6 +6,7 @@
> /kallsyms
> /module.lds
> /recordmcount
> +/rustc-wrapper
> /rustdoc_test_builder
> /rustdoc_test_gen
> /sign-file
> diff --git a/scripts/Makefile b/scripts/Makefile
> index 0941e5ce7..287421322 100644
> --- a/scripts/Makefile
> +++ b/scripts/Makefile
> @@ -29,6 +29,9 @@ generate_rust_target-rust := y
> rustdoc_test_builder-rust := y
> rustdoc_test_gen-rust := y
>
> +always-$(CONFIG_RUST) += rustc-wrapper
> +clean-files += rustc-wrapper
> +
> HOSTCFLAGS_tracepoint-update.o = -I$(srctree)/tools/include
> HOSTCFLAGS_elf-parse.o = -I$(srctree)/tools/include
> HOSTCFLAGS_sorttable.o = -I$(srctree)/tools/include
> diff --git a/scripts/Makefile.host b/scripts/Makefile.host
> index c1dedf646..84fd87a53 100644
> --- a/scripts/Makefile.host
> +++ b/scripts/Makefile.host
> @@ -87,11 +87,30 @@ hostcxx_flags = -Wp,-MMD,$(depfile) \
> $(KBUILD_HOSTCXXFLAGS) $(HOST_EXTRACXXFLAGS) \
> $(HOSTCXXFLAGS_$(target-stem).o)
>
> +# 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 wrapper script that forwards arguments to
> +# `HOSTRUSTC_LD` so that such commands can be used safely.
> +#
> +# Set `HOSTRUSTC_LD` for a different rustc linker command than `HOSTCC`
> +HOSTRUSTC_LD ?= $(HOSTCC)
> +
> +define filechk_rustc-wrapper
> + printf "%s\n" \
> + '#!/bin/sh' \
> + '$(call escsq,$(HOSTRUSTC_LD)) "$$@"'
> +endef
> +
> +$(obj)/rustc-wrapper: FORCE
> + $(call filechk,rustc-wrapper)
> + $(Q)chmod +x $@
> +
> # `--out-dir` is required to avoid temporaries being created by `rustc` in the
> # 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=$(obj)/rustc-wrapper \
> -Clink-args='$(call escsq,$(KBUILD_HOSTLDFLAGS))' \
> $(KBUILD_HOSTRUSTFLAGS) $(HOST_EXTRARUSTFLAGS) \
> $(HOSTRUSTFLAGS_$(target-stem))
> @@ -153,10 +172,11 @@ $(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 $(obj)/rustc-wrapper FORCE
> +$(call if_changed_dep,host-rust)
>
> -targets += $(host-csingle) $(host-cmulti) $(host-cobjs) \
> +targets += $(obj)/rustc-wrapper \
> + $(host-csingle) $(host-cmulti) $(host-cobjs) \
> $(host-cxxmulti) $(host-cxxobjs) $(host-rust)
>
> # %.lex.o <- %.lex.c <- %.l


--
Yoann Congal
Smile ECS