Cross-Compilation for Multiple Platforms

Updated

September 12, 2026

Cross-Compilation for Multiple Platforms

Hand-rolled aarch64-linux-gnu-gcc prefixes are a wiki. The boring default is: pkgs.pkgsCross.<target> on nixpkgs 26.05, or a native remote builder when you need speed and KVM.

Mental model

Platform Meaning
build Machine compiling (laptop)
host Machine running the output
target What a compiler emits (only when building gcc)

pkgsCross.aarch64-multiplatform.hello is hello for ARM, built on x86_64 with a cross toolchain.

Remote builder (ssh-ng to an ARM box) is native. Prefer it for Go, kernels, and NixOS tests. Cross is fine for small C. Go also has GOOS/GOARCH (its own chapter) — that is not pkgsCross.

x86_64 laptop
    pkgsCross.aarch64-multiplatform.stdenv.cc
            │
            ▼
    ELF aarch64  (cannot run here without qemu-user)

Worked examples

Case 1: Cross hello

nix build -f '<nixpkgs>' pkgsCross.aarch64-multiplatform.hello
file result/bin/hello

Output (shape):

result/bin/hello: ELF 64-bit LSB executable, ARM aarch64, …

You cannot run it on x86_64 without qemu-user. file first, then decide.

Case 2: Your C program

Save as cross_demo.nix:

# cross_demo.nix
let
  pkgs = import <nixpkgs> { };
  armPkgs = pkgs.pkgsCross.aarch64-multiplatform;
in
armPkgs.stdenv.mkDerivation {
  pname = "desk-arm";
  version = "1.0";
  src = pkgs.writeTextDir "main.c" ''
    #include <stdio.h>
    int main(void) {
      puts("desk-arm");
      return 0;
    }
  '';
  buildPhase = ''
    $CC $src/main.c -o desk-arm
  '';
  installPhase = ''
    mkdir -p $out/bin
    cp desk-arm $out/bin/
  '';
}
nix-build cross_demo.nix
file result/bin/desk-arm

$CC is the cross compiler. Do not hardcode gcc. The sandbox’s gcc would emit x86_64.

Case 3: nativeBuildInputs vs buildInputs

Save as zlib-arm.nix fragment inside a derivation:

# zlib-arm.nix
{ stdenv, pkg-config, zlib }:

stdenv.mkDerivation {
  pname = "desk-z";
  version = "1.0";
  src = ./.;
  nativeBuildInputs = [ pkg-config ]; # runs on the laptop
  buildInputs = [ zlib ];             # links into the ARM binary
  doCheck = stdenv.buildPlatform == stdenv.hostPlatform;
}

Tests that execute the binary must be off when crossing (doCheck false). pkg-config is a build tool; zlib is a host library. Swap them and the ARM binary links x86 zlib — or eval fails.

Case 4: Flake output for the ARM artifact

Save as flake.nix:

# flake.nix
{
  description = "Desk ARM binary";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in
    {
      packages.x86_64-linux.desk-arm =
        pkgs.pkgsCross.aarch64-multiplatform.callPackage ./cross_demo.nix { };
    };
}
nix build .#desk-arm
file result/bin/desk-arm

The package lives under packages.x86_64-linux because that is where it was built. The ELF is still aarch64. Do not confuse flake system with file output.

Case 5: Prefer a builder for NixOS

nixos-rebuild / nixosTest for aarch64: remote aarch64-linux builder with kvm. Cross-compiling a full NixOS closure is possible and painful.

nix build .#packages.aarch64-linux.desk-api --max-jobs 0

--max-jobs 0 forces the remote builder. If you have no builder, that error is correct — do not reach for qemu-binfmt as the release path.

The trap

The trap is running ./result/bin/desk-arm on the laptop and calling the compiler broken. file first. qemu-user if you must execute; a builder if you must test for real. The other trap is doCheck = true on a cross drv that execs the binary.

The boring rule

  • pkgsCross.* for small C. Native builder for OS and Go.
  • $CC, not gcc. nativeBuildInputs vs buildInputs.
  • doCheck only when build == host.
  • 26.05 pin so the cross toolchain matches prod.
  • file the output. Flake system is the builder, not the ELF.

Try this

  1. nix eval --raw -f '<nixpkgs>' 'pkgsCross.aarch64-multiplatform.stdenv.cc' | xargs basename
  2. Case 1 file.
  3. Enable doCheck = true on a cross drv that runs the binary; watch it fail; add the platform guard.
  4. nix eval --expr 'builtins.attrNames (import <nixpkgs> {}).pkgsCross' and pick one target you actually own.