Skip to content
Select theme

How to cross-compile for another platform

Short answer: rustup target add <target-triple> installs the standard library for the platform, and cargo build --release --target <target-triple> builds for it. Between two Macs that’s all you need. For Linux you also need a linker that produces ELF, and nothing you build will run on the machine that built it.

Start from a new project:

Terminal window
cargo new crosser
Creating binary (application) `crosser` package

A target triple is <arch>-<vendor>-<os>-<abi>: x86_64-unknown-linux-gnu is an x86-64 CPU running Linux, linked against glibc, and x86_64-unknown-linux-musl is the same machine with musl instead. Installing a target downloads one component, the rust-std for it:

Terminal window
rustup target add x86_64-unknown-linux-gnu
info: downloading component 'rust-std' for 'x86_64-unknown-linux-gnu'
info: installing component 'rust-std' for 'x86_64-unknown-linux-gnu'
Terminal window
rustup target list --installed
aarch64-apple-darwin
thumbv7em-none-eabihf
wasm32-unknown-unknown
x86_64-apple-darwin
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl

The host target is always there, and this list is from the machine this article was written on, so yours will differ. Every build below needs its target installed first. The rest of the article uses x86_64-unknown-linux-musl, the Linux target that links on a Mac with nothing but the toolchain.

Between two Apple platforms the system linker links every architecture, so there’s nothing to configure:

Terminal window
cargo build --release --target x86_64-apple-darwin
Compiling crosser v0.1.0 (/private/tmp/crosser)
Finished `release` profile [optimized] target(s) in 0.73s
Terminal window
file target/x86_64-apple-darwin/release/crosser
target/x86_64-apple-darwin/release/crosser: Mach-O 64-bit executable x86_64

That’s the build the previous step points at when an Intel Mac asks for a binary: no flags, no config. Artifacts land in target/<triple>/release/, beside the host build rather than replacing it.

cargo run --target runs the binary that was just built, the target’s and not the host’s:

Terminal window
cargo run -q --release --target x86_64-apple-darwin
error: could not execute process `target/x86_64-apple-darwin/release/crosser` (never executed)
Caused by:
Bad CPU type in executable (os error 86)

On an Intel Mac it runs. On the Apple Silicon machine this was written on, the OS refuses it until Rosetta is installed. file on the output is the check that matters here, not cargo run.

The same command for the musl target, in a project with no linker configured, fails:

Terminal window
cargo build --release --target x86_64-unknown-linux-musl
Compiling crosser v0.1.0 (/private/tmp/crosser)
WARN rustc_codegen_ssa::back::link Linker does not support -static-pie command line option. Retrying with -static instead.
error: linking with `cc` failed: exit status: 1
|
= note: "cc" "-m64" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crt1.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crti.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtbegin.o" "/private/tmp/crosser/target/x86_64-unknown-linux-musl/release/deps/rustcIDc5Sa/symbols.o" "<2 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/{libstd-*,libpanic_unwind-*,libobject-*,libmemchr-*,libaddr2line-*,libgimli-*,libcfg_if-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*}.rlib" "-lunwind" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/liblibc-*.rlib" "-lc" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/{librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib" "-L" "/private/tmp/crosser/target/x86_64-unknown-linux-musl/release/deps/rustcIDc5Sa/raw-dylibs" "-Wl,-Bdynamic" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-nostartfiles" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib" "-o" "/private/tmp/crosser/target/x86_64-unknown-linux-musl/release/deps/crosser-25e2004f86d7172b" "-Wl,--gc-sections" "-static" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-debug" "-nodefaultlibs" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtend.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtn.o"
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: ld: unknown options: --as-needed -Bstatic -Bdynamic --eh-frame-hdr -z --gc-sections -z -z --strip-debug
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: could not compile `crosser` (bin "crosser") due to 1 previous error

rustc drives the link through cc, and the cc on this Mac produces Mach-O. The WARN line is rustc noticing that and retrying without -static-pie; macOS ld then rejects the ELF options outright. Musl itself isn’t the problem: the cc line above is musl’s own crt1.o and crti.o, straight out of the toolchain’s sysroot. The linker is. rust-lld, which ships with the toolchain, reads ELF:

.cargo/config.toml
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
Terminal window
cargo build --release --target x86_64-unknown-linux-musl
file target/x86_64-unknown-linux-musl/release/crosser
Compiling crosser v0.1.0 (/private/tmp/crosser)
Finished `release` profile [optimized] target(s) in 0.07s
target/x86_64-unknown-linux-musl/release/crosser: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, not stripped

The stanza is per-target: it applies to builds for x86_64-unknown-linux-musl and to nothing else, where RUSTFLAGS="-C linker=rust-lld" would apply to every build, host ones included. The gnu target needs the same stanza pointed at a cross C toolchain, because glibc’s startup files don’t ship with the target the way musl’s do. That’s a separate install, not a config line.

Running it. The OS refuses a foreign binary, and so does the test harness, which is compiled for the target too:

Terminal window
cargo test --target x86_64-unknown-linux-musl
Compiling crosser v0.1.0 (/private/tmp/crosser)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.11s
Running unittests src/main.rs (target/x86_64-unknown-linux-musl/debug/deps/crosser-f86a36f8604f21a9)
/private/tmp/crosser/target/x86_64-unknown-linux-musl/debug/deps/crosser-f86a36f8604f21a9: /private/tmp/crosser/target/x86_64-unknown-linux-musl/debug/deps/crosser-f86a36f8604f21a9: cannot execute binary file
error: test failed, to rerun pass `--bin crosser`
Caused by:
process didn't exit successfully: `/private/tmp/crosser/target/x86_64-unknown-linux-musl/debug/deps/crosser-f86a36f8604f21a9` (exit status: 126)
note: test exited abnormally; to see the full output pass --no-capture to the harness.

The hash in the test binary’s name comes from the package metadata and will differ on your machine. The same stanza has a runner key that points cargo test at an emulator (qemu-x86_64 for this target), which is how cross-target CI runs tests.

C code. Anything that compiles C needs the cross toolchain as well: the -sys crates, and anything using the cc crate. Build scripts are built for the machine doing the building and run there, so one that probes the system probes the build machine, not the target.

Windows. Same story: the msvc targets link only where the MSVC linker exists, and the gnu targets want MinGW.

The two Apple builds merge into one file:

Terminal window
cargo build --release --target aarch64-apple-darwin
lipo -create target/aarch64-apple-darwin/release/crosser target/x86_64-apple-darwin/release/crosser -output crosser-universal
lipo -info crosser-universal
Compiling crosser v0.1.0 (/private/tmp/crosser)
Finished `release` profile [optimized] target(s) in 0.23s
Architectures in the fat file: crosser-universal are: x86_64 arm64