Effortless Cross-Compilation for Rust: Building for Any Platform
Building a Rust application is a fantastic experience, but when it comes time to distribute your software to different operating systems or architectures — like building a Linux binary from your macOS machine, or a Windows executable from your Linux server — you hit a wall. This is cross-compilation, and it’s notoriously difficult due to the need for complex C toolchains, which rustc often relies on for linking.
Cross-compilation is the process of building executable binaries for a target platform different from the one you’re currently developing on. For Rust developers, this capability is essential when you need to build Windows executables from macOS or Linux, create ARM binaries from x86 systems, or support multiple platforms without maintaining separate build machines.
Why Cross-Compile? Before diving into the how, let’s understand why cross-compilation matters:
- Cost Efficiency: No need for multiple development machines or CI/CD runners for each target platform
- Faster Development: Build and test for multiple platforms from a single environment
- Consistency: Ensure all platform builds use the same source code and dependencies
- Automation: Simplify your CI/CD pipeline by building all targets in one workflow
Understanding Rust’s Cross-Compilation Architecture Rust’s cross-compilation relies on several key components:
Target Triples: These identify the platform you’re building for, following the pattern <architecture>-<vendor>-<system>-<ABI>. Common examples include:
- x86_64-unknown-linux-gnu - 64-bit Linux with GNU toolchain
- x86_64-pc-windows-gnu - 64-bit Windows with MinGW
- i686-pc-windows-gnu - 32-bit Windows with MinGW
- aarch64-unknown-linux-gnu - 64-bit ARM Linux
- x86_64-apple-darwin - 64-bit macOS
Toolchains: Rust uses rustup to manage different compiler toolchains, each capable of generating code for specific targets. Linkers: The final step of compilation requires a linker that understands the target platform’s binary format.
Method 1: Native Cross-Compilation with rustup
The simplest approach uses Rust’s built-in cross-compilation support through rustup.
Step 1: Install Target Support
First, add support for your target platform:
- Add Windows 64-bit target
rustup target add x86_64-pc-windows-gnu
- Add Windows 32-bit target
rustup target add i686-pc-windows-gnu
- Add Linux ARM target
rustup target add aarch64-unknown-linux-gnu
- List all available targets
rustup target list
Step 2: Install Required Linkers
For Windows targets on macOS or Linux, you’ll need MinGW:
- On macOS with Homebrew
brew install mingw-w64
- On Ubuntu/Debian
sudo apt-get install mingw-w64
- On Fedora/RHEL
sudo dnf install mingw64-gcc mingw32-gcc For Linux targets on macOS, you may need:
- Install cross-compilation tools
brew install SergioBenitez/osxct/x86_64-unknown-linux-gnu
Step 3: Configure Cargo
Create or edit .cargo/config.toml in your project directory to specify linkers: [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" ar = "x86_64-w64-mingw32-ar"
[target.i686-pc-windows-gnu] linker = "i686-w64-mingw32-gcc" ar = "i686-w64-mingw32-ar"
[target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc"
Step 4: Build for Target Platform
- Build for Windows 64-bit
cargo build --target x86_64-pc-windows-gnu --release
- Build for Windows 32-bit
cargo build --target i686-pc-windows-gnu --release
- Build for Linux ARM
cargo build --target aarch64-unknown-linux-gnu --release Your compiled binaries will be in target/<target-triple>/release/.
Method 2: Using Cross for Robust Cross-Compilation
Cross is a powerful tool that uses Docker containers to provide complete cross-compilation environments, eliminating many dependency and configuration headaches. The community-maintained wrapper around cargo that makes cross-compilation truly painless. It uses Docker or Podman containers to provide the correct toolchains and environment, ensuring your build works the first time, every time.
Why Use Cross?
- Zero Configuration: Works out of the box for most targets
- Consistent Environment: Docker ensures reproducible builds
- Handles Dependencies: Automatically manages system libraries and tools
- Wide Platform Support: Supports numerous target platforms
- CI/CD Friendly: Perfect for automated builds
Install a Container Engine
cross relies on containers to isolate the necessary toolchains. We recommend Docker or Podman.
- macOS and Windows: Docker Desktop is the easiest choice. Ensure you follow the installation instructions and verify it’s running. On Windows, make sure the WSL2 backend is enabled.
- Linux: Podman is often preferred as it runs rootless by default, but Docker Engine is also a great option.
Basic Usage
Cross uses the same syntax as cargo, but handles all the toolchain complexity:
- Build for Windows 64-bit
cross build --target x86_64-pc-windows-gnu --release
- Build for Windows 32-bit
cross build --target i686-pc-windows-gnu --release
- Build for Linux on different architecture
cross build --target aarch64-unknown-linux-gnu --release
- Build for ARM embedded systems
cross build --target armv7-unknown-linux-gnueabihf --release You can verify your container engine is running by checking the active containers: docker ps -a
- or
podman ps -a
Installing the cross Tool
Once your Rust environment and container engine are ready, installing cross is a simple cargo command: cargo install cross Now you are ready to compile!
The Core: Cross-Compiling Your Rust Code
Instead of calling cargo build, you simply substitute it with cross build. The cross tool automatically handles fetching the correct target toolchain and the corresponding Docker image.
Building Windows PE Executable
A common task is building Portable Executable (PE) files for Windows from a Linux or macOS host. You’ll typically target the GNU environment for the most compatible results. To build your hello.rs project for Windows, use the following commands:
- For 64-bit Windows
cross build --target=x86_64-pc-windows-gnu --release
- Target Architecture Command Output Location
- 32-bit Windows i686-pc-windows-gnu
cross build --target=i686-pc-windows-gnu --release target/i686-pc-windows-gnu/release/hello.exe
- 64-bit Windows x86_64-pc-windows-gnu
cross build --target=x86_64-pc-windows-gnu --release target/x86_64-pc-windows-gnu/release/hello.exe The resulting executables will be located in the target/<target-triple>/release/ directory and can be transferred to Windows machines for execution.
Other Common Targets
The pattern is the same for other architectures:
PlatformTarget TripleExample CommandLinux (ARM64)aarch64-unknown-linux-gnucross build --target aarch64-unknown-linux-gnuLinux (32-bit)i686-unknown-linux-gnucross build --target i686-unknown-linux-gnu 💡 Tip: To see a full list of supported targets, run rustup target list or check the cross project documentation.
Handling Non-Host Toolchains
When building for platforms significantly different from your host (like building Windows PE files on macOS), you may need to install the toolchain explicitly: rustup toolchain install stable-x86_64-unknown-linux-gnu --force-non-host This ensures the correct standard library and compiler components are available.
Troubleshooting Cross
If you encounter issues with cross:
- Update cross and Docker images:
cargo install cross --git https://github.com/cross-rs/cross --force docker pull ghcr.io/cross-rs/x86_64-pc-windows-gnu:latest
- Check Docker is running:
docker ps
- Enable BuildKit for better performance:
export DOCKER_BUILDKIT=1
- Use verbose output for debugging:
cross build --target x86_64-pc-windows-gnu --verbose
Configuring Cross with Cross.toml
For advanced scenarios, create a Cross.toml file in your project root: [build]
- Use a specific Docker image
[target.x86_64-pc-windows-gnu] image = "ghcr.io/cross-rs/x86_64-pc-windows-gnu:latest"
- Mount additional directories
[build.env] volumes = ["./data:/data:ro"]
- Pass environment variables
[build.env.passthrough] env = ["MY_CUSTOM_VAR"]
Platform-Specific Considerations
Building for Windows When building Windows executables from Unix systems:
- Use the -gnu targets (MinGW) rather than -msvc for better cross-compilation support
- The resulting .exe files can be directly executed on Windows
- Consider using cargo-wix for creating Windows installers
- Static linking is easier with MinGW targets
- Build with optimizations for Windows
cross build --target x86_64-pc-windows-gnu --release
- The output will be at:
- target/x86_64-pc-windows-gnu/release/your_binary.exe
Building for Linux When targeting different Linux distributions or architectures:
- Use musl targets for static binaries that work everywhere
- Consider the glibc version compatibility
- ARM targets require proper target specification
- Build static Linux binary
cross build --target x86_64-unknown-linux-musl --release
- Build for ARM Linux
cross build --target aarch64-unknown-linux-gnu --release B uilding for macOS
Cross-compiling for macOS is more complex due to Apple’s licensing:
- The osxcross project can help but requires macOS SDK
- Building macOS binaries from Linux requires careful setup
- Consider using macOS-based CI runners for macOS builds
Handling Dependencies
Native Dependencies
When your Rust project depends on C libraries or system packages:
- Using Cross: It handles most common dependencies automatically through pre-configured Docker images
- Custom Docker Images: For unusual dependencies, extend Cross’s images:
FROM ghcr.io/cross-rs/x86_64-pc-windows-gnu:latest
RUN apt-get update && \
apt-get install -y your-package
Reference this in Cross.toml: [target.x86_64-pc-windows-gnu] image = "your-registry/custom-cross-image:latest"
Static vs Dynamic Linking
Control linking behavior through environment variables:
- Force static linking
RUSTFLAGS='-C target-feature=+crt-static' cross build --target x86_64-pc-windows-gnu --release
- For musl targets (already static by default)
cross build --target x86_64-unknown-linux-musl --release
CI/CD Integration
Cross-compilation shines in automated environments. Here’s a GitHub Actions example: name: Cross-Platform Build
on: [push, pull_request] jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-pc-windows-gnu
- x86_64-apple-darwin
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: $Template:Matrix.target
override: true
- name: Install Cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build
run: cross build --target $Template:Matrix.target --release
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: binary-$Template:Matrix.target
path: target/$Template:Matrix.target/release/
Testing Cross-Compiled Binaries
Using QEMU
For testing binaries of different architectures:
- Install QEMU
sudo apt-get install qemu-user-static
- Run ARM binary on x86
qemu-aarch64-static target/aarch64-unknown-linux-gnu/release/your_binary
- Run with binfmt support for transparent execution
docker run --rm -t arm64v8/ubuntu ./your_binary
Wine for Windows Binaries
Test Windows executables on Linux:
- Install Wine
sudo apt-get install wine64
- Run Windows binary
wine target/x86_64-pc-windows-gnu/release/your_binary.exe
Best Practices
- Start Simple: Begin with native compilation, then add cross-compilation targets as needed
- Use Cross for Complex Setups: If you’re dealing with C dependencies or multiple targets, cross saves significant time
- Document Your Process: Keep a README with build instructions for each target platform
- Automate Testing: Set up CI/CD to build and test all targets automatically
- Version Lock Dependencies: Use Cargo.lock to ensure reproducible builds across platforms
- Consider Binary Size: Different targets may produce different binary sizes; optimize accordingly
- Handle Platform-Specific Code: Use conditional compilation for platform differences:
- [cfg(target_os = "windows")]
fn platform_specific() {
// Windows-specific code
}
- [cfg(target_os = "linux")]
fn platform_specific() {
// Linux-specific code
}
Common Issues and Solutions
Troubleshooting a Common Toolchain Error When trying to build for a target that is the same as your host machine (e.g., building for x86_64-unknown-linux-gnu on a Linux machine), you might encounter an error like: "Couldn't install toolchain" · Issue #1649 · cross-rs/cross or an error indicating the toolchain does not support components. This happens because rustup tries to install the host toolchain in a way that conflicts with cross's requirements. The fix is to explicitly force the installation as a "non-host" toolchain:
- 1. Uninstall the potentially broken toolchain (replace the triple as needed)
rustup toolchain uninstall stable-x86_64-unknown-linux-gnu
- 2. Re-install it, forcing it to be treated as a non-host toolchain
rustup toolchain install stable-x86_64-unknown-linux-gnu --force-non-host
Issue: Linker Not Found
Solution: Ensure the appropriate cross-compilation toolchain is installed:
- For Windows targets
brew install mingw-w64 # macOS sudo apt-get install mingw-w64 # Linux
Issue: Missing Standard Library
Solution: Add the target explicitly: rustup target add x86_64-pc-windows-gnu
Issue: Docker Permission Denied (Cross)
Solution: Add your user to the docker group or use sudo: sudo usermod -aG docker $USER
- Then log out and back in
Issue: OpenSSL Linking Errors
Solution: Use vendored OpenSSL or rustls: [dependencies] openssl = { version = "0.10", features = ["vendored"] }
- Or use rustls instead
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
Real-World Example
Let’s walk through a complete example of setting up cross-compilation for a CLI tool:
- 1. Create a new project
cargo new hello-cross cd hello-cross
- 2. Install cross
cargo install cross --git https://github.com/cross-rs/cross
- 3. Build for multiple platforms
cross build --target x86_64-pc-windows-gnu --release cross build --target x86_64-unknown-linux-gnu --release cross build --target aarch64-unknown-linux-gnu --release
- 4. Your binaries are now ready
ls -lh target/x86_64-pc-windows-gnu/release/hello-cross.exe ls -lh target/x86_64-unknown-linux-gnu/release/hello-cross ls -lh target/aarch64-unknown-linux-gnu/release/hello-cross
- 5. Package for distribution
mkdir -p releases cp target/x86_64-pc-windows-gnu/release/hello-cross.exe releases/hello-cross-windows-x64.exe cp target/x86_64-unknown-linux-gnu/release/hello-cross releases/hello-cross-linux-x64 cp target/aarch64-unknown-linux-gnu/release/hello-cross releases/hello-cross-linux-arm64
Resources and Further Reading
- Cross GitHub Repository: https://github.com/cross-rs/cross — Comprehensive documentation and examples
- Cross Getting Started Guide: https://github.com/cross-rs/cross/wiki/Getting-Started
- Rust Platform Support: https://doc.rust-lang.org/nightly/rustc/platform-support.html — Official list of supported targets
- Rustup Book: https://rust-lang.github.io/rustup/ — Details on managing toolchains
- Example Project: https://github.com/Yen-Coder/Rust-CICD-Test — Real-world CI/CD setup for cross-compilation
Conclusion
Cross-compiling Rust code empowers you to support multiple platforms efficiently without the overhead of maintaining separate build environments. Whether you choose the native rustup approach for simple projects or leverage Cross’s Docker-based system for complex scenarios, Rust’s excellent cross-compilation support makes multi-platform development accessible and straightforward. Start with a single additional target, integrate it into your workflow, and gradually expand your platform support. The investment in setting up cross-compilation pays dividends in development speed, deployment flexibility, and user reach. For a real-world example of how this can be implemented in a Continuous Integration (CI) environment, you can explore this repository: Yen-Coder/Rust-CICD-Test Happy cross-compiling! 🦀
Read the full article here: https://medium.com/rust-rock/effortless-cross-compilation-for-rust-building-for-any-platform-6cce81558123