引言
nixpkgs 有八万多个包,但你几乎总得改点什么:换个依赖版本、加个编译参数、引入自己写的包、或者干脆要一份「所有包都静态链接」的变体。Nix 的答案是 overlay——一层不改动 nixpkgs 源码的「叠加层」,把定制与上游彻底解耦。
本文从 overlay 的签名讲起,说清 final 与 prev 的区别与惰性互引、多层 overlay 的叠加顺序、import nixpkgs { overlays = [...] } 的几种注入姿势,再系统对比 override、overrideAttrs、overrideScope 三件套的适用边界,最后给出多 overlay 的组织方式与常见坑。
目录
- 1. overlay 是什么:不改 nixpkgs 的叠加层
- 2. final 与 prev:签名与惰性互引
- 3. 叠加顺序:谁覆盖谁
- 4. import nixpkgs 注入 overlays 的几种姿势
- 5. override、overrideAttrs 与 overrideScope
- 6. 包集变体:pkgsStatic 与 pkgsCross
- 7. 多 overlay 的组织与复用
- 8. overlay 与 flake 的结合
- 9. 常见坑:无限递归、不级联与丢属性
- 10. 速查表与一句话记忆
- 延伸阅读
1. overlay 是什么:不改 nixpkgs 的叠加层
1.1 从「打补丁」到「叠加」
传统做法是 fork nixpkgs 改源码——上游一更新就冲突。overlay 的思路完全不同:nixpkgs 本身不动,在求值结果上再叠一层函数。你写的 overlay 是纯函数,nixpkgs 是它的输入之一,因此可复用、可组合、可独立版本化。
# 一个最小 overlay:给包集加一个新包
final: prev: {
mytool = prev.stdenv.mkDerivation { pname = "mytool"; version = "1.0"; src = ./.; };
}
1.2 overlay 的类型
overlay :: (final: prev: attrs) -> pkgs -> pkgs
也就是说,overlay 是接受 final 与 prev、返回一组「要覆盖/新增的属性」的函数。nixpkgs 会把所有 overlay 依次折叠(foldl)到基础包集上,得到最终的 pkgs。
1.3 overlay 与 config 的分工
| 机制 | 作用层面 | 典型用途 |
|---|---|---|
config(allowUnfree 等) | 影响 nixpkgs 求值开关 | 许可、平台、CUDA 支持 |
overlays | 影响包集内容(增删改包) | 换版本、加包、改参数 |
system | 影响目标平台 | 交叉/原生平台选择 |
记忆:overlay 是「不改 nixpkgs 源码的叠加层」——类型是
final: prev: attrs,nixpkgs 把所有 overlay 依次折叠到基础包集上;config 管求值开关、overlays 管包集内容、system 管平台,三者分工不同。
2. final 与 prev:签名与惰性互引
2.1 两个参数到底是什么
| 参数 | 含义 | 什么时候用 |
|---|---|---|
final | 叠加完成后的完整包集 | 需要引用「别的 overlay 改过的包」时 |
prev | 本层叠加前的包集 | 基于原始包做修改、避免自我引用 |
2.2 惰性带来的互引能力
Nix 是惰性求值,所以 final 与 prev 都可以在定义时「先引用、后求值」:
final: prev: {
# 用 final 的依赖构建新包——拿到的是所有 overlay 生效后的版本
myapp = prev.callPackage ./myapp.nix { inherit (final) openssl; };
# 用 prev 拿原包再改,避免引用自己造成递归
curl = prev.curl.override { openssl = final.openssl_3; };
}
2.3 final 与 prev 的经典误用
final: prev: {
# 反例:用 final.mytool 定义 mytool → 无限递归
# mytool = final.mytool.overrideAttrs (o: { ... });
# 正解:基于 prev 改
mytool = prev.mytool.overrideAttrs (o: { configureFlags = [ "--x" ]; });
}
规则很简单:改「自己这个属性」用 prev,引用「别人的最终结果」用 final。
记忆:final 是叠加完成后的完整包集(引用别人的最终结果用它),prev 是叠加前的包集(基于原包修改用它);定义自己这个属性时必须用 prev,否则无限递归。
3. 叠加顺序:谁覆盖谁
3.1 列表顺序即覆盖顺序
import nixpkgs {
overlays = [ overlayA overlayB overlayC ];
}
nixpkgs 把列表从左到右依次折叠:先应用 A,再在 A 的结果上应用 B,再应用 C。因此后应用者赢——C 若也定义 mytool,会覆盖 A、B 的定义。
3.2 同名属性的覆盖语义
| 情况 | 结果 |
|---|---|
| 多个 overlay 定义同一属性 | 列表中最靠后的胜出 |
| 后面的 overlay 想基于前面改 | 用 prev.mytool 拿到前面版本再改 |
| 前面的 overlay 想看后面结果 | 用 final.mytool(可拿到最终版) |
3.3 顺序与引用对象共同决定结果
假设 overlay A 把 openssl 换成 3.x、overlay B 给 curl 打补丁:若顺序为 A → B 且 B 引用 final.openssl,curl 会用 3.x;若 B 引用 prev.openssl,则拿到 A 之前的版本。想让改动级联到别的包就引用 final,想固定住当前版本就引用 prev。
记忆:overlays 列表从左到右折叠、后应用者胜出;想让改动级联到别的包就引用 final,想固定住当前版本就引用 prev——顺序与引用对象共同决定结果。
4. import nixpkgs 注入 overlays 的几种姿势
4.1 四种注入方式
# 方式一:import 时传入
let pkgs = import nixpkgs { overlays = [ myOverlay ]; }; in ...
# 方式二:模块系统的选项(NixOS/Home Manager)
{ nixpkgs.overlays = [ myOverlay ]; }
# 方式三:apply 到已有 pkgs(不重新求值 nixpkgs)
let pkgs' = pkgs.extend myOverlay; in ...
# 方式四:flake 的 legacyPackages 工厂
nixpkgs.legacyPackages.${system}.extend myOverlay
4.2 各方式的取舍
| 方式 | 是否重新求值 | 适用场景 |
|---|---|---|
import nixpkgs { overlays = [...] } | 是 | 脚本、独立 nix 表达式 |
nixpkgs.overlays(模块) | 是 | NixOS / Home Manager 模块 |
pkgs.extend | 否(惰性) | 已有 pkgs 上再叠一层 |
flake legacyPackages | 否 | flake outputs 内 |
4.3 extend 与 overlays 的关系
pkgs.extend f 等价于「在已有包集上再叠一个 overlay」,不重新求值 nixpkgs 的其余部分。它比 import nixpkgs { overlays = ... } 更轻,但在模块系统里不方便表达——模块系统用的是 nixpkgs.overlays 选项。
# pkgs.extend 与「再叠一个 overlay」在结果上等价
pkgs.extend (final: prev: { foo = prev.foo.override { bar = "x"; }; })
记忆:注入 overlays 有四姿势——
import nixpkgs { overlays }(重新求值)、模块的nixpkgs.overlays(NixOS/HM 里用)、pkgs.extend(已有包集上轻量叠加)、flake 的 legacyPackages.extend;模块系统认选项、脚本认 import。
5. override、overrideAttrs 与 overrideScope
5.1 三件套的定位
| 方法 | 作用对象 | 改什么 | 是否级联 |
|---|---|---|---|
pkg.override | 单个 derivation | 函数参数(依赖) | 否 |
pkg.overrideAttrs | 单个 derivation | 属性(编译参数等) | 否 |
pkgs.overrideScope | 整个包集 | 包之间的依赖关系 | 是 |
5.2 override:换依赖版本
{
# 换依赖:把 curl 的 openssl 换成 3.x
curl = prev.curl.override { openssl = final.openssl_3; };
mypy = prev.python3Packages.requests.override { python3 = final.python311; };
}
override 改的是函数调用参数,因此只有那些「以参数形式接收依赖」的包才支持它。
5.3 overrideAttrs:改构建属性
{
hello = prev.hello.overrideAttrs (old: {
# 追加而不是覆盖 configureFlags
configureFlags = (old.configureFlags or [ ]) ++ [ "--enable-nls" ];
# 追加补丁
patches = (old.patches or [ ]) ++ [ ./fix.patch ];
});
}
关键点:old 里未必有你要的字段(如 patches 可能不存在),要用 old.patches or [ ] 兜底。
5.4 overrideScope:让改动级联
{
# 在 python3Packages 这个「包集」里统一换 openssl
python3Packages = prev.python3Packages.overrideScope (pyself: pysuper: {
cryptography = pysuper.cryptography.override { openssl = final.openssl_3; };
});
}
overrideScope 是包集级的 override,它让改动在包集内部级联——所有依赖该属性的包都会用新版本。这是 override 做不到的(单个 override 不级联,见第 9 节)。
记忆:override 换依赖参数、overrideAttrs 改构建属性(用
old.x or []兜底)、overrideScope 在包集内让改动级联——三者的本质区别是「改单包」还是「改包集内部依赖关系」。
6. 包集变体:pkgsStatic 与 pkgsCross
6.1 nixpkgs 提供的包集变体
{ inherit (prev) pkgsStatic pkgsCross pkgsLLVM pkgsMusl; }
| 变体 | 含义 | 典型用途 |
|---|---|---|
pkgsStatic | 全静态链接 | 无依赖分发的二进制 |
pkgsMusl | 用 musl libc | 容器镜像瘦身 |
pkgsCross.aarch64-multiplatform | 交叉编译到 aarch64 | 树莓派、ARM 服务器 |
pkgsLLVM | 用 LLVM 工具链 | 需要 clang 的场景 |
6.2 在 overlay 里改整个包集与常见用法
在 overlay 里可以用 prev.pkgs.extend (f: p: { openssl = f.openssl_3; }) 造一个自定义变体。实际使用时,用静态包集构建 mytool = pkgs.pkgsStatic.callPackage ./mytool.nix { } 产物可直接拷到任何 Linux;交叉编译到 aarch64 则用 pkgs.pkgsCross.aarch64-multiplatform.callPackage ./mytool.nix { }。
记忆:包集变体是「整体替换」——pkgsStatic(静态)、pkgsMusl(musl)、pkgsCross(交叉)、pkgsLLVM;在 overlay 里用
prev.pkgs.extend还能造自定义变体,用于产出可移植二进制。
7. 多 overlay 的组织与复用
7.1 按「关注点」拆文件
overlays/
default.nix # 汇总导出列表
toolchain.nix # 工具链与编译器
desktop.nix # 桌面/字体/主题
servers.nix # 服务端依赖
local-pkgs.nix # 私有包
# overlays/default.nix
[
(import ./toolchain.nix)
(import ./desktop.nix)
(import ./servers.nix)
(import ./local-pkgs.nix)
]
7.2 组织原则
| 原则 | 理由 |
|---|---|
| 一层只干一件事 | 便于定位「是谁改的」 |
| 不跨层引用具体包 | 避免顺序耦合 |
| 私有包单独一层 | 与上游定制解耦 |
| 顺序显式注释 | 覆盖关系一眼可见 |
7.3 复用:把 overlay 也做成 flake output
# 库 flake 导出 overlay,供别的仓库消费
outputs = { self, nixpkgs, ... }: {
overlays.default = final: prev: {
mylib = prev.callPackage ./pkgs/mylib.nix { };
};
};
上层消费:inputs.mylib.overlays.default,再塞进自己的 nixpkgs.overlays。
记忆:多 overlay 按关注点拆文件(工具链/桌面/服务/私有包各一层),一层只干一件事、不跨层引用具体包;把 overlay 作为 flake output 导出,就能跨仓库复用。
8. overlay 与 flake 的结合
8.1 flake 里的三种组织法
{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; };
in {
overlays.default = final: prev: { mytool = prev.callPackage ./pkgs/mytool.nix { }; };
packages.${system}.default = pkgs.mytool;
};
}
8.2 用 follows 保证 nixpkgs 一致
当 overlay 消费别人的 flake 时,务必让 nixpkgs 走 follows,否则同一份 overlay 会被求值到两个不同的 nixpkgs 上,导致类型/ABI 不匹配:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
inputs.other.url = "github:team/other";
inputs.other.inputs.nixpkgs.follows = "nixpkgs";
}
8.3 NixOS 模块里应用 overlay
{
nixpkgs.overlays = [ (import ./overlays/toolchain.nix) ];
# 之后所有 pkgs.* 都带上了这层叠加
}
记忆:flake 里把 overlay 作为
overlays.<name>导出、用import nixpkgs { overlays }求值包集;消费外部 flake 的 overlay 时 nixpkgs 必须走 follows,否则同一 overlay 会落到两个不同 nixpkgs 上。
9. 常见坑:无限递归、不级联与丢属性
9.1 三大坑对照
| 现象 | 根因 | 修复 |
|---|---|---|
infinite recursion | 定义自己时引用了 final.自己 | 改用 prev.自己 |
| 改了依赖但别的包没变 | override 不级联 | 用 overrideScope |
| 属性莫名消失 | overlay 用新 attrset 覆盖了旧的 | 用 // 合并或 inherit |
9.2 无限递归的三种形态
final: prev: {
# 形态一:直接自引用 a = final.a.overrideAttrs (o: {});
# 形态二:间接成环(a 用 final.b,b 用 final.a)
# 形态三:整体覆盖丢字段 mypkg = { name = "x"; }; # 丢了 version
}
9.3 不级联的正确理解
{
# override 只影响这一个 derivation,依赖它的包仍用旧版
openssl = prev.openssl.override { ... }; # 别的包看不到这个改动
# 想级联,必须改「包集」——用 extend 或 overrideScope
}
9.4 排错清单
# ☐ 报 infinite recursion:先找「自己引用了 final.自己」
# ☐ 改动没生效:确认是 override(不级联)还是 overrideScope(级联)
# ☐ 包缺属性:确认 overlay 是「合并」而非「整体替换」
# ☐ 顺序不对:把列表顺序与 final/prev 引用方式一起看
# ☐ 用 builtins.trace 打印 final.mypkg 的值确认求值结果
记忆:overlay 三大坑——自引用
final.自己致无限递归(改 prev)、override不级联(要overrideScope/extend)、整体覆盖丢属性(用//合并);排错先定位「改的是单包还是包集」。
10. 速查表与一句话记忆
| 需求 | 写法 | 一句话 |
|---|---|---|
| 加新包 | final: prev: { x = ...; } | 叠加层即函数 |
| 改自己 | 用 prev.自己 | 防无限递归 |
| 引用最终版 | 用 final.别的包 | 拿叠加后结果 |
| 换依赖 | pkg.override { dep = ...; } | 不级联 |
| 改属性 | pkg.overrideAttrs (o: ...) | old.x or [] 兜底 |
| 让改动级联 | overrideScope / extend | 包集级 |
| 静态包集 | pkgsStatic | 可移植二进制 |
| 交叉包集 | pkgsCross.<target> | 跨架构构建 |
一句话记忆:overlay 是 final: prev: attrs 的叠加层——nixpkgs 从左到右折叠、后应用者胜出,改自己用 prev、引用别人的最终结果用 final(用错就无限递归);注入有四姿势(import nixpkgs { overlays }、模块的 nixpkgs.overlays、pkgs.extend、flake legacyPackages);定制三件套各司其职——override 换依赖参数、overrideAttrs 改构建属性、overrideScope 让改动在包集内级联(override 不级联是最大误解);整包集变体用 pkgsStatic/pkgsMusl/pkgsCross;多 overlay 按关注点拆层、一层一事、作为 flake output 导出复用——「先分清改单包还是改包集,再选对应工具」是 overlay 的核心判断。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。