The Cargo Guide
Target Triples and Cross-Compilation
Why Target Triples Matter
A target triple identifies the platform Cargo and rustc should build for. Cross-compilation begins when the machine doing the build is not the same platform as the one the final artifact is meant to run on. Cargo supports this through --target, target-specific dependencies, and per-target configuration in .cargo/config.toml. :contentReference[oaicite:1]{index=1}
A useful mental model is:
- host: the platform running Cargo, build scripts, and proc macros
- target: the platform the final crate artifact is being compiled for
What a Target Triple Looks Like
A target triple is usually a string such as:
x86_64-unknown-linux-gnu
aarch64-unknown-linux-gnu
x86_64-pc-windows-msvc
wasm32-unknown-unknownCargo uses these identifiers in command-line flags, target-specific dependency tables, and .cargo/config.toml target sections. :contentReference[oaicite:2]{index=2}
Host vs Target
The host is the platform where Cargo and the compiler are running. The target is the platform you are compiling for. In build-script environments, Cargo exposes both through the HOST and TARGET environment variables. Native code compiled from a build script should generally be compiled for TARGET, while the build script itself runs on HOST. :contentReference[oaicite:3]{index=3}
Example build script:
fn main() {
let host = std::env::var("HOST").unwrap();
let target = std::env::var("TARGET").unwrap();
println!("cargo::warning=host={host} target={target}");
}Why Host vs Target Is Easy to Confuse
A major source of confusion is that not every artifact in a Cargo build is built for the same platform. Your library or binary may be built for the target, while build scripts and proc macros are host-side artifacts. Cargo's own unstable documentation calls this behavior historically confusing, especially around linker and rustflags behavior when --target is or is not passed. :contentReference[oaicite:4]{index=4}
Using --target
The main way to cross-compile in Cargo is with --target.
Examples:
cargo build --target aarch64-unknown-linux-gnu
cargo test --target aarch64-unknown-linux-gnu
cargo run --target x86_64-unknown-linux-gnuWhen --target is passed, Cargo builds the selected package for that target triple instead of the default host target. Cargo's environment variable documentation and configuration reference both treat target triples as first-class build selectors. :contentReference[oaicite:5]{index=5}
A Small Example Package
Suppose you create a small package:
cargo new cross_demo
cd cross_demoSource:
fn main() {
println!("hello target world");
}A normal host build:
cargo buildA cross build:
cargo build --target aarch64-unknown-linux-gnuThe command changes which platform the final binary is compiled for, even though the package source is the same. :contentReference[oaicite:6]{index=6}
Per-Target Configuration in .cargo/config.toml
Cargo supports target-specific configuration in .cargo/config.toml. This is one of the main tools for practical cross-compilation because it lets you specify target-specific linkers, runners, and flags. :contentReference[oaicite:7]{index=7}
Example:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64"This tells Cargo how to link and, when relevant, how to run artifacts for that target.
Setting a Default Build Target
Cargo configuration can also set a default target under [build].
Example:
[build]
target = "aarch64-unknown-linux-gnu"With that in place, a plain command like:
cargo builduses the configured default target unless overridden. This is often convenient in dedicated cross-build environments. :contentReference[oaicite:8]{index=8}
Target-Specific Dependencies
Cargo supports target-specific dependency tables in Cargo.toml, which lets a crate depend on different crates depending on the target platform. :contentReference[oaicite:9]{index=9}
Example:
[target.'cfg(windows)'.dependencies]
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2"This is useful when the dependency surface itself changes by platform.
Platform-Specific Builds in Rust Code
Target-specific dependencies usually go together with target-specific code.
Example:
#[cfg(windows)]
pub fn platform_name() -> &'static str {
"windows"
}
#[cfg(unix)]
pub fn platform_name() -> &'static str {
"unix"
}A useful mental model is:
- target-specific dependencies control which crates enter the graph
#[cfg(...)]controls which Rust code is compiled
Linkers
Cross-compilation often requires a target-specific linker. Cargo configuration supports this directly. :contentReference[oaicite:10]{index=10}
Example:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"Without a suitable linker, Cargo may be able to compile Rust code but fail at the link step for the target platform.
Runners
Cargo configuration also supports target-specific runners. This is especially useful for cross-testing, emulation, or embedded workflows where the built artifact cannot run natively on the host system. :contentReference[oaicite:11]{index=11}
Example:
[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64"With this, commands that execute a target artifact can use the configured runner instead of trying to run the foreign binary directly.
A Practical Cross-Build Config Example
A minimal .cargo/config.toml for a Linux host targeting AArch64 Linux might look like this:
[build]
target = "aarch64-unknown-linux-gnu"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64"Now a normal build command:
cargo builduses that target by default, and a run-oriented workflow can use the configured runner.
Build vs Host Confusion in Configuration
Cargo's unstable-feature documentation explicitly notes historical confusion around whether linker and rustflags from [target], environment variables, and [build] apply to host artifacts such as build scripts when --target is and is not passed. That means cross-compilation configuration can be correct for final target artifacts while still behaving unexpectedly for host-side build tools if the distinction is not understood. :contentReference[oaicite:12]{index=12}
Target-Specific Rustflags
Cargo configuration can also apply target-specific rustflags.
Example:
[target.aarch64-unknown-linux-gnu]
rustflags = ["-C", "target-feature=+crt-static"]This can be useful when a target needs specific codegen or linker-facing options. But because host and target artifacts may behave differently, these settings should be used carefully in cross-build setups. :contentReference[oaicite:13]{index=13}
Std Availability
Not every target has the same level of standard-library support. The Rust platform-support pages document that some targets support only no_std today, and some targets do not ship precompiled standard library artifacts. For example, the x86_64-lynx-lynxos178 target currently supports only no_std programs and requires a custom-enabled Rust build for target support. :contentReference[oaicite:14]{index=14}
A useful mental model is:
- many common targets support ordinary
stdbuilds out of the box - some targets are more limited and require
no_stdor extra setup
Why Std Availability Matters
Cross-compilation is not just about finding a linker. It also depends on whether the target has the necessary Rust standard library artifacts available. If the target does not support std or does not ship the needed precompiled artifacts, the cross-compilation story becomes more advanced. :contentReference[oaicite:15]{index=15}
Cross Testing
Cross-testing is more complicated than cross-building because the test binaries must be executed somehow. In many cases, that means a target-specific runner such as QEMU or a device-specific execution path. Cargo's configuration reference supports runner, which is the main mechanism for this. :contentReference[oaicite:16]{index=16}
Example:
[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64"Then:
cargo test --target aarch64-unknown-linux-gnucan use the runner instead of assuming native execution.
Cross Testing vs Cross Building
A useful distinction is:
- cross-building only needs the ability to compile and link for the target
- cross-testing also needs a way to run the target artifact, either through emulation, remote execution, or target-native hardware
This is why a cross-build setup can be correct while cross-tests still fail until a runner or execution environment is configured.
Emulation Workflows
Emulation is a common cross-testing strategy. Cargo itself does not provide emulation, but its runner configuration makes it easy to integrate tools like QEMU into the workflow. :contentReference[oaicite:17]{index=17}
Example:
[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64"This is often the cleanest way to make cargo run or cargo test usable for foreign-architecture targets on a host machine.
Native Code in Cross Builds
Build scripts receive both HOST and TARGET, and Cargo's environment-variable reference explicitly states that native code should be compiled for TARGET. This matters when a crate uses build.rs to compile bundled C or C++ code, because the build script runs on the host but should generate native artifacts for the target. :contentReference[oaicite:18]{index=18}
Example build script:
fn main() {
let target = std::env::var("TARGET").unwrap();
println!("cargo::warning=building native code for {target}");
}A Small Cross-Aware Build Script
Suppose a crate wants to emit diagnostics about host and target during a build.
build.rs:
fn main() {
let host = std::env::var("HOST").unwrap();
let target = std::env::var("TARGET").unwrap();
println!("cargo::warning=host={host}");
println!("cargo::warning=target={target}");
}This is often a useful first debugging step when cross-compilation behavior is not what you expect.
Per-Target Config in a Workspace
In workspaces, target-specific config is often placed in a root .cargo/config.toml so all members share the same target policy.
Example layout:
workspace_demo/
āāā .cargo/
ā āāā config.toml
āāā Cargo.toml
āāā app/
āāā core/Root config:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64"This is often the cleanest way to keep workspace members aligned on cross-build behavior.
A Full Small Example
Suppose you have this package:
cross_demo/
āāā .cargo/
ā āāā config.toml
āāā Cargo.toml
āāā build.rs
āāā src/
āāā main.rs.cargo/config.toml:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64"build.rs:
fn main() {
let host = std::env::var("HOST").unwrap();
let target = std::env::var("TARGET").unwrap();
println!("cargo::warning=host={host} target={target}");
}src/main.rs:
fn main() {
println!("hello cross world");
}Useful commands:
cargo build --target aarch64-unknown-linux-gnu
cargo run --target aarch64-unknown-linux-gnu
cargo test --target aarch64-unknown-linux-gnuCommon Beginner Mistakes
Mistake 1: assuming host and target are the same thing.
Mistake 2: configuring a target linker but forgetting that build scripts and proc macros are host-side artifacts.
Mistake 3: trying to cross-test without configuring a runner or execution environment.
Mistake 4: assuming every target has ordinary std support and precompiled standard libraries.
Mistake 5: putting target-specific behavior in ad hoc scripts instead of .cargo/config.toml.
Mistake 6: changing --target behavior without checking whether build-script native compilation is using TARGET correctly.
Hands-On Exercise
Create a small package and make it cross-aware.
Start here:
cargo new cross_lab
cd cross_lab
mkdir -p .cargoCreate .cargo/config.toml:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64"Create build.rs:
fn main() {
println!("cargo::warning=HOST={}", std::env::var("HOST").unwrap());
println!("cargo::warning=TARGET={}", std::env::var("TARGET").unwrap());
}Use this program:
fn main() {
println!("hello target world");
}Then try:
cargo build
cargo build --target aarch64-unknown-linux-gnu
cargo run --target aarch64-unknown-linux-gnuCompare the host-target diagnostics from the two builds. This makes the distinction between native and cross builds concrete.
Mental Model Summary
A strong mental model for target triples and cross-compilation in Cargo is:
- the host runs Cargo, build scripts, and other host-side artifacts
- the target is the platform the final crate artifact is built for
--targetselects the target triple explicitly.cargo/config.tomlis the main place for per-target linkers, runners, and target defaults- target-specific dependencies and
#[cfg(...)]code are complementary tools - cross-building and cross-testing are related but different problems
- std availability and target support vary by platform
- many cross-compilation problems are really host-versus-target confusion problems
Once this model is stable, Cargo's cross-compilation behavior becomes much easier to reason about as a structured multi-platform workflow rather than a collection of special cases.
