Error

"target not found" When Cross-Compiling — How to Fix

Fix the 'target not found' error by installing the missing cross-compilation target using the rustup target add command.

When the compiler doesn't speak the target's language

You've written a Rust program on your laptop. It works perfectly. Now you want to run it on a headless server, an embedded device, or a browser. You fire up the terminal, type the command to build for that other machine, and the compiler stops you dead.

error: failed to run `rustc`: process didn't exit successfully: ... target not found

The compiler doesn't know how to speak to that other machine. You haven't installed the translator. Rust is a cross-compilation powerhouse, but it requires you to explicitly add support for every architecture and operating system combination you want to build. The error means your toolchain is missing the standard library for that specific target.

What is a target triple?

Rust identifies every build target with a string called a target triple. The name comes from the original three-part format, but modern targets have four parts separated by hyphens. Each part describes a constraint on the binary you are generating.

The format is architecture-vendor-os-environment.

  • Architecture defines the CPU instruction set. Common values are x86_64 for modern desktops, aarch64 for ARM64 chips like Apple Silicon or Raspberry Pi 4, armv7 for 32-bit ARM, and wasm32 for WebAssembly.
  • Vendor identifies the manufacturer. unknown is the modern standard for Linux and most embedded systems. apple is used for macOS. pc appears in some Windows targets.
  • OS specifies the operating system. linux, windows, macos, and none for bare-metal embedded devices.
  • Environment describes the ABI and runtime libraries. gnu means the binary links against glibc. musl means it links against musl libc. eabi is used for embedded ARM. emscripten appears in some WebAssembly targets.

Think of the target triple like a shipping label combined with a power adapter spec. You need to know where the package goes, what language the receiver speaks, who made the device, and what kind of electricity it uses. If you send a US plug to a UK socket, nothing works. Rust needs the exact spec to generate code that fits.

A common convention in the Rust community is to use unknown for the vendor whenever possible. Old tutorials might show aarch64-linux-gnu, but the current standard is aarch64-unknown-linux-gnu. The unknown vendor signals that the target is generic Linux, not tied to a specific vendor's toolchain.

The fix: installing the target

The solution is to tell rustup to download the standard library for the missing target. rustup manages the Rust toolchain and handles these downloads automatically.

/// A simple program to verify the cross-compilation setup.
fn main() {
    println!("Hello from the cross-compiled binary!");
}

Run the install command with the exact target triple you need.

rustup target add aarch64-unknown-linux-gnu

Once the download finishes, you can build for that target.

cargo build --target aarch64-unknown-linux-gnu

The build succeeds. The binary lives in target/aarch64-unknown-linux-gnu/debug/your-project. You can copy this file to your ARM device and run it.

What happens under the hood

When you run rustup target add, you are not installing a new compiler. You are downloading a pre-compiled version of the Rust standard library for that target. Rust's architecture separates the code generator from the standard library. The same rustc binary can generate code for dozens of targets, but it needs the matching libstd.rlib to link against.

This design makes cross-compilation fast and reliable. You don't need to compile a cross-compiler from source. You don't need a complex toolchain setup. You just download the library component. The download is usually small, often under 10 MB, and rustup caches it for future builds.

Convention aside: use rustup target list to see every target your toolchain supports. Add --installed to see what you have, or --available to see what you can add. This command is your reference when you're unsure of the exact triple.

Realistic setup: locking targets for a team

In a real project, you want every developer to have the same targets installed. You don't want Alice building for wasm32-unknown-unknown while Bob gets an error because he forgot to add it.

The standard practice is to declare targets in a rust-toolchain.toml file at the root of your project. This file pins the Rust version and lists required targets.

[toolchain]
channel = "stable"
targets = ["wasm32-unknown-unknown", "aarch64-unknown-linux-gnu"]

When a developer runs cargo build in this directory, rustup automatically checks for the listed targets and installs any missing ones. This removes the friction of manual setup. The file is part of version control, so the configuration travels with the code.

