Skip to main content
  1. Languages/
  2. Rust Guides/

Rust Cross-Compilation Guide: Effortlessly Target Multiple Platforms

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

“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:

  1. Rust Toolchain: Stable channel (1.80+ recommended).
  2. Docker or Podman: Required for using the cross tool to handle linker environments.
  3. 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-list

Level 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-gnu

Step 2: Build
#

Run Cargo with the --target flag.

cargo build --release --target x86_64-unknown-linux-gnu

If 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:

flowchart TD A[Start Build Process] --> B{Does Project use<br>C Dependencies?} B -- No (Pure Rust) --> C[Use Standard Cargo] B -- Yes (libc, openssl) --> D[Use 'cross' tool] C --> C1[rustup target add <target>] C1 --> C2[cargo build --target <target>] D --> D1[cargo install cross] D1 --> D2[cross build --target <target>] D2 --> E{Docker Installed?} E -- Yes --> F[Spins up Container with<br>Cross-Toolchain] E -- No --> G[Error: Docker required] C2 --> H[Binary in /target] F --> H style A fill:#f9f,stroke:#333,stroke-width:2px style H fill:#90EE90,stroke:#333,stroke-width:2px,color:black style D fill:#FFD700,stroke:#333,stroke-width:2px,color:black

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 cross

2. 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_demo

Edit 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 --release

What happens here?

  1. cross checks if the Docker image for aarch64-unknown-linux-gnu exists locally. If not, it pulls it from the registry.
  2. It mounts your source code into the container.
  3. It runs cargo build inside the container using the container’s cross-compiler (gcc-aarch64-linux-gnu).
  4. 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_demo

Output (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 --release

Musl 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:

  1. Use rustup target add for WASM or pure Rust libraries.
  2. Use cross for building executables for different OSs (Linux/Windows) or Architectures (x86/ARM).
  3. Prefer pure-Rust dependencies (like rustls over openssl) to simplify your build pipeline.

Now, go build that ARM binary and deploy it to the edge!

Further Reading
#