Nix Overlays 与包集定制:final/prev、叠加顺序与 override 体系

Nix Overlays 与包集定制实战:overlay 的本质与 final/prev 签名、惰性互引与叠加顺序、import nixpkgs 注入 overlays 的几种姿势、override/overrideAttrs/overrideScope 三件套的取舍、替换整个包集与 pkgsStatic/pkgsCross、多 overlay 的组织与复用、与 flake 的结合、常见坑(无限递归/不级联/丢属性)与排错。

引言

nixpkgs 有八万多个包,但你几乎总得改点什么:换个依赖版本、加个编译参数、引入自己写的包、或者干脆要一份「所有包都静态链接」的变体。Nix 的答案是 overlay——一层不改动 nixpkgs 源码的「叠加层」,把定制与上游彻底解耦。

本文从 overlay 的签名讲起,说清 final 与 prev 的区别与惰性互引、多层 overlay 的叠加顺序、import nixpkgs { overlays = [...] } 的几种注入姿势,再系统对比 override、overrideAttrs、overrideScope 三件套的适用边界,最后给出多 overlay 的组织方式与常见坑。

前置:Nix 语言深度、包打补丁与版本定制、交叉编译与 overlay。


目录


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 的核心判断。


延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「nix」更多文章

  1. nixpkgs 贡献与维护:从 by-name 到 backport
  2. Nix 中的 CUDA 与机器学习环境:cudaPackages 与 PyTorch
  3. nix-darwin:macOS 的声明式系统配置