Cross Compilation
Cross compilation is compiling on one architecture to produce a binary that runs on another. Target triples, sysroots, and a GitHub Actions example.
Cross compilation is compiling source on a machine of one architecture to produce a binary that runs on a different architecture, using a toolchain configured to emit code for that other target. An x86-64 build machine can therefore produce an ARM64 executable that the build machine itself is unable to run.
In GitHub Actions this appears whenever a workflow ships artifacts for more than one architecture or operating system. A single job on a single runner can emit several targets, because the compiler picks the output architecture from a flag rather than from the hardware underneath it.
Definition
Toolchain documentation still uses the three machine roles the GNU build system established:
- Build: the machine that compiled the toolchain itself.
- Host: the machine that runs the compiler. In GitHub Actions the host is the runner.
- Target: the machine that runs the compiler's output.
A native build puts host and target on the same architecture. A cross build separates them, and everything else about cross compilation follows from that separation.
A working cross toolchain has four parts. A missing part is where most cross builds fail:
- A compiler backend that emits instructions for the target instruction set.
- A linker and binutils that understand the target object format.
- A sysroot holding the target's headers and libraries, so includes resolve and the link step finds the libc for the target ABI.
- A standard library or runtime compiled for the target, in languages that ship one.
The target is named by a target triple, conventionally written arch-vendor-os-abi even though the field count varies by toolchain. aarch64-unknown-linux-gnu asks for 64-bit ARM instructions, a Linux kernel interface, and the GNU C library ABI. Changing the final field to musl changes the libc the binary links against and yields a different artifact from identical source. Rust publishes its full list on the platform support page, and GCC documents the naming convention under configure terms.
Every mainstream toolchain exposes the target as a build-time selector:
| Toolchain | How the target is selected | Example value |
|---|---|---|
| Go | GOOS and GOARCH environment variables | GOOS=linux GOARCH=arm64 |
| Rust | --target plus a std library added by rustup target add | aarch64-unknown-linux-gnu |
| Clang | --target plus --sysroot | aarch64-linux-gnu |
| GCC | a target-prefixed driver binary | aarch64-linux-gnu-gcc |
| .NET | --runtime, or the RuntimeIdentifier property | linux-arm64 |
| Zig | -target | aarch64-linux-gnu |
Cross compilation and emulation solve the same problem from opposite ends. A cross compiler writes out target instructions while every process on the machine stays native, so the build executes at host speed and no target code ever runs. An emulator translates target instructions at run time, which lets a native toolchain and a test suite for the target execute on host hardware at a translation cost. The second path is covered in QEMU emulation.
A green cross build proves that the source compiled and linked for the target triple. Nothing executed the output, so run-time behavior stays unverified. Weak memory ordering on aarch64, the unsigned default for char on ARM64, and a sysroot glibc newer than the deployment target are invisible to the compiler and surface the first time the binary runs on real target hardware.
Example
This workflow cross compiles a Rust binary for 64-bit ARM Linux on an x86-64 runner. The target triple is stated explicitly on the cargo build line, and the job never executes the artifact it produces.
name: release
on:
push:
tags: ["v*"]
jobs:
build-arm64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup target add aarch64-unknown-linux-gnu
- run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
- run: cargo build --release --target aarch64-unknown-linux-gnu
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- uses: actions/upload-artifact@v4
with:
name: app-aarch64
path: target/aarch64-unknown-linux-gnu/release/appEach step supplies one part of the toolchain listed above:
rustup target adddownloads a standard library precompiled foraarch64-unknown-linux-gnu.- The
gcc-aarch64-linux-gnupackage installs the cross linker and the target sysroot from the Ubuntu archive. CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKERpoints Cargo at that linker, because the hostccis unable to link aarch64 objects.--targetselects the code generation backend and the ABI.
The artifact lands under target/aarch64-unknown-linux-gnu/release/, and file identifies it as an aarch64 object rather than an x86-64 one:
app: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linkedRunning that binary on the same runner returns cannot execute binary file: Exec format error, which is the expected result and the clearest sign that the cross build worked.
Go reaches the same output with two environment variables and no extra packages, because the Go toolchain ships every target it supports:
- run: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-arm64 ./cmd/appSetting CGO_ENABLED=1 puts that build back on a C toolchain, so a CC variable pointing at a cross compiler becomes necessary again. The same rule governs Python wheels with native extensions, Node addons built by node-gyp, and any JNI library in a JVM project.
Runner images differ in which cross toolchains they carry, so a workflow either installs the packages it needs in a step or selects an image that already ships them. Preinstalled software lists the toolchains on each image, and cloud runners lists the labels a workflow can request.
Related Terms
- Cross compiling compared with native ARM64 builds: when a cross-compiled artifact is enough, and when a job has to run on ARM64 hardware.
- QEMU emulation in Docker builds: executing target instructions on the host instead of compiling for the target.
- Building a Rust binary for multiple targets: a matrix that produces one artifact per target triple.
- Runner labels available to a workflow: the runner types a
runs-onlabel can select. - WarpBuild pricing: per minute rates by runner type.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.