Cross-Compilation for Multiple Platforms

Updated

October 31, 2026

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.

Note

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 .#riscv

Common 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, and targetPlatform.
  • Cross-compiled a binary using pkgsCross.aarch64-multiplatform without running QEMU.
  • Correctly separated host libraries in buildInputs from build tools in nativeBuildInputs.
  • Exported multi-architecture cross builds cleanly in flake.nix.