Same-arch cross-compile linker mixup

TL;DR: Host rust in a same-arch cross-build environment insists on using the same linker for the target triple as it does for the host build-script commands but the target libc isn't the host libc, etc, and it fails

How do I persuade cargo/rust to use one linker for the target but another for the build-script commands when they both have the same triple?

The gory detail:

I'm building using host Rust tools in a cross-build environment. It can cross-build for x86_64-systemq-linux or aarch64-systemq-linux where systemq is really just a generic linux distro. I'm invoking Cargo from a makefile of a bigger project which now has some parts in rust.

In the SDK environment, CC, LD, CROSS_COMPILE etc are all set, and may contain parameters, e.g.

LD=aarch64-systemq-linux-ld --sysroot=/build/RUST/test/sdk/sysroots/aarch64-systemq-linux

Because of issues probably related to this Automatically detect the appropriate linker to use when cross-compiling · Issue #4133 · rust-lang/cargo · GitHub I find that dylib crate-types won't link to produce the .so because rust is trying to link to host libraries. I fixed this by passing to cargo the linker to use, from $LD environment variable, which is set by the cross-environment.

Because of reasons rust won't accept any arguments to the linker name, so I also have to make a linker wrapper in the makefile of the project invoking Cargo:

I map the rust target from the cross compile target to convert x86_64-systemq-linux- to x86_64-unknown-linux-gnu

RUST_TARGET=$(subst -systemq-,-unknown-,$(CROSS))gnu

And then I produce the linker wrapper so that arguments in $LD take effect

RUSTLD=rust-$(strip $(notdir $(firstword $(LD))))
printf '%s\n%s\n' '#! /bin/bash' 'exec $$LD "$$@"' > "$$PWD/$(RUSTLD)" && chmod +x "$$PWD/$(RUSTLD)"

and now I can invoke Cargo like this:

$(CARGO) build $(RUST_BUILD_FLAGS) --target-dir $(RUST_TARGET_DIR) --target $(RUST_TARGET) --manifest-path $(<D)/Cargo.toml --config target.$(RUST_TARGET).linker=\"$$PWD/$(RUSTLD)\"

and it works -- but only for arm.

When building for x86_64-systemq-linux (for which RUST_TARGET becomes x86_64-unknown-linux-gnu) it seems that cargo/Rust assumes there is no cross-compiling, and so it tries to use the linker specified on the command line to link the build-script commands as well as the target binaries, which of course fails.

The build-script commands need to run in the host environment which might be the same architecture as the target environment but that's all, it doesn't have the same runtime libraries.

The failure is along the lines of:

= note: x86_64-systemq-linux-ld: cannot find -lgcc

I have tried using $(CC) to link (deriving a wrapper in a similar way) but then it fails with:

error: failed to run custom build command for quote v1.0.21
Caused by:
could not execute process.../rust/release/build/quote-07b2e30f991f4ba0/build-script-build (never executed)
Caused by:
No such file or directory (os error 2)

which likely has the same ultimate cause

So the question is, how do I specify a linker to be used for the target but not for the host tools?

(And it would be nice if Cargo/Rust honoured the $(LD) variable complete with spaces, to save me messing about with wrappers)

This seems relevant: change musl linker to /lib/ld-musl-x86_64.so.1 or support -dynamic-linker option · Issue #40049 · rust-lang/rust · GitHub

Angles of attack:

  • first build with no linker specified which will fail but at least get the build-scripts compiled. Then build again with the right linker. Is there a way to tell cargo only to build the build-script commands? Maybe have the linker wrapper heuristically tell what it is building?
  • build rust for a different triple to use in the cross-environment, perhaps with systemq instead of unknown
  • hope for finer configuration controls

I'm using stable rustc 1.64.0 (a55dd71d5 2022-09-19)

Thanks for reading this far.

A satisfactory work-around seems to be this linker wrapper, using gcc as the fall-back linker for build_script_build targets. The wrapper name ends in -cc so that rust knows the C compiler is called as the linker.

#! /bin/bash
grep -q 'build_script_main\|build_script_build' <<<"$*" && exec gcc "$@" || exec ${TARGET_CC:-${CC}} "$@"

and then pass it to cargo with: --config target.$(RUST_TARGET).linker=\"my-hacky-linker-cc\"

If build_script_... occurs in the arguments then plain gcc is called, otherwise $(CC) is called.