A hands-on technical reference for platform engineers operationalizing Nix-based SBOMs, CVE remediation, and supply chain controls in real C

Cross-Compilation in Nix for Aarch64 Targets From X86-64 Hosts

Choose between native cross-compilation and QEMU emulation based on your build's cache hit rate.

· 7 min read
Cover illustration for “Cross-Compilation in Nix for Aarch64 Targets From X86-64 Hosts”
Environment Parity · October 31, 2026 · 7 min read · 1,673 words

Nix offers two paths for producing AArch64 binaries from x86-64 hosts, namely native cross-compilation through pkgsCross and QEMU-backed emulation through binfmt_misc. The right choice depends on whether the workflow spends more time compiling source or retrieving cached dependencies. Cross-compilation avoids emulation overhead, while emulation preserves compatibility with the upstream AArch64 binary cache.

Why x86-64 teams increasingly need to produce AArch64 binaries

AWS Graviton, Ampere Altra, and Apple Silicon build farms are common destinations for code developed on x86-64 machines. Development laptops and CI runners therefore do not necessarily share the architecture of the systems that run their output. A team producing AArch64 artifacts needs its build tools, libraries, and image configuration to agree about which machine executes each part of the build.

Nix handles that difference through its derivation model, so each project can use the same way to set up its build. Build inputs become part of a declared dependency graph, including the platform choices that determine how packages are built. Architecture becomes part of the build definition. That gives teams a common way to describe the work, even when they choose different mechanisms for executing it.

How Nix models the build/host platform distinction

Nix names the machine running the compiler buildPlatform and the machine that will run the resulting binary hostPlatform. These roles coincide in an ordinary native build. Cross-compilation deliberately separates them. For an x86-64 compiler producing an AArch64 Linux executable, buildPlatform is x86_64-linux, while hostPlatform is aarch64-linux, with “host” referring to the destination for the compiled program.

Tools executed during the build belong in nativeBuildInputs, with buildPackages providing packages for buildPlatform. Libraries needed by the resulting program belong in buildInputs for hostPlatform. Compiler targets introduce a further distinction. The separate, rarely used targetPackages attribute concerns standard libraries and runtime dependencies needed by compiler targets.

Native cross-compilation gives buildPlatform and hostPlatform different values. Emulation instead presents an AArch64 build environment to Nix, with both roles set to aarch64-linux, while QEMU executes its programs on the physical x86-64 machine. The hardware remains x86-64 in either case. What changes is whether the compiler itself runs as an x86-64 program or an emulated AArch64 program.

Path one: native cross-compilation toolchain via pkgsCross

Native cross-compilation uses an x86-64 GCC or Clang toolchain that emits AArch64 binaries without instruction-set emulation. The compiler runs at the build machine’s full speed. Nixpkgs exposes prepared cross-compilation package sets through pkgsCross, including pkgsCross.aarch64-multiplatform. A complete command for building the example package is nix build nixpkgs#legacyPackages.x86_64-linux.pkgsCross.aarch64-multiplatform.hello. It produces an AArch64 executable while the compilation work runs on x86-64.

By contrast, hugo, when used only as a build-time tool, must come from pkgsCross.aarch64-multiplatform.buildPackages. Otherwise, Nix attempts to cross-compile the tool unnecessarily. A debugger such as gdb needs special handling because its execution platform and the platform it targets serve different roles.

The nix-cross-toolchains flake provides ready-to-use environments for aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, and aarch64-unknown-linux-musl-static. Within that flake, nix develop .#aarch64-unknown-linux-gnu enters the first of these environments. Cross-compiled artifacts have different derivation hashes from their natively built AArch64 counterparts, so the upstream NixOS binary cache cannot supply those outputs. Dependencies must be built on the local machine unless a self-hosted cache has the cross-compiled artifacts.

Path two: QEMU-backed emulation via binfmt_misc

The emulation path registers AArch64 executables with the Linux kernel through binfmt_misc. The operating system then routes those executables to QEMU transparently. No cross-targeting compiler is needed for this path. From Nix’s perspective, the build is an ordinary AArch64 build, with both buildPlatform and hostPlatform set to aarch64-linux. On NixOS, the enabling configuration is boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; in configuration.nix.

That arrangement preserves the derivation identity of a native AArch64 build. Consequently, packages available in the upstream NixOS binary cache can download, avoiding compilation under emulation. The time saved can outweigh the time spent running AArch64 instructions through QEMU, especially when the dependency tree is large and much of it is in the cache. Uncached work still pays the emulation penalty. The substantial slowdown for a trivial program illustrates the direction of that overhead, but it is not a general benchmark for applications or complete image builds.

The setup also depends on the operating system. binfmt_misc binfmt_misc requires a Linux kernel, so macOS developers need a VM layer. Ubuntu CI runners can enable the mechanism through qemu-user-static and binfmt-support. That takes more setup work outside the package definition. Emulation therefore trades some machine-level setup and slower source builds for access to the same cached packages that a native AArch64 machine would use.

Choosing between the two paths based on what the build actually costs

Compiler throughput alone cannot settle the choice. A native cross compiler may execute faster while spending its time rebuilding dependencies that an emulated build would simply download. Conversely, cache compatibility offers little advantage when a workflow changes enough packages that most of the graph must be compiled anyway.

