An example of cross compiling Rust apps to Linux on Windows, with only rustup

  • Users starred: 17
  • Users forked: 1
  • Users watching: 17
  • Updated at: 2020-05-28 23:17:58

Statically linking and cross-compiling Rust apps on Windows Build status

This example demonstrates how you can use just the tools that are readily accessible through rustup to statically link and cross-compile Rust apps on Windows.

Rust is compiled ahead-of-time to machine code that runs directly on an end-user's machine. That means you have to know upfront what platforms you're going to target and have the right build tools and libraries available for each of them.

Even when you do have compiled binaries, you can run into problems distributing them if you find yourself depending on the availability of C runtime libraries on the end-user's machine.

When your build environment is Windows and you also need to target Linux, it turns out we can solve both our cross-compilation and distribution problems at once by statically linking MSVCRT for Windows and by cross-compiling our Linux builds to target musl instead of glibc.

Let's start with a fresh Hello World Rust app:

cargo new --bin cross-compile-sample
cd cross-compile-sample

Statically linking MSVCRT

If we build our library for the MSVC target (which is the default for Windows) now, it'll dynamically link to MSVCRT:

cargo build --target x86_64-pc-windows-msvc
ls target/x86_64-pc-windows-msvc/debug
Length Name
------ ----
144384 cross-compile-sample.exe

In order to tell the Rust compiler to statically link MSVCRT, we need to add some configuration to a ./cargo/config file:

rustflags = ["-C", "target-feature=+crt-static"]

The crt-static target feature is a code generation option that's only available for targets that are suitable for both static and dynamic linkage. MSVC is one of those targets. When crt-static is specified, the C runtime libraries will be linked statically instead of dynamically.

Building our app again results in a different binary:

cargo build --target x86_64-pc-windows-msvc
ls target/x86_64-pc-windows-msvc/debug
Length Name
------ ----
241152 cross-compile-sample.exe

It's bigger than before because we have the relevant pieces of MSVCRT included.

Cross-compiling to Linux

For Windows, we statically link MSVCRT because it's more convenient for end-users. The crt-static feature solves our distribution problem. For Linux, we're going to statically link the musl libc because it's more convenient for us at build time (and is more portable). musl is a complete, self-contained Linux libc with no system dependencies. That means we don't have to provide Linux system libraries to dynamically link to in our Windows build environment. musl solves our cross-compilation problem.

We can install musl as a target for Rust using rustup:

rustup target add x86_64-unknown-linux-musl

Attempting to build right now probably won't work though:

cargo build --target x86_64-unknown-linux-musl
error: linker `cc` not found
error: could not compile `cross-compile-sample`

We've got the runtime we need, but not the build tools to link up our final Linux binary. Well, actually we do have the build tools we need. We're just not using them yet. Rust embeds LLVM's linker, lld, which we can use instead of the unavailable cc to link our Linux binary on Windows.

Adding rust-lld as the linker for our musl target in our ./cargo/config file will switch from cc to Rust's lld:

linker = "rust-lld"

We should now be able to cross-compile a Linux binary from our Windows host:

cargo build --target x86_64-unknown-linux-musl
ls target/x86_64-unknown-linux-musl/debug
 Length Name
 ------ ----
3041624 cross-compile-sample


Other approaches for cross-compilation

This example uses a combination of musl and LLD to cross-compile a Linux binary from Windows without needing any tools that aren't readily available through rustup. Other approaches include:

  • Use LCOW to build your Linux binaries natively.
  • Use a separate Linux build agent.

They're probably more robust, but depend on the availability of those features, or additional build complexity to coordinate the bundling of artifacts produced in separate environments. Azure Pipelines makes this coordination fairly straightforward though. I've got an example of building a native library (in this case LLVM itself) in Azure Pipelines on several platforms and packaging their artifacts together at the end here.