1. 交叉编译为什么难,Nix 为什么能做好
传统交叉编译的痛点:
- 工具链混乱:
arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc各种前缀,依赖库也要为 target 编译 - 依赖地狱:一个库的交叉编译依赖链很长,手工管理必然出错
- 环境漂移:编译机的 glibc/内核头文件影响产物
Nix 的两大机制让它成为交叉编译的最优解:
- 三平台分离模型:
hostPlatform(运行平台)、buildPlatform(编译平台)、targetPlatform(生成代码的平台),三者可自由组合 - 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-multiplatform | aarch64-linux | 树莓派 3B+/4、AWS Graviton |
aarch64-darwin | aarch64-darwin | Apple Silicon(host 构建 aarch64) |
armv7l-hf-multiplatform | armv7l-linux | 树莓派 2、BeagleBone |
riscv64 | riscv64-linux | RISC-V 开发板 |
x86_64-multiplatform | x86_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 -lxxx | buildInputs 放错平台(用了 native 的库) | 库进 buildInputs,工具进 nativeBuildInputs |
configure: error: cannot run C compiled programs | autotools 尝试在 build 平台跑 host 程序 | 检查 configurePlatforms;doCheck 关测试 |
| 产物是 x86_64 不是 aarch64 | pkgs 用的是本地包集而非 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 目标即拿即用nativeBuildInputsvsbuildInputs:交叉编译唯一要真正理解的概念- 多架构矩阵:
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 到异构构建的完整拼图就闭合了。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。