Statically link glibc in Rust binary

I am trying to create a derivation to compile my Rust project with static linking to glibc. This works in cargo-land by adding RUSTFLAGS="-C target-feature=+crt-static".

In my derivation (which is in a flake), I have packages.default using pkgs.rustPlatform.buildRustPackage.

I set it up how I usually do for other projects and it works as long as I don’t add the flag above.

However, when using the above flag and during linking, I get the following errors:

       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lrt: No such file or directory
       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lpthread: No such file or directory
       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lm: No such file or directory
       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -ldl: No such file or directory
       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lc: No such file or directory
       >           /nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lc: No such file or directory
       >           collect2: error: ld returned 1 exit status

I tried to do the following:

  • Add pkg-config to nativeBuildInputs (this is true for all cases)
  • Add rustPlatform.bindgenHook to nativeBuildInputs
  • Add clang and/or libclang to nativeBuildInputs and/or buildInputs
  • Add glibc or glibc.static to nativeBuildInputs and/or buildInputs
  • Add override { stdenv = pkgs.clangStdenv; } to buildRustPackage
  • Add LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.clang pkgs.libclang pkgs.glibc.static ] to the inputs of buildRustPackage.

But none of those seem to solve my problem.

Any idea how I can achieve this?

What does your flake look like here? Can you share some sort of minimal example that I can replicate this with?

Hey @iFreilicht yes I can, here is an example repro GitHub - beeb/rust-nix-static

Hmm, that builds just fine on my machine:

$ clone git@github.com:beeb/rust-nix-static.git
Cloning into 'rust-nix-static'...
[...]
Resolving deltas: 100% (6/6), done.
$ cd rust-nix-static
$ nix build
$ file result/bin/rust-nix-static
result/bin/rust-nix-static: Mach-O 64-bit executable arm64

What version of Nix are you running? And what system/arch are you on?

I’m on Linux on x86 (Ubuntu LTS), I see you are on mac aarch64 so it’s probably not the best combo to experience my problem.

Ah ok! I have a Linux x86 machine as well, I’ll try again on that one tomorrow. This did yield a useful datapoint, though.

1 Like

I managed to reproduce in Docker so I added a Dockerfile that shows the problem.

As far as I know, Glibc can’t be statically linked to your Rust program, only Musl can.

You’ll need to use the Musl stdenv to statically build your hello world program:

diff --git a/flake.nix b/flake.nix
index 18cce92..cc16cec 100644
--- a/flake.nix
+++ b/flake.nix
@@ -36,7 +36,7 @@
           RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
         };
 
-        packages.default = pkgs.rustPlatform.buildRustPackage {
+        packages.default = pkgs.pkgsMusl.rustPlatform.buildRustPackage {
           pname = "rust-nix-static";
           inherit ((lib.importTOML ./Cargo.toml).package) version;

[Edit]: note: you’ll likely have to build llvm and the full rust toolchain. You’ll likely want to set up a binary cache for your project.

Ah that’s a good point! I guess the binary appears to be statically linked but will still call out to glibc and would not run on a machine with an older glibc? @beeb did you test that?

I have almost 0 darwin experience, so I’m not sure what’s happening in your stdenv.

I guess the binary appears to be statically linked but will still call out to glibc and would not run on a machine with an older glibc?

I don’t think so. If you managed to build this from Darwin, it’s likely static for real. My huntch would be Darwin’s default libc can be statically linked to Rust programs, unlike Glibc. Maybe somebody reading this more familiar with Darwin could enlighten us.

Nope, still dynamic:

$ otool -L result/bin/rust-nix-static
result/bin/rust-nix-static:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.60.1)
	/nix/store/02jndc2wwsfigf12wv53k8zwyfvwl9gf-libiconv-50/lib/libiconv.dylib (compatibility version 7.0.0, current version 7.0.0)

I dug deeper into this, and found the official rust docs on static linkage, which say (emphasis by me):

The standard library in general strives to support both statically linked and dynamically linked C runtimes for targets as appropriate.
[…]
The linkage of the C runtime is configured to respect the crt-static target feature.
[…]
Targets which do not support switching between linkage of the C runtime will ignore this flag. It’s recommended to inspect the resulting binary to ensure that it’s linked as you would expect after the compiler succeeds.

So, actually,

might be incorrect. It could’ve just silently ignored the flag and created a dynamically linked binary anyway.

Nixpkgs seems to set this flag by default when you’re using musl, but I can’t see anything that would specifically cause the error message you’ve shown.

I see. Thanks for the details.

I nudged a colleague in the meantime, who reminded me that the Darwin ABI is not stable (like all BSDs). So this behavior makes a lot of sense: the ABI is unstable, a statically-linked libc would only be compatible with a single kernel version.

anw, TIL :slight_smile:

The default Linux stdenv is using Glibc, not Musl. The original snippet was trying to statically link Glibc, hence the error.

You can for sure statically link glibc and I did it using cargo build with the appropriate compiler flags on target x86_64-unknown-linux-gnu.

$ file ./myexecutable
./myexecutable: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, for GNU/Linux 3.10.0, stripped
$ ldd ./myexecutable
        statically linked

The executable works fine in all environments where I tested it, while the dynamically linked binary was not working due to a glibc version mismatch or something else, can’t remember right now.

So my question still stands, what am I missing in my flake to make this work? I had issues using MUSL with one of the crates I was using so I think I need to link against glibc.

NOTE: I’m not on darwin, I am on Linux x86-64

Oh sorry, yeah that seems legit. I tried it now on my machine and can 100% reproduce this. Regular build works fine and yields a static binary, nix build works without the envvar, but crashes with the same message you observed.

In addition to your attempted solutions, I also tried explicitly setting the linker with CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER to both gcc and clang’s ld, and switching the build to nearsk, but to no success.

I feel like there’s some issue with the way the way the command line arguments are built up. They might be in the wrong order, so ld confuses some flags for filepaths? I’m really out of my comfort zone here, so just a shot in the dark.

I tried running nix develop -i .#packages.x86_64-linux.default and then run the stages separately, but that seemed to work mostly fine, so it’s not a good representation of the process.

It’s also a little surprising that the build doesn’t use the .cargo/config.toml file, Nix does copy it into the build directory during the unpackPhase.

Thanks for confirming the issue!

It’s also a little surprising that the build doesn’t use the .cargo/config.toml file, Nix does copy it into the build directory during the unpackPhase.

From what I read, this function doesn’t use cargo and instead calls to rustc directly, so that could explain why the config file is not used.

I have a machine where I can’t compile with cargo build --release (presumably because I’m missing the glibc system-wide) and I noticed that I can fix it if I add pkgs.glibc.static to my dev shell prior to launching cargo. So that’s another data point, it seems it should be able to link properly if pkgs.glibc.static is present, but doesn’t do it from within the buildRustPackage function.