For standard nixpkgs packages that are mostly in the upstream cache, emulation is the easier default. It also fits workflows with few build machines that accept longer builds to limit the cost of getting started. Complete bootable NixOS images are another strong case when compatibility with the AArch64 channel’s cached outputs is required. Here, the cache does most of the work.

Custom or patched packages shift the balance toward native cross-compilation because their builds cannot rely on those upstream artifacts. Heavily customized embedded and appliance images have the same tendency. An existing self-hosted binary cache further strengthens this path by retaining the cross-compiled results for reuse. The relevant comparison becomes the cost of producing and maintaining the team’s artifacts.

For the Linux-target workflows described here, macOS developers need a Linux VM or remote builder for either path. Improvements in aarch64-darwin package compatibility do not remove that Linux execution requirement, and some packages remain problematic. CI can use emulation where cache hits are frequent, while developers on non-NixOS Linux machines use cross-compilation for custom-package iteration.

Flake patterns for producing AArch64 images from an x86-64 host

A flake can expose packages.x86_64-linux.image-aarch64 as a separate output derived from the same NixOS configuration. That output calls extendModules to inject the architecture-specific settings rather than maintaining an independent configuration for the image. For native cross-compilation, the added module contains nixpkgs.buildPlatform = "x86_64-linux"; and nixpkgs.hostPlatform = "aarch64-linux";. The extension changes where compilation occurs and where the resulting image runs.

Once that output is defined, nix build .#image-aarch64 produces the complete bootable AArch64 image from the x86-64 machine. The command selects the flake output rather than specifying the compiler and every dependency separately. For emulation, the same structure retains the AArch64 hostPlatform but omits the x86-64 buildPlatform override. The builder must already have binfmt_misc enabled. Under that condition, the AArch64 build programs execute through QEMU.

Executable build tools belong in nativeBuildInputs or come through the cross package set’s buildPackages, rather than being selected indiscriminately from the cross namespace. Image configuration cannot fix shell tools chosen for the wrong platform. The NixOS Wiki uses callPackage in its primary development-shell example because it resolves the appropriate architecture for dependencies automatically.

Supply chain visibility as a byproduct of the content-addressed build graph

The same input graph used to describe an AArch64 build also provides a basis for tracing its provenance. A build output’s input-derived identifier can be attached to a deployment and associated with the dependencies used to produce it. This supports later CVE searches without requiring a fresh image scan for each search. The graph supplies the audit starting point. Turning it into an SBOM or an attestation still requires tooling that understands Nix derivations.

The available tools operate at different layers. bombon generates SBOMs at the .nix expression level, where it can access package metadata. genealogos uses nixtract for dependency introspection and represents the results hierarchically. sbomnix, from TIIUAE, works at the .drv level and supports build-time and runtime pruning. The last tool also includes a provenance CLI that emits SLSA v1.0-compliant attestation JSON for a Nix flake or derivation.

For production images, build-time and runtime dependencies answer different questions. Cross-compilation makes that distinction especially useful because tools running on x86-64 need not be components of the deployed AArch64 system. Derivation-aware tooling can use the declared graph rather than reconstructing the dependency history solely from the finished image. Provenance is therefore connected to the build itself. It becomes an operational output when the team includes the appropriate SBOM and attestation tooling in its workflow.

CI/CD patterns that enforce architecture parity across the pipeline

A cross-build that works on a local machine does not prove it matches CI if the runner resolves a different nixpkgs input. Pinning that input in flake.lock and committing the lock file is the minimum requirement. The pin must travel with the build definition. For enterprise pipelines, disabling network access during the build phase provides a stronger constraint on inputs than a shared shell alone. These controls make architecture parity part of the pipeline rather than a property assumed from one developer’s successful run.

GitHub Actions can enter the project environment through the nix develop -c bash -lc "..." invocation pattern, with the pipeline command replacing the placeholder. That uses the flake’s development shell for the cross toolchain, dependency versions, and environment variables. The shell definition is shared rather than recreated. It does not, however, replace the Linux VM, remote builder, or kernel configuration required by the selected execution path.

For native cross-compilation, a self-hosted binary cache prevents the same cross-compiled dependencies from being rebuilt on every CI run. This is the prerequisite for making that path competitive with emulation across repeated builds. The arnarg/nixidy project demonstrates the Rendered Manifests Pattern for Kubernetes, in which Nix generates plain YAML, teams review exact changes in pull requests, and Argo CD deploys the manifests.

A committed lock file only helps when local development and CI both use it, and a shared cache only helps when the pipeline consistently reuses its artifacts. The organizational requirement is ownership of those conventions. Toolchain updates, image changes, and deployment manifests need to pass through the same versioned workflow so that a new engineer inherits the team’s build environment rather than reconstructing it from setup instructions.

Sources

  1. Cross Compiling - Official NixOS Wiki
  2. Cross compilation — nix.dev documentation
  3. Nixpkgs Reference Manual
  4. NixOS on ARM/Building Images - Official NixOS Wiki
  5. Recommended style to cross-compile flake nixosSystems - Help - NixOS Discourse
  6. How do I cross-compile a flake? - Help - NixOS Discourse

More in Environment Parity