“It runs on my machine.” In 2025, that phrase is a career-limiting statement. As Rust developers, we are often working on a MacBook or a Windows workstation, but deploying to AWS Graviton (ARM64) instances, pushing to a Raspberry Pi at the edge, or distributing CLIs to users on three different operating systems.
Rust’s motto is “empowering everyone to build reliable and efficient software,” and part of that efficiency is the ability to compile code for a different architecture or operating system than the one you are currently using. This is cross-compilation.
While Rust’s tooling is lightyears ahead of C++ in this regard, cross-compilation can still hit a wall when system dependencies (like OpenSSL or C runtimes) enter the chat. In this guide, we will move past the basics and look at a robust, production-ready workflow to build binaries for any target without leaving your terminal.
Prerequisites #
To follow this guide, you should have an intermediate understanding of Cargo. We will use a virtualized approach for the complex parts, so ensure you have the following installed:
- Rust Toolchain: Stable channel (1.80+ recommended).
- Docker or Podman: Required for using the
crosstool to handle linker environments. - Terminal: PowerShell, Bash, or Zsh.
Let’s ensure your environment is ready:
rustc --version
# rustc 1.83.0 (or newer)
docker --version
# Docker version 27.x.x...Understanding the Target Triple #
Before we compile, we must define where the code will run. Rust uses the same “target triple” format as LLVM. It looks like this:
cpu-vendor-os
For example:
x86_64-unknown-linux-gnu: Standard Intel/AMD Linux.aarch64-unknown-linux-gnu: ARM64 Linux (AWS Graviton, Raspberry Pi 4/5).x86_64-pc-windows-msvc: Standard 64-bit Windows.aarch64-apple-darwin: Apple Silicon (M1/M2/M3).
You can see all supported targets built into your compiler by running:
rustc --print target-listLevel 1: The “Pure Rust” Happy Path #
If your application is 100% Rust with no C dependencies (and no linking against the system libc), cross-compilation is built directly into rustup.
Let’s say you are on an Apple Silicon Mac (aarch64-apple-darwin) and want to build for an Intel Linux server.
Step 1: Add the Target #
Tell rustup to download the standard library for the target architecture.
rustup target add x86_64-unknown-linux-gnuStep 2: Build #
Run Cargo with the --target flag.
cargo build --release --target x86_64-unknown-linux-gnuIf your project is simple, you will find your binary in target/x86_64-unknown-linux-gnu/release/.
The Catch: This usually fails the moment you try to print “Hello World” dynamically or use a library that binds to C code. Why? Because while you have the Rust standard library for Linux, your Mac does not have a Linux linker or the Linux C libraries installed.
Level 2: The Production Solution with cross
#
For professional applications, the “Pure Rust” method rarely suffices. You will eventually depend on openssl, sqlite, or zlib. To compile these, you need a full toolchain for the target OS installed on your host. Setting this up manually is painful and error-prone.
Enter cross.
cross is a “zero setup” cross-compilation tool maintained by the Rust Embedded working group. It wraps Cargo and runs the build command inside a Docker container that comes pre-configured with the correct linker and C libraries for your target.
Workflow Visualization #
Here is how the decision process looks when choosing a build strategy:
Step-by-Step Implementation #
Let’s create a scenario where we build a Rust app from macOS/Windows for an ARM64 Linux server (like a Raspberry Pi or AWS Graviton).
1. Install cross
#
You only need to do this once.
cargo install cross2. Create a Test Project #
We will add a dependency that often causes linking issues to demonstrate the power of cross. We’ll use openssl (via the reqwest crate).
cargo new cross_demo
cd cross_demoEdit your Cargo.toml to add a dependency:
[package]
name = "cross_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
# We use blocking for simplicity in this script
reqwest = { version = "0.11", features = ["blocking"] }
tokio = { version = "1", features = ["full"] }Edit src/main.rs:
fn main() {
let target = env!("TARGET_TRIPLE"); // We will pass this via CLI or just print OS
println!("Running on architecture: {}", std::env::consts::ARCH);
println!("Running on OS: {}", std::env::consts::OS);
// Simple network call to prove SSL linking works
match reqwest::blocking::get("https://www.rust-lang.org") {
Ok(_) => println!("Successfully connected to Rust homepage (HTTPS works!)"),
Err(e) => println!("Connection failed: {}", e),
}
}Note: In a real scenario, you usually don’t need TARGET_TRIPLE env var for logic, but it’s useful for debugging build scripts.
3. Build with Cross #
Now, instead of cargo build, we use cross build. We want to target aarch64-unknown-linux-gnu.
cross build --target aarch64-unknown-linux-gnu --releaseWhat happens here?
crosschecks if the Docker image foraarch64-unknown-linux-gnuexists locally. If not, it pulls it from the registry.- It mounts your source code into the container.
- It runs
cargo buildinside the container using the container’s cross-compiler (gcc-aarch64-linux-gnu). - It outputs the binary back to your host’s
target/directory.
4. Verify the Output #
Check the file type to confirm it is indeed an ARM binary.
file target/aarch64-unknown-linux-gnu/release/cross_demoOutput (Example):
cross_demo: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked...You have successfully built an ARM binary on an x86 machine with full OpenSSL linkage!
Comparison: Cargo vs. Cross #
When should you use which tool?
| Feature | cargo build --target |
cross build --target |
|---|---|---|
| Setup Complexity | Low (just rustup) |
Medium (requires Docker) |
| Pure Rust Code | Works perfectly | Works perfectly |
| C Dependencies | Fails (usually linker errors) | Works (includes C toolchains) |
| Build Speed | Fast (Native) | Slightly Slower (Container overhead) |
| System Pollution | Low | None (Isolated in Docker) |
| Use Case | WebAssembly, Simple CLIs | Production Binaries, Embedded, IoT |
Common Traps and Best Practices #
1. The OpenSSL Trap #
Even with cross, OpenSSL can be tricky because different Linux distributions use different versions (1.1 vs 3.0).
Best Practice: Whenever possible, avoid linking against the system OpenSSL. Instead, use rustls, which is a pure Rust implementation of TLS. It makes cross-compilation trivial because it removes the C dependency entirely.
In Cargo.toml:
[dependencies]
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }2. Docker Permissions #
On Linux, cross might sometimes generate files owned by root inside the target directory. If you cannot delete or modify files after a build, run:
sudo chown -R $USER:$USER target/Current versions of cross handle UID mapping well, but this remains a common edge case in CI environments.
3. Static Linking (The “Run Anywhere” Binary) #
If you want a binary that runs on any Linux distro (Alpine, Ubuntu, CentOS), target Musl instead of Gnu.
cross build --target x86_64-unknown-linux-musl --releaseMusl produces statically linked binaries that have zero external dependencies.
Conclusion #
In 2025, cross-compilation in Rust has become a solved problem for 95% of use cases thanks to cross. While rustup handles the Rust side of things effortlessly, the complexity of C toolchains makes containerization the only sane strategy for linking dependencies.
Key Takeaways:
- Use
rustup target addfor WASM or pure Rust libraries. - Use
crossfor building executables for different OSs (Linux/Windows) or Architectures (x86/ARM). - Prefer pure-Rust dependencies (like
rustlsoveropenssl) to simplify your build pipeline.
Now, go build that ARM binary and deploy it to the edge!