Cross-Compilation for Multiple Platforms
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/helloOutput (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-armThe 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, notgcc.nativeBuildInputsvsbuildInputs.doCheckonly when build == host.- 26.05 pin so the cross toolchain matches prod.
filethe output. Flakesystemis the builder, not the ELF.
Try this
nix eval --raw -f '<nixpkgs>' 'pkgsCross.aarch64-multiplatform.stdenv.cc' | xargs basename- Case 1
file. - Enable
doCheck = trueon a cross drv that runs the binary; watch it fail; add the platform guard. nix eval --expr 'builtins.attrNames (import <nixpkgs> {}).pkgsCross'and pick one target you actually own.