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.
Targets
Section titled “Targets”Start from a new project:
cargo new crosser Creating binary (application) `crosser` packageA 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:
rustup target add x86_64-unknown-linux-gnuinfo: downloading component 'rust-std' for 'x86_64-unknown-linux-gnu'info: installing component 'rust-std' for 'x86_64-unknown-linux-gnu'rustup target list --installedaarch64-apple-darwinthumbv7em-none-eabihfwasm32-unknown-unknownx86_64-apple-darwinx86_64-unknown-linux-gnux86_64-unknown-linux-muslThe 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.
Mac to Mac needs nothing
Section titled “Mac to Mac needs nothing”Between two Apple platforms the system linker links every architecture, so there’s nothing to configure:
cargo build --release --target x86_64-apple-darwin Compiling crosser v0.1.0 (/private/tmp/crosser) Finished `release` profile [optimized] target(s) in 0.73sfile target/x86_64-apple-darwin/release/crossertarget/x86_64-apple-darwin/release/crosser: Mach-O 64-bit executable x86_64That’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:
cargo run -q --release --target x86_64-apple-darwinerror: 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.
Linux: the linker is the wall
Section titled “Linux: the linker is the wall”The same command for the musl target, in a project with no linker configured, fails:
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 errorrustc 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:
[target.x86_64-unknown-linux-musl]linker = "rust-lld"cargo build --release --target x86_64-unknown-linux-muslfile target/x86_64-unknown-linux-musl/release/crosser Compiling crosser v0.1.0 (/private/tmp/crosser) Finished `release` profile [optimized] target(s) in 0.07starget/x86_64-unknown-linux-musl/release/crosser: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, not strippedThe 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.
What else breaks
Section titled “What else breaks”Running it. The OS refuses a foreign binary, and so does the test harness, which is compiled for the target too:
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 fileerror: 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.
One binary, both Macs
Section titled “One binary, both Macs”The two Apple builds merge into one file:
cargo build --release --target aarch64-apple-darwinlipo -create target/aarch64-apple-darwin/release/crosser target/x86_64-apple-darwin/release/crosser -output crosser-universallipo -info crosser-universal Compiling crosser v0.1.0 (/private/tmp/crosser) Finished `release` profile [optimized] target(s) in 0.23sArchitectures in the fat file: crosser-universal are: x86_64 arm64- How to build a binary you can hand to someone — the previous step: release profile, stripping, and the musl target
- Shipping it: release builds and installers — the same job inside a real project
- Back to Building a command-line tool