Introduction
Most of us develop on macOS or Linux, but our production systems run on anything from AWS Graviton instances to embedded Linux on ARM boards. Cross-compiling Rust: a practical guide for developers is essential knowledge when you need to build binaries for platforms you're not currently sitting in front of. We've shipped services across multiple architectures, and the difference between struggling with manual toolchain setup and having a streamlined build pipeline comes down to knowing your options.
Rust's tooling makes cross-compilation far simpler than it was in C or C++. In this post, we'll walk through the practical steps we've used in production to build for different targets, automate those builds in CI/CD, and avoid the common pitfalls that can waste hours of debugging.
Adding targets with rustup
The foundation of cross-compilation in Rust is rustup target add. This downloads precompiled standard library binaries for your target platform, so you don't have to rebuild the entire std from source.
When we need to deploy an application to AWS Graviton (ARM64), the first step is simple:
rustup target add aarch64-unknown-linux-gnuThen you build with:
cargo build --release --target aarch64-unknown-linux-gnuThat's it for simple binaries with no C dependencies. The output binary lands in target/aarch64-unknown-linux-gnu/release/.
The catch arrives when your crate depends on native libraries. A crate like libsodium or rdkafka needs the actual compiled library for your target architecture. If you're building on an x86_64 machine for aarch64, you'll need to cross-compile those dependencies too. That's where it gets messy—unless you use the right tools.
Common targets we've needed in production:
x86_64-unknown-linux-gnu(standard Linux 64-bit)aarch64-unknown-linux-gnu(ARM 64-bit, Graviton, newer Raspberry Pi)armv7-unknown-linux-gnueabihf(32-bit ARM, older embedded systems)x86_64-pc-windows-msvc(Windows 64-bit)x86_64-apple-darwinandaarch64-apple-darwin(macOS)
For a pure Rust binary with no native dependencies, rustup target add is all you need. But the moment you add a C library binding, you need cross.
Simplifying builds with the cross crate
The cross tool is a Cargo wrapper that runs your build inside a Docker container with the right toolchain already set up. Instead of manually installing ARM compilers, linkers, and system libraries on your host, you use prebuilt images.
Install cross globally:
cargo install crossThen use it exactly like Cargo:
cross build --release --target aarch64-unknown-linux-gnuUnder the hood, cross spins up a Docker container with a complete ARM64 build environment, mounts your source, runs the build, and extracts the binary. For projects depending on openssl, librdkafka, protobuf, or other C libraries, this saves hours of environment setup.
We used cross in a real e-commerce order processing service that needed to run on both x86_64 and Graviton. The service depended on OpenSSL for TLS and librdkafka for Kafka. Building with:
cross build --release --target aarch64-unknown-linux-gnuworked first try. Without cross, we would have spent time installing arm-linux-gnueabihf-gcc, tracking down the ARM version of OpenSSL, and debugging linker errors. Instead, the Docker image handled it all.
cross does have limitations. Some projects with very custom build scripts or unusual C dependencies may need hand-tuning. And if you're building for a non-standard target (like a custom RISC-V board), you might need to create a custom Docker image. But for standard Linux targets and C libraries in the standard repositories, cross is the practical choice.
Building for AWS Graviton and ARM64
AWS Graviton instances are ARM64 and significantly cheaper than equivalent x86_64 instances. We've migrated several services to Graviton for cost savings, but it requires an ARM64 build.
The target is aarch64-unknown-linux-gnu. On an x86_64 developer machine, use cross:
cross build --release --target aarch64-unknown-linux-gnuThe binary runs on any ARM64 Linux system: Graviton, modern Raspberry Pis, or custom ARM boards.
A real scenario: we had a microservice using Tokio, Axum, and SQLx with a PostgreSQL backend. The code was pure Rust—no C dependencies. We added the target and built:
rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnuThen we deployed the binary to a Graviton instance in EC2 and it worked without modification. The service processed ride-hailing requests at the same speed as the x86_64 version, just cheaper to run.
If you deploy to Graviton in production, test the ARM64 build in your CI/CD pipeline. A subtle platform difference (endianness, pointer size, or alignment) might only show up at runtime. We'll cover automation next.
Automating cross-compilation with GitHub Actions
Building multiple targets locally is tedious and error-prone. We automate it in CI/CD using GitHub Actions matrices. This ensures every commit is tested and built for all target platforms.
Here's a workflow that builds and tests for x86_64 and ARM64:
name: Cross-compile
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- armv7-unknown-linux-gnueabihf
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cross
- name: Build
run: cross build --release --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/my-appThis workflow runs a job for each target. The taiki-e/install-action@cross step installs cross on the GitHub runner. Each job builds the release binary and uploads it as an artifact. In minutes, you have binaries for all platforms ready to download or deploy.
We extended this for a SaaS backend by adding a release job that uploads binaries to S3 when we tag a commit:
release:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
- name: Upload to S3
run: |
aws s3 cp binary-x86_64-unknown-linux-gnu/my-app s3://my-bucket/releases/
aws s3 cp binary-aarch64-unknown-linux-gnu/my-app s3://my-bucket/releases/Now tagging a release builds for all architectures and uploads binaries automatically. Your ops team can deploy the right binary to the right instance type without manual builds.
Common mistakes
Building without testing the target. We've shipped binaries for ARM that compiled fine but crashed at runtime due to alignment assumptions in the code. Always run at least smoke tests on the actual target, not just the build output. Use cross test --target aarch64-unknown-linux-gnu if possible, or spin up a test instance.
Forgetting to strip binaries for size. Cross-compiled binaries can be large. In CI/CD, add a strip step:
aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/my-appThis reduces binary size by 50% or more, which matters for container image sizes and deployment bandwidth.
Assuming C dependency compatibility. Just because a C library compiles for your target doesn't mean it works correctly. OpenSSL on ARM has had platform-specific bugs. Test with the real library versions your target uses, not just the build.
Ignoring CPU features. ARM64 has optional extensions (NEON, SVE). If you use Rust crates that compile SIMD code, it might work on some ARM64 systems but not others. Stick to the base ARM64 instruction set unless you're confident about your deployment environment.
Key takeaways
- Use
rustup target addfor pure Rust binaries with no C dependencies; it's the simplest path. - Use
crossfor projects with C library dependencies; it handles the toolchain and system libraries in Docker. - Test cross-compiled binaries on the actual target architecture or a close simulation; compilation success doesn't guarantee runtime correctness.
- Automate cross-compilation in CI/CD with GitHub Actions matrices to ensure all targets build consistently on every commit.
- For production deployments to Graviton or ARM instances, build and test the ARM64 binary in your CI/CD pipeline before merging.
- Strip release binaries for production to reduce size and deployment overhead.






Comments
Loading comments…