Cross-Compilation — One Source, Many Targets 🟡
交叉编译:一套源码,多种目标 🟡
What you’ll learn:
本章将学到什么:
- How Rust target triples work and how to add them with
rustup
Rust target triple 是怎么工作的,以及如何用rustup安装目标- Building static musl binaries for container/cloud deployment
如何为容器和云部署构建静态 musl 二进制- Cross-compiling to ARM (aarch64) with native toolchains,
cross, andcargo-zigbuild
如何用原生工具链、cross和cargo-zigbuild交叉编译到 ARM(aarch64)- Setting up GitHub Actions matrix builds for multi-architecture CI
如何给 GitHub Actions 配置多架构矩阵构建Cross-references: Build Scripts — build.rs runs on HOST during cross-compilation · Release Profiles — LTO and strip settings for cross-compiled release binaries · Windows — Windows cross-compilation and
no_stdtargets
交叉阅读: 构建脚本 说明了build.rs在交叉编译时运行在 HOST 上;发布配置 继续讲 LTO 和 strip 等发布参数;Windows 负责 Windows 交叉编译与no_std目标的另一半话题。
Cross-compilation means building an executable on one machine (the host) that runs on a different machine (the target). The host might be your x86_64 laptop; the target might be an ARM server, a musl-based container, or even a Windows machine. Rust makes this remarkably feasible because rustc is already a cross-compiler — it just needs the right target libraries and a compatible linker.
交叉编译的意思很简单:在一台机器上构建,在另一台机器上运行。前者叫 host,后者叫 target。host 可能是 x86_64 笔记本,target 可能是 ARM 服务器、基于 musl 的容器,甚至是 Windows 主机。Rust 在这件事上天生就占便宜,因为 rustc 本身就是交叉编译器,只是还需要正确的目标库和匹配的链接器。
The Target Triple Anatomy
Target Triple 的结构
Every Rust compilation target is identified by a target triple which often has four parts despite the name:
每一个 Rust 编译目标都由一个 target triple 标识。名字虽然叫 triple,实际上经常有四段。
<arch>-<vendor>-<os>-<env>
Examples:
x86_64 - unknown - linux - gnu ← standard Linux (glibc)
x86_64 - unknown - linux - musl ← static Linux (musl libc)
aarch64 - unknown - linux - gnu ← ARM 64-bit Linux
x86_64 - pc - windows- msvc ← Windows with MSVC
aarch64 - apple - darwin ← macOS on Apple Silicon
x86_64 - unknown - none ← bare metal (no OS)
<arch>-<vendor>-<os>-<env>
示例:
x86_64 - unknown - linux - gnu ← 标准 Linux(glibc)
x86_64 - unknown - linux - musl ← 静态 Linux(musl libc)
aarch64 - unknown - linux - gnu ← ARM 64 位 Linux
x86_64 - pc - windows- msvc ← 使用 MSVC 的 Windows
aarch64 - apple - darwin ← Apple Silicon 上的 macOS
x86_64 - unknown - none ← 裸机,无操作系统
List all available targets:
查看可用目标:
# Show all targets rustc can compile to (~250 targets)
rustc --print target-list | wc -l
# Show installed targets on your system
rustup target list --installed
# Show current default target
rustc -vV | grep host
Installing Toolchains with rustup
用 rustup 安装目标工具链
# Add target libraries (Rust std for that target)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu
# Now you can cross-compile:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu # needs a linker — see below
What rustup target add gives you: the pre-compiled std, core, and alloc libraries for that target. It does not give you a C linker or C library. For targets that need a C toolchain, especially most gnu targets, you still need to install that part yourself.rustup target add 到底装了什么:它只会给出目标平台预编译好的 std、core、alloc。它不会顺手给出 C 链接器,也不会给出目标平台的 C 库。所以只要目标依赖 C 工具链,尤其是大部分 gnu 目标,就还得额外安装对应的系统工具。
# Ubuntu/Debian — install the cross-linker for aarch64
sudo apt install gcc-aarch64-linux-gnu
# Ubuntu/Debian — install musl toolchain for static builds
sudo apt install musl-tools
# Fedora
sudo dnf install gcc-aarch64-linux-gnu
.cargo/config.toml — Per-Target Configuration
.cargo/config.toml:按目标配置
Instead of passing --target on every command, configure defaults in .cargo/config.toml at your project root or home directory:
如果不想每次命令都手敲 --target,可以把目标配置放进项目根目录或者用户目录下的 .cargo/config.toml。
# .cargo/config.toml
# Default target for this project (optional — omit to keep native default)
# [build]
# target = "x86_64-unknown-linux-musl"
# Linker for aarch64 cross-compilation
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]
# Linker for musl static builds (usually just the system gcc works)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]
# ARM 32-bit (Raspberry Pi, embedded)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
# Environment variables for all targets
[env]
# Example: set a custom sysroot
# SYSROOT = "/opt/cross/sysroot"
Config file search order (first match wins):
配置文件查找顺序,先找到谁就用谁:
<project>/.cargo/config.toml
1. 当前项目下的.cargo/config.toml。<project>/../.cargo/config.toml(parent directories, walking up)
2. 沿父目录逐级向上查找的.cargo/config.toml。$CARGO_HOME/config.toml(usually~/.cargo/config.toml)
3.$CARGO_HOME/config.toml,通常就是~/.cargo/config.toml。
Static Binaries with musl
用 musl 构建静态二进制
For deploying to minimal containers such as Alpine or scratch, or to systems where you can’t control the glibc version, musl is often the cleanest answer:
如果目标环境是 Alpine、scratch 这类极简容器,或者压根控制不了线上 glibc 版本,那 musl 静态构建通常是最省心的方案。
# Install musl target
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools # provides musl-gcc
# Build a fully static binary
cargo build --release --target x86_64-unknown-linux-musl
# Verify it's static
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked
ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable
Static vs dynamic trade-offs:
静态链接和动态链接的取舍:
| Aspect 方面 | glibc (dynamic) glibc 动态链接 | musl (static) musl 静态链接 |
|---|---|---|
| Binary size 体积 | Smaller (shared libs) 更小,依赖共享库 | Larger (~5-15 MB increase) 更大,通常多 5 到 15 MB |
| Portability 可移植性 | Needs matching glibc version 依赖目标机 glibc 版本匹配 | Runs anywhere on Linux 基本能在 Linux 上通跑 |
| DNS resolution DNS 解析 | Full nsswitch support支持更完整 | Basic resolver (no mDNS) 解析器较基础 |
| Deployment 部署 | Needs sysroot or container 通常要容器或系统依赖配合 | Single binary, no deps 单文件部署,几乎没额外依赖 |
| Performance 性能 | Slightly faster malloc 内存分配通常略快 | Slightly slower malloc 分配器通常略慢 |
dlopen() supportdlopen() | Yes | No |
For the project: A static musl build is ideal for deployment to diverse server hardware where you can’t guarantee the host OS version. The single-binary deployment model eliminates “works on my machine” issues.
对这个工程来说,如果二进制要部署到版本混杂的服务器环境,musl 静态构建会非常合适。单文件交付的方式,也能少掉一堆“本机能跑,线上炸了”的破事。
Cross-Compiling to ARM (aarch64)
交叉编译到 ARM(aarch64)
ARM servers such as AWS Graviton、Ampere Altra、Grace are becoming more common. Cross-compiling for aarch64 from an x86_64 host is a very normal requirement now:
AWS Graviton、Ampere Altra、Grace 这类 ARM 服务器越来越常见了。所以从 x86_64 主机构建 aarch64 二进制,现在已经是很正常的需求。
# Step 1: Install target + cross-linker
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu
# Step 2: Configure linker in .cargo/config.toml (see above)
# Step 3: Build
cargo build --release --target aarch64-unknown-linux-gnu
# Step 4: Verify the binary
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64
Running tests for the target architecture requires either an actual ARM machine or QEMU user-mode emulation:
如果还想跑目标架构测试,那就得有真实 ARM 机器,或者上 QEMU 用户态模拟。
# Install QEMU user-mode (runs ARM binaries on x86_64)
sudo apt install qemu-user qemu-user-static binfmt-support
# Now cargo test can run cross-compiled tests through QEMU
cargo test --target aarch64-unknown-linux-gnu
# (Slow — each test binary is emulated. Use for CI validation, not daily dev.)
Configure QEMU as the test runner in .cargo/config.toml:
可以把 QEMU 直接配成目标测试运行器:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"
The cross Tool — Docker-Based Cross-Compilation
cross:基于 Docker 的交叉编译
The cross tool provides a nearly zero-setup cross-compilation experience by using pre-configured Docker images:cross 通过预配置好的 Docker 镜像,把交叉编译这件事做成了接近零准备的体验。
# Install cross (from crates.io — stable releases)
cargo install cross
# Or from git for latest features (less stable):
# cargo install cross --git https://github.com/cross-rs/cross
# Cross-compile — no toolchain setup needed!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf
# Cross-test — QEMU included in the Docker image
cross test --target aarch64-unknown-linux-gnu
How it works: cross replaces cargo and runs the build inside a Docker container that already contains the right sysroot, linker, and toolchain. Your source is mounted into the container, and the output still goes into the usual target/ directory.
它的工作方式 其实很朴素:用 cross 代替 cargo,把构建过程扔进一个已经准备好 sysroot、链接器和工具链的容器里。源码还是挂载进容器,输出也还是回到熟悉的 target/ 目录。
Customizing the Docker image with Cross.toml:
如果默认镜像不够用,可以通过 Cross.toml 自定义。
# Cross.toml
[target.aarch64-unknown-linux-gnu]
# Use a custom Docker image with extra system libraries
image = "my-registry/cross-aarch64:latest"
# Pre-install system packages
pre-build = [
"dpkg --add-architecture arm64",
"apt-get update && apt-get install -y libpci-dev:arm64"
]
[target.aarch64-unknown-linux-gnu.env]
# Pass environment variables into the container
passthrough = ["CI", "GITHUB_TOKEN"]
cross requires Docker or Podman, but it saves you from manually dealing with cross-compilers, sysroots, and QEMU. For CI, it’s usually the most straightforward choice.cross 的代价就是要有 Docker 或 Podman,但好处也很明显:不用手工折腾交叉编译器、sysroot 和 QEMU。对 CI 来说,它通常是最省脑子的方案。
Using Zig as a Cross-Compilation Linker
把 Zig 当成交叉编译链接器
Zig bundles a C compiler and cross-compilation sysroot for dozens of targets in a single small download. That makes it a very convenient cross-linker for Rust:
Zig 把 C 编译器和多目标 sysroot 都打包进一个很小的下载里,所以拿它做 Rust 的交叉链接器会非常顺手。
# Install Zig (single binary, no package manager needed)
# Download from https://ziglang.org/download/
# Or via package manager:
sudo snap install zig --classic --beta # Ubuntu
brew install zig # macOS
# Install cargo-zigbuild
cargo install cargo-zigbuild
Why Zig? The biggest advantage is glibc version targeting. Zig lets you specify the exact glibc version to link against, which is gold when your binaries must run on older enterprise distributions:
为什么要用 Zig:最大的亮点就是它能精确指定 glibc 版本。只要目标环境里存在老旧企业发行版,这一点就非常值钱。
# Build for glibc 2.17 (CentOS 7 / RHEL 7 compatibility)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
# Build for aarch64 with glibc 2.28 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28
# Build for musl (fully static)
cargo zigbuild --release --target x86_64-unknown-linux-musl
The .2.17 suffix is Zig-specific. It tells Zig to link against glibc 2.17 symbol versions so the result still runs on CentOS 7 and later, without needing Docker or hand-managed sysroots.
这里的 .2.17 后缀是 Zig 扩展语法,意思是按 glibc 2.17 的符号版本去链接。这样产物就能在 CentOS 7 及之后的系统上运行,而且不用靠 Docker,也不用自己维护 sysroot。
Comparison: cross vs cargo-zigbuild vs manual:cross、cargo-zigbuild 和手工配置的对比:
| Feature 维度 | Manual 手工配置 | cross | cargo-zigbuild |
|---|---|---|---|
| Setup effort 准备成本 | High 高 | Low (needs Docker) 低,但需要 Docker | Low (single binary) 低,只要一个 Zig |
| Docker required 需要 Docker | No | Yes | No |
| glibc version targeting glibc 版本可控 | No | No | Yes |
| Test execution 测试执行 | Needs QEMU 自己配 QEMU | Included 镜像里通常带好 | Needs QEMU 自己配 QEMU |
| macOS → Linux macOS 到 Linux | Difficult 较麻烦 | Easy 简单 | Easy 简单 |
| Linux → macOS Linux 到 macOS | Very difficult 很难 | Not supported 不支持 | Limited 支持有限 |
| Binary size overhead 额外体积 | None | None | None |
CI Pipeline: GitHub Actions Matrix
CI 流水线:GitHub Actions 矩阵构建
A production-grade CI workflow that builds for multiple targets often looks like this:
面向生产环境的多目标 CI,通常长得就是下面这样。
# .github/workflows/cross-build.yml
name: Cross-Platform Build
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
name: linux-x86_64
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
name: linux-x86_64-static
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
name: linux-aarch64
use_cross: true
- target: x86_64-pc-windows-msvc
os: windows-latest
name: windows-x86_64
runs-on: ${{ matrix.os }}
name: Build (${{ matrix.name }})
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get install -y musl-tools
- name: Install cross
if: matrix.use_cross
run: cargo install cross
- name: Build (native)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: Build (cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: Run tests
if: "!matrix.use_cross"
run: cargo test --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: diag_tool-${{ matrix.name }}
path: target/${{ matrix.target }}/release/diag_tool*
Application: Multi-Architecture Server Builds
应用场景:多架构服务器构建
The binary currently has no cross-compilation setup. For a diagnostics tool meant to cover diverse server fleets, the following structure is a sensible addition:
当前二进制还没有正式的交叉编译配置。如果它的部署目标是一堆架构和系统都不统一的服务器,那下面这套结构就很值得补上。
my_workspace/
├── .cargo/
│ └── config.toml ← linker configs per target
├── Cross.toml ← cross tool configuration
└── .github/workflows/
└── cross-build.yml ← CI matrix for 3 targets
Recommended .cargo/config.toml:
建议的 .cargo/config.toml:
# .cargo/config.toml for the project
# Release profile optimizations (already in Cargo.toml, shown for reference)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true
# aarch64 for ARM servers (Graviton, Ampere, Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
# musl for portable static binaries
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
Recommended build targets:
建议重点支持的目标:
| Target | Use Case 用途 | Deploy To 部署位置 |
|---|---|---|
x86_64-unknown-linux-gnu | Default native build 默认原生构建 | Standard x86 servers 普通 x86 服务器 |
x86_64-unknown-linux-musl | Static binary, any distro 静态单文件 | Containers, minimal hosts 容器、极简主机 |
aarch64-unknown-linux-gnu | ARM servers ARM 服务器构建 | Graviton, Ampere, Grace Graviton、Ampere、Grace |
Key insight: The
[profile.release]in the workspace root already haslto = true,codegen-units = 1,panic = "abort", andstrip = true. That combination is already extremely suitable for cross-compiled deployment binaries. Add musl on top, and you get a compact single binary with almost no runtime dependency burden.
关键点:workspace 根下的[profile.release]已经配好了lto = true、codegen-units = 1、panic = "abort"、strip = true。这套配置本来就很适合交叉编译后的部署二进制。再叠一层 musl,基本就能得到一个紧凑、依赖极少的单文件产物。
Troubleshooting Cross-Compilation
交叉编译排障
| Symptom 现象 | Cause 原因 | Fix 处理方式 |
|---|---|---|
linker 'aarch64-linux-gnu-gcc' not found找不到 aarch64-linux-gnu-gcc | Missing cross-linker toolchain 没装交叉链接器 | sudo apt install gcc-aarch64-linux-gnu |
cannot find -lssl (musl target)musl 目标找不到 -lssl | System OpenSSL is glibc-linked 系统 OpenSSL 绑定的是 glibc | Use vendored feature: openssl = { version = "0.10", features = ["vendored"] }改用 vendored OpenSSL。 |
build.rs runs wrong binarybuild.rs 跑错平台逻辑 | build.rs runs on HOST, not targetbuild.rs 运行在 HOST 上 | Check CARGO_CFG_TARGET_OS in build.rs, not cfg!(target_os)在 build.rs 里读 CARGO_CFG_TARGET_OS。 |
Tests pass locally, fail in cross本地测试过了, cross 里挂了 | Docker image missing test fixtures 容器里缺测试资源 | Mount test data via Cross.toml用 Cross.toml 把测试数据挂进去。 |
undefined reference to __cxa_thread_atexit_impl出现 __cxa_thread_atexit_impl 未定义 | Old glibc on target 目标机 glibc 太旧 | Use cargo-zigbuild with explicit glibc version用 cargo-zigbuild 锁定 glibc 版本。 |
| Binary segfaults on ARM ARM 上运行直接崩 | Compiled for wrong ARM variant ARM 目标选错了 | Verify target triple matches hardware 确认 target triple 和硬件一致。 |
GLIBC_2.XX not found at runtime运行时报 GLIBC_2.XX not found | Build machine has newer glibc 构建机 glibc 太新 | Use musl or cargo-zigbuild for glibc pinning用 musl,或者用 cargo-zigbuild 锁版本。 |
Cross-Compilation Decision Tree
交叉编译决策树
flowchart TD
START["Need to cross-compile?<br/>需要交叉编译吗?"] --> STATIC{"Static binary?<br/>要静态二进制吗?"}
STATIC -->|Yes<br/>要| MUSL["musl target<br/>--target x86_64-unknown-linux-musl"]
STATIC -->|No<br/>不要| GLIBC{"Need old glibc?<br/>需要兼容老 glibc 吗?"}
GLIBC -->|Yes<br/>需要| ZIG["cargo-zigbuild<br/>--target x86_64-unknown-linux-gnu.2.17"]
GLIBC -->|No<br/>不需要| ARCH{"Target arch?<br/>目标架构是什么?"}
ARCH -->|"Same arch<br/>同架构"| NATIVE["Native toolchain<br/>rustup target add + linker"]
ARCH -->|"ARM/other<br/>ARM 或其他"| DOCKER{"Docker available?<br/>有 Docker 吗?"}
DOCKER -->|Yes<br/>有| CROSS["cross build<br/>Docker-based, zero setup"]
DOCKER -->|No<br/>没有| MANUAL["Manual sysroot<br/>apt install gcc-aarch64-linux-gnu"]
style MUSL fill:#91e5a3,color:#000
style ZIG fill:#91e5a3,color:#000
style CROSS fill:#91e5a3,color:#000
style NATIVE fill:#e3f2fd,color:#000
style MANUAL fill:#ffd43b,color:#000
🏋️ Exercises
🏋️ 练习
🟢 Exercise 1: Static musl Binary
🟢 练习 1:构建静态 musl 二进制
Build any Rust binary for x86_64-unknown-linux-musl. Verify it’s statically linked using file and ldd.
为任意 Rust 二进制构建 x86_64-unknown-linux-musl 版本,并用 file 和 ldd 验证它真的是静态链接。
Solution 参考答案
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl
# Verify
file target/x86_64-unknown-linux-musl/release/hello-static
# Output: ... statically linked ...
ldd target/x86_64-unknown-linux-musl/release/hello-static
# Output: not a dynamic executable
🟡 Exercise 2: GitHub Actions Cross-Build Matrix
🟡 练习 2:GitHub Actions 交叉构建矩阵
Write a GitHub Actions workflow that builds a Rust project for three targets: x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, and aarch64-unknown-linux-gnu. Use a matrix strategy.
写一个 GitHub Actions 工作流,用矩阵方式为 x86_64-unknown-linux-gnu、x86_64-unknown-linux-musl、aarch64-unknown-linux-gnu 三个目标构建 Rust 项目。
Solution 参考答案
name: Cross-build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross
run: cargo install cross --locked
- name: Build
run: cross build --release --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/my-binary
Key Takeaways
本章要点
- Rust’s
rustcis already a cross-compiler — you just need the right target and linkerrustc天生就是交叉编译器,关键只是目标库和链接器配对要对。 - musl produces fully static binaries with zero runtime dependencies — ideal for containers
musl 能产出几乎零运行时依赖的静态二进制,非常适合容器和复杂部署环境。 cargo-zigbuildsolves the “which glibc version” problem for enterprise Linux targetscargo-zigbuild专门解决企业 Linux 里最讨厌的 glibc 版本兼容问题。crossis the easiest path for ARM and other exotic targets — Docker handles the sysrootcross是 ARM 和其他异构目标最省事的路线,sysroot 这些脏活都让 Docker 干了。- Always test with
fileandlddto verify the binary matches your deployment target
最后一定要用file和ldd验证产物,别光看它编过了就以为万事大吉。