Cross-Compilation for Multiple Platforms
Cross-Compilation for Multiple Platforms
Goal: Master native cross-compilation in Nixpkgs, cross-compiling C/C++, Rust, and Go binaries from an x86_64-linux host to aarch64-linux (ARM64) and riscv64-linux targets without resorting to slow QEMU emulation, distinguishing buildPlatform, hostPlatform, and targetPlatform accurately.
This chapter completes Part 6: Packaging Craft. While previous chapters focused on compiling for the native host platform, production platform engineering requires building software for distributed fleets of varied architectures—ARM64 cloud servers (AWS Graviton, Ampere), embedded boards (Raspberry Pi), and edge gateways.
The Cross-Compilation Problem: Why Emulation Fails
Traditional CI/CD pipelines build multi-architecture binaries using QEMU user emulation (binfmt_misc):
x86_64 CI Runner ──> [QEMU Software Translation] ──> Emulated aarch64 gcc / cargo
While emulation is conceptually simple, it incurs severe penalties: 1. Extreme Performance Penalty: Emulating an ARM64 instruction set on an x86_64 CPU slows execution down by 5x to 20x. Large C++ or Rust codebases that compile in 5 minutes natively take over an hour under QEMU. 2. Emulation Instability: QEMU user-mode frequently crashes on complex multithreaded compilers, memory barriers, or signal handlers. 3. Wasted Cloud Compute: Engineering organizations burn thousands of dollars in CI compute hours waiting for emulated compilers to finish.
The Solution: True cross-compilation. An x86_64 compiler binary runs natively at full x86_64 hardware speed while emitting machine code targeted for ARM64 or RISC-V.
The Nixpkgs Platform Model: Triples and System Types
Nixpkgs implements a rigorous, formalized model of cross-compilation based on the GNU toolchain conventions. Every derivation is evaluated in the context of three platforms:
| Platform Concept | Nix Attribute | Definition | Practical Meaning |
|---|---|---|---|
| Build Platform | stdenv.buildPlatform |
The platform where compilation executes | The CPU and OS running gcc, rustc, or go (e.g. x86_64-linux) |
| Host Platform | stdenv.hostPlatform |
The platform where the built package will run | The machine where the resulting application binary executes (e.g. aarch64-linux) |
| Target Platform | stdenv.targetPlatform |
The platform for which a compiler generates code | Only relevant when building compilers (e.g. building an ARM64-targeted compiler) |
Build Platform: x86_64-linux (Your laptop / CI Runner)
│
▼ [Cross-Compiler runs at native x86_64 speed]
Host Platform: aarch64-linux (Your AWS Graviton or Raspberry Pi server)
In standard native builds, buildPlatform == hostPlatform. In cross-compilation, buildPlatform != hostPlatform.
Instant Cross-Toolchains with pkgsCross
Nixpkgs includes pre-configured cross-compilation package sets under pkgsCross. You do not need to compile a custom cross-toolchain from scratch; Nix provides them out of the box:
# Query available cross sets in the REPL
nix repl '<nixpkgs>'
nix-repl> builtins.attrNames pkgsCross
[ "aarch64-multiplatform" "armv7l-hf-multiplatform" "ppc64le" "riscv64" "x86_64-embedded" ... ]To cross-compile any existing package from Nixpkgs for ARM64:
# Cross-compile GNU Hello from x86_64 to aarch64
nix build nixpkgs#pkgsCross.aarch64-multiplatform.hello
# Verify the resulting ELF binary architecture
file ./result/bin/hello
# Output: ELF 64-bit LSB executable, ARM a-arch64, version 1 (SYSV), dynamically linked ...Notice that hello was built without executing a single ARM instruction on your machine.
Classifying Inputs: nativeBuildInputs vs buildInputs
The most critical rule in cross-compiling Nix derivations is placing dependencies in the correct input list:
| Input Attribute | Target Execution Platform | What Belongs Here |
|---|---|---|
nativeBuildInputs |
Build Platform (buildPlatform) |
Compilers, code generators, build systems (cmake, pkg-config, bison, protobuf, cargo) |
buildInputs |
Host Platform (hostPlatform) |
Libraries and headers that the binary links against (openssl, glibc, zlib, sqlite) |
depsBuildBuild |
Build Platform (buildPlatform) |
Tools required to build tools for the build platform |
If you accidentally place pkg-config in buildInputs, Nix will attempt to run an ARM64 binary on your x86_64 machine, crashing with Exec format error.
Language Ecosystems in Cross-Compilation
1. Cross-Compiling C/C++ Projects (CMake)
When using stdenv with cmake, Nixpkgs automatically generates a CMake toolchain file configured with the target triple:
# packages/my-c-service/default.nix
{ stdenv, lib, fetchFromGitHub, cmake, pkg-config, openssl, zlib }:
stdenv.mkDerivation rec {
pname = "sensor-service";
version = "1.2.0";
src = fetchFromGitHub {
owner = "example-corp";
repo = pname;
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
# Build-time tools running on the builder CPU
nativeBuildInputs = [ cmake pkg-config ];
# Target libraries for ARM64
buildInputs = [ openssl zlib ];
# CMake automatically receives CMAKE_SYSTEM_NAME and CMAKE_C_COMPILER
meta = with lib; {
description = "C++ sensor processing service";
platforms = platforms.linux;
};
}2. Cross-Compiling Rust with Cargo
rustPlatform.buildRustPackage natively supports cross-compilation when invoked from pkgsCross:
# packages/rust-service/default.nix
{ lib, rustPlatform, fetchFromGitHub, pkg-config, openssl }:
rustPlatform.buildRustPackage rec {
pname = "telemetry-agent";
version = "0.4.1";
src = fetchFromGitHub {
owner = "example-corp";
repo = pname;
rev = "v${version}";
hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
};
cargoHash = "sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=";
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl ];
# Disable tests during cross-compilation (since host binaries cannot run on build machine)
doCheck = false;
}Building this via nix build .#pkgsCross.aarch64-multiplatform.telemetry-agent uses an x86_64 rustc with --target aarch64-unknown-linux-gnu and cross-compiles OpenSSL cleanly.
3. Cross-Compiling Go Modules
Go has native cross-compilation built into its compiler via GOOS and GOARCH. buildGoModule handles this seamlessly:
# packages/go-api/default.nix
{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "gateway-api";
version = "2.1.0";
src = fetchFromGitHub {
owner = "example-corp";
repo = pname;
rev = "v${version}";
hash = "sha256-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=";
};
vendorHash = "sha256-EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=";
# If CGO is required, ensure cross-gcc is used
CGO_ENABLED = 0; # Pure Go cross-compiles instantly
doCheck = false;
}Flake Pattern for Multi-Architecture Outputs
Here is how to expose cross-compiled packages in your flake.nix:
{
description = "Multi-Architecture Cross-Compilation Flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
};
outputs = { self, nixpkgs }:
let
# Build platform is x86_64-linux
pkgsNative = import nixpkgs { system = "x86_64-linux"; };
# Cross package sets
pkgsCrossAarch64 = import nixpkgs {
system = "x86_64-linux";
crossSystem = { config = "aarch64-unknown-linux-gnu"; };
};
pkgsCrossRiscv64 = import nixpkgs {
system = "x86_64-linux";
crossSystem = { config = "riscv64-unknown-linux-gnu"; };
};
in {
packages.x86_64-linux = {
# Native build
default = pkgsNative.callPackage ./packages/my-service.nix {};
# Cross-compiled builds for ARM64 and RISC-V
arm64 = pkgsCrossAarch64.callPackage ./packages/my-service.nix {};
riscv = pkgsCrossRiscv64.callPackage ./packages/my-service.nix {};
};
};
}Now, from an x86_64 laptop or CI server, simply run:
nix build .#arm64
nix build .#riscvCommon Cross-Compilation Pitfalls
| Failure Symptom | Root Cause | Fix |
|---|---|---|
Exec format error during build |
A host-architecture binary was executed on the build machine (e.g. running unit tests or a code generator) | Set doCheck = false; during cross builds. Ensure code generators are placed in nativeBuildInputs. |
Cannot find -lssl |
Dynamic library placed in nativeBuildInputs instead of buildInputs |
Move libraries the application links with into buildInputs. |
pkg-config outputs host paths |
Using host pkg-config instead of target cross-wrapper |
Always pull pkg-config from nativeBuildInputs; Nix provides the target-prefixed wrapper (e.g. aarch64-unknown-linux-gnu-pkg-config). |
Checkpoint
- Explained the difference between
buildPlatform,hostPlatform, andtargetPlatform. - Cross-compiled a binary using
pkgsCross.aarch64-multiplatformwithout running QEMU. - Correctly separated host libraries in
buildInputsfrom build tools innativeBuildInputs. - Exported multi-architecture cross builds cleanly in
flake.nix.