Trust the toolchain file. It keeps your team synchronized and prevents "works on my machine" errors caused by missing targets.

Pitfalls and the second boss

Installing the target fixes the immediate error, but cross-compilation often reveals deeper issues. The "target not found" error is usually the first boss. The linker is the second.

Wrong target triple

The compiler rejects invalid triples immediately. If you type aarch64-linux-gnu, you get an error because that triple doesn't exist in the current toolchain. The vendor part matters. Use unknown for Linux. Use apple for macOS. Use pc for Windows.

Check the triple carefully. A single typo in the environment part can break the build. aarch64-unknown-linux-gnu fails because the environment is gnu, not gnu with a typo. The error message usually points out the missing component.

Linker not found

Once the target is installed, the build might fail with a linker error.

error: linker `aarch64-linux-gnu-gcc` not found

Rust uses a linker to combine object files into a binary. For cross-compilation, the linker must match the target architecture. Your host machine doesn't have an ARM linker by default. You need to install a cross-linker.

On macOS, you can install osxcross or use a Docker container. On Linux, you can install gcc-aarch64-linux-gnu via your package manager. On Windows, you might need aarch64-linux-gnu-gcc from MSYS2 or WSL.

The cc crate, which many Rust projects use to compile C code, looks for environment variables to find the cross-linker. You can set these in your shell.

export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc

The variable name follows the pattern CARGO_TARGET_<TRIPLE>_LINKER with the triple uppercased and hyphens replaced by underscores. This tells the build system exactly which linker to use.

Sysroot and C dependencies

If your project depends on C libraries like OpenSSL or zlib, cross-compilation gets harder. The compiler needs the headers and static libraries for the target. These live in a sysroot.

Without a sysroot, the build fails with missing header errors. You have to install the cross-development packages for your dependencies. On Ubuntu, this might be libssl-dev:aarch64. On macOS, you might need to build the libraries yourself or use a tool that provides them.

This is where manual cross-compilation becomes painful. You have to manage the sysroot, the linker, and the package versions for every target.

Musl vs Gnu

The environment part of the triple affects how your binary links to C libraries. The gnu environment links against glibc, which is dynamic by default. Your binary depends on the glibc version installed on the target machine. If the target has an older glibc, your binary crashes.

The musl environment links against musl libc, which is static by default. Your binary includes the C library inside itself. The result is a larger binary that runs on any Linux system, regardless of the glibc version.

Use aarch64-unknown-linux-musl when you want a portable binary that runs on Alpine Linux or other minimal distributions. Use aarch64-unknown-linux-gnu when you are targeting a standard distribution and want smaller binaries.

WebAssembly variants

WebAssembly has multiple targets. wasm32-unknown-unknown is the standard target for the browser. It has no access to the file system or network. It relies on JavaScript glue code for I/O.

wasm32-wasip1 targets WASI, the WebAssembly System Interface. It allows the binary to access the file system and environment variables. This is used for server-side WebAssembly runtimes.

Pick the target that matches your runtime. The browser needs unknown-unknown. A WASI runtime needs wasip1.

Decision: choosing your cross-compilation path

Cross-compilation ranges from simple to complex. Choose the tool that matches your needs.

Use rustup target add when you need a quick build for a target with no C dependencies and you have the necessary linker installed. This works for pure Rust projects and simple binaries.

Use the cross tool when your project depends on C libraries or you are targeting an obscure architecture. cross uses Docker or QEMU to emulate the target environment, providing the sysroot and linker automatically. It handles the hard parts for you.

Use wasm-pack when you are building for the browser and need JavaScript bindings. It wraps wasm32-unknown-unknown compilation and generates the glue code needed to call your Rust from JS.

Use native compilation when you are developing and testing on the target machine itself. Cross-compilation is for deployment. Native builds are faster for iteration and debugging.

Where to go next