交叉编译与 overlay:异构构建全攻略

为树莓派、ARM 服务器交叉编译是 Nix 的王牌场景。本文深入 nixpkgs overlay 机制、cross 工具链(aarch64 目标)、lib.genAttrs 多架构矩阵、异构编译流程与常见坑,带你写出可同时产出 x86_64 与 aarch64 产物的 flake。

1. 交叉编译为什么难,Nix 为什么能做好

传统交叉编译的痛点:

  • 工具链混乱:arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc 各种前缀,依赖库也要为 target 编译
  • 依赖地狱:一个库的交叉编译依赖链很长,手工管理必然出错
  • 环境漂移:编译机的 glibc/内核头文件影响产物

Nix 的两大机制让它成为交叉编译的最优解:

  1. 三平台分离模型:hostPlatform(运行平台)、buildPlatform(编译平台)、targetPlatform(生成代码的平台),三者可自由组合
  2. overlay 机制:在不改动 nixpkgs 源码的情况下替换、注入、修正任意包的平台配置

本文从 overlay 讲起,再讲 cross 工具链与多架构 flake 布局,最后用 Cachix 缓存打通「构建机一次构建、多平台复用」(配合 https://plumephp.com/nix-ci-cachix/)。

📌 相关专题:语言层面见 https://plumephp.com/nix-language-deep-dive/(fix/extends 是 overlay 的底层);CI 多架构矩阵见 https://plumephp.com/nix-ci-cachix/;Flakes 布局见 https://plumephp.com/nix-flakes-best-practices/。


2. overlay:不改源码的「补丁层」

2.1 overlay 的本质

overlay 是形如 final: prev: { ... } 的函数,在 pkgs 求值完成后做一层叠加。final 是叠加后的完整 pkgs,prev 是叠加前的 pkgs——两者都是惰性的,所以可以互相引用:

# overlay 的签名:final = 叠加后的包集,prev = 原始包集
final: prev:
{
  # 新增包:用 prev 里的组件构建新包
  myapp = prev.callPackage ./pkgs/myapp.nix { };

  # 覆盖已有包:从 prev 取,改依赖
  hello = prev.hello.overrideAttrs (old: {
    configureFlags = old.configureFlags ++ [ "--enable-nls" ];
  });
}

2.2 overlay 能做什么

能力写法场景
新增包myapp = ...私有软件、新包
覆盖已有包pkg = prev.pkg.override { ... }改依赖版本
覆盖依赖树pkg = prev.pkg.overrideAttrs (old: { ... })改编译参数
修正补丁pkg = prev.pkg.overrideAttrs (old: { patches = old.patches ++ [ ./fix.patch ]; })临时修复上游 bug
注入平台配置pkg = prev.pkg.override { stdenv = prev.pkgsCross.aarch64-multiplatform.stdenv; }交叉编译

2.3 overlay 的注册方式

# flake.nix 输出 overlays
outputs = { self, nixpkgs }:
let
  system = "x86_64-linux";
in {
  overlays.default = import ./overlays/default.nix;

  # 应用到本地 pkgs
  packages.${system}.default = (import nixpkgs {
    inherit system;
    overlays = [ self.overlays.default ];
  }).myapp;

  # 或通过 NixOS 模块注入系统全局
  nixosModules.my-overlay = { config, pkgs, lib, ... }: {
    nixpkgs.overlays = [ self.overlays.default ];
  };
};
# overlays/default.nix
final: prev: {
  myapp = prev.callPackage ./myapp.nix { };
  # 引入 stable 与 unstable 混用(需要第二个 input)
  # pythonForBuild = prev.python312;
}

2.4 overlay 与 override 的取舍

方式作用域适用
pkg.override { }单包一次性、临时替换
pkg.overrideAttrs { }单包(build 参数)编译参数微调
overlay全局包集全仓库统一替换、注入私有包
nixpkgs.config.packageOverrides全局(传统)旧式配置兼容

最佳实践:有意的、长期存在的修改用 overlay;临时的、单次的修改用 override。overlay 是「依赖注入」层面的,让整个依赖树感知你的修改。


3. cross 工具链:理解三平台模型

3.1 三个平台的三角关系

Nix 交叉编译的核心概念:

  • buildPlatform:编译/构建发生的平台(你的 CI 机器)
  • hostPlatform:产物运行的平台(目标设备)
  • targetPlatform:生成的编译器代码要运行的平台(仅编译器链有用,普通包 host == target)
# pkgsCross 预设了常见 target
pkgs.pkgsCross.aarch64-multiplatform.hello
pkgs.pkgsCross.aarch64-multiplatform.stdenv
pkgs.pkgsCross.raspberryPi4.pkgs.hello

# 查看某个 cross pkgs 的平台信息
pkgs.pkgsCross.aarch64-multiplatform.stdenv.hostPlatform.system
# => "aarch64-linux"

3.2 常用 pkgsCross 目标

目标名hostPlatform典型设备
aarch64-multiplatformaarch64-linux树莓派 3B+/4、AWS Graviton
aarch64-darwinaarch64-darwinApple Silicon(host 构建 aarch64)
armv7l-hf-multiplatformarmv7l-linux树莓派 2、BeagleBone
riscv64riscv64-linuxRISC-V 开发板
x86_64-multiplatformx86_64-linux反向:ARM 机交叉编译 x86

3.3 交叉编译一个包

# flake.nix —— 多架构矩阵的核心
{
  outputs = { self, nixpkgs }:
    let
      # 定义需要支持的系统
      systems = [ "x86_64-linux" "aarch64-linux" "armv7l-linux" ];
      forSystem = system:
        let
          # 本地包集(build == host,用于本机构建)
          pkgs = nixpkgs.legacyPackages.${system};
          # 交叉包集(build 固定为本机,host 为 target)
          crossPkgs = nixpkgs.legacyPackages.x86_64-linux.pkgsCross.${crossName system};
          crossName = s: if s == "armv7l-linux" then "armv7l-hf-multiplatform" else "aarch64-multiplatform";
        in
        { packages.default = pkgs.callPackage ./pkgs/default.nix { }; };
    in
    builtins.foldl' (acc: system: acc // {
      packages.${system}.default = ...;
    }) {} systems;
}

更简洁的写法:用 lib.genAttrs 生成多架构矩阵:

{
  outputs = { self, nixpkgs }:
    let
      lib = nixpkgs.lib;
      systems = [ "x86_64-linux" "aarch64-linux" ];
      # 对每个系统生成 packages
      packages = lib.genAttrs systems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in {
          default = pkgs.callPackage ./pkgs/default.nix { };
        });
    in { inherit packages; };
}

4. 异构编译实战:一个 C 项目同时出 x86_64 与 aarch64

4.1 典型场景

开发机是 x86_64 的 Mac/Linux,产物要跑到树莓派(aarch64)上。用 Nix 交叉编译,全程不需要登录树莓派。

4.2 完整 flake.nix

# flake.nix
{
  description = "cross-compile demo";

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

  outputs = { self, nixpkgs }:
    let
      # 本机构建机平台
      buildSystem = "x86_64-linux";
      buildPkgs = nixpkgs.legacyPackages.${buildSystem};
      lib = nixpkgs.lib;

      # 目标平台列表:本地 + 交叉
      targets = [
        { name = "x86_64-linux";      crossName = null; }
        { name = "aarch64-linux";     crossName = "aarch64-multiplatform"; }
      ];
    in
    {
      packages = lib.genAttrs (map (t: t.name) targets) (target:
        let
          pkgs = if target.crossName == null
            then buildPkgs
            else buildPkgs.pkgsCross.${target.crossName};
        in {
          default = pkgs.callPackage ./pkgs/hello.nix { };
        });

      # 默认输出指向本机构建
      packages.x86_64-linux.default = buildPkgs.callPackage ./pkgs/hello.nix { };
    };
}
# pkgs/hello.nix —— 普通 derivation,无需交叉感知
{ lib, stdenv, fetchFromGitHub }:

stdenv.mkDerivation {
  pname = "hello-cross";
  version = "1.0.0";

  src = fetchFromGitHub {
    owner = "octocat";
    repo = "hello-cross";
    rev = "v1.0.0";
    sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  # 交叉编译时 stdenv 自动切换为 cross 工具链
  meta = {
    platforms = lib.platforms.linux;
    description = "Cross-compilable hello world";
  };
}

4.3 构建命令

# 本机构建
nix build .#packages.x86_64-linux.default

# 交叉构建(在 x86_64 机器上产 aarch64 二进制)
nix build .#packages.aarch64-linux.default

# 检查产物架构
file ./result/bin/hello-cross
# => ELF 64-bit LSB executable, ARM aarch64

# 用 qemu 直接跑(验证行为)
nix shell nixpkgs#qemu -- qemu-aarch64 ./result/bin/hello-cross

4.4 C 项目交叉编译的注意事项

注意点说明写法
buildInputs vs nativeBuildInputs交叉编译的关键:nativeBuildInputs 跑在 build 平台,buildInputs 链接进 host 平台编译工具(cmake/autoconf)放 native,库放 build
configure 缓存变量autotools 的 cache 变量会因平台不同而污染尽量不用 cache、或按平台命名
--host / --build 参数由 configurePlatforms 自动处理不必手工写,但要知道它存在
测试跳过cross 环境无法直接跑 host 二进制doCheck = false; 或加 emulator
stdenv.mkDerivation {
  pname = "myapp";
  # nativeBuildInputs 在编译机运行(cmake 本身也是 cross 工具链的)
  nativeBuildInputs = [ cmake pkg-config ];
  # buildInputs 是产物链接的宿主库(必须是 target 架构的)
  buildInputs = [ openssl zlib ];
  # autotools 自动注入 --host 参数
  configurePlatforms = [ "host" ];
  # 交叉环境跳过测试(或配置 qemu emulator)
  doCheck = false;
}

5. 高级:overlay 与 cross 的配合

5.1 用 overlay 给交叉包集注入私有包

私有包不直接 callPackage 进 crossPkgs(会丢失交叉感知),而是通过 overlay 注入,让 nixpkgs 的交叉机制自动处理:

# overlays/cross.nix
final: prev: {
  myprivate = final.callPackage ./pkgs/myprivate.nix { };
}
# 构建时同时叠加 overlay 的交叉包集
let
  crossPkgs = import nixpkgs {
    inherit (buildPkgs.stdenv) system;
    crossSystem = { config = "aarch64-unknown-linux-gnu"; };
    overlays = [ self.overlays.cross ];
  };
in
crossPkgs.myprivate   # 自动使用交叉 stdenv

5.2 处理「交叉编译失败的包」

某些包在交叉时坏掉,用 overlay 定向修正:

final: prev: {
  # 该包交叉构建会失败,强制跳过
  badpkg = prev.badpkg.overrideAttrs (old: {
    meta.platforms = lib.platforms.x86_64;  # 只允许本机构建
  });
  # 或为交叉平台换一个替代实现
  libfoo = if final.stdenv.hostPlatform.isAarch64
    then final.callPackage ./pkgs/libfoo-arm.nix { }
    else prev.libfoo;
}

5.3 多架构产物发布

配合 Cachix(见 https://plumephp.com/nix-ci-cachix/),交叉产物直接进二进制缓存,目标设备/CI 直接拉取:

# 推送交叉产物闭包
cachix push mycache ./result-aarch64
cachix push mycache ./result-x86_64

6. 异构编译场景:aarch64-darwin 与 Docker

6.1 Apple Silicon 交叉编译

在 x86_64-darwin 机器上产 aarch64-darwin 产物:

pkgs.pkgsCross.aarch64-darwin.something

注意:darwin 交叉依赖 Xcode SDK,必须在 macOS 构建机上进行。

6.2 用 QEMU 让 cross 变「仿真的本地」

本地跑 aarch64 容器/系统,用于测试而非构建:

# 注册 binfmt(让 docker 能跑 arm 镜像)
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

# 构建 aarch64 容器镜像(配合 dockerTools,见 nix-nix-docker)
nix build .#docker-aarch64
docker load < ./result

7. 常见坑速查

症状原因解法
cannot find -lxxxbuildInputs 放错平台(用了 native 的库)库进 buildInputs,工具进 nativeBuildInputs
configure: error: cannot run C compiled programsautotools 尝试在 build 平台跑 host 程序检查 configurePlatforms;doCheck 关测试
产物是 x86_64 不是 aarch64pkgs 用的是本地包集而非 pkgsCross确认从 pkgs.pkgsCross.aarch64-multiplatform 取包
unsupported system包 meta.platforms 不含目标平台检查 meta.platforms = lib.platforms.all 或 linux
overlay 不生效overlay 未注册或作用到错误包集确认 overlays 参数传入 import nixpkgs
交叉构建巨慢每个依赖都要重新交叉编译上 Cachix 缓存;拆小 derivation
doCheck 失败测试二进制是 host 架构doCheck = false 或配 qemu emulator

8. 总结

Nix 交叉编译与 overlay 的工程价值:

  • overlay:不改上游源码,以 final: prev 叠加注入,是「依赖注入」级别的可定制
  • 三平台模型:build/host/target 分离让交叉编译变成「换一个 pkgs`」的事
  • pkgsCross:预设的 aarch64/armv7l/riscv64 目标即拿即用
  • nativeBuildInputs vs buildInputs:交叉编译唯一要真正理解的概念
  • 多架构矩阵:lib.genAttrs 生成 x86_64/aarch64 双产物,配合 Cachix 全球复用

落地路径:先掌握 overlay 的 final/prev 语义 → 用 pkgsCross.aarch64-multiplatform 跑通第一个交叉包 → 用 https://plumephp.com/nix-flakes-best-practices/ 的布局沉淀为多架构 flake → 接 https://plumephp.com/nix-ci-cachix/ 把异构产物发布到二进制缓存。至此,Nix 专题从语言到系统、从 CI 到异构构建的完整拼图就闭合了。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力