- 包管理器
- 操作系统
【免费下载链接】nixpkgs
Nix Packages collection & NixOS
本篇技术指南系统讲解 Nixpkgs 中覆盖(Overriding)机制的四大核心工具:<pkg>.override、<pkg>.overrideAttrs、<pkg>.overrideDerivation以及底层的lib.makeOverridable。它们用于在不修改 nixpkgs 仓库源码的前提下,对单个软件包的构建参数与派生属性进行定制。读完本文,你将掌握每种覆盖函数的使用场景、函数签名语义、与 Overlay 的分工关系,以及它们背后的 Nix 求值原理,能够独立写出可复用的包定制表达式。
什么是覆盖(Overriding)
在 Nixpkgs 中,有时我们需要覆盖nixpkgs的部分内容,例如某个 derivation 的属性、甚至 derivation 的构建结果本身。覆盖机制正是为此设计的:这些函数用于对包做出修改,并只返回被修改后的单个包。
与之相对,Overlay(覆盖层) 则是另一套机制——它作用于整个 Nixpkgs 包集合(package set),可以在pkgs这个集合层面统一修改多个包,并通过final: prev: { ... }函数形式叠加在包的固定点(fixpoint)计算之上。因此:
- 覆盖(Override):针对单个包做局部修改,粒度细、局部性强;
- Overlay:把被覆盖的包重新组合进整个 Nixpkgs 包集合,粒度是"集合"。
在 overlays.chapter.md 中有这样一个典型示例,展示了二者如何配合使用——先用override修改单个包,再通过 overlay 把它放回包集合:
final: prev: { boost = prev.boost.override { python = final.python3; }; rr = prev.callPackage ./pkgs/rr { stdenv = final.stdenv_32bit; }; }注意其中的依赖关系:final.python3是覆盖后包集合中的 Python 3,而prev.boost则是覆盖前的原始 Boost。这种override+ overlay 的组合是 Nixpkgs 生态中最常见的定制方式。
<pkg>.override:覆盖函数的输入参数
基本用法
override函数通常对 nixpkgs 表达式(pkgs)中的所有 derivation 都可用。它用于覆盖传给包函数的参数。所谓包函数,是指形如{ pname, version, stdenv, ... }:的 Nix 函数——包源码文件(如pkgs/by-name/...或pkgs/top-level/all-packages.nix中引用的文件)导出的通常是这样的函数,调用时传入一个属性集。
典型用法:
pkgs.foo.override { arg1 = val1; arg2 = val2; # ... }即:用新的参数集{ arg1 = val1; arg2 = val2; }重新调用pkgs.foo背后的那个包函数,其余未提及的参数沿用默认值。
访问上一组参数
也可以传入一个函数来访问上一组参数,从而基于旧值派生新值:
pkgs.foo.override (previous: { arg1 = previous.arg1; # ... })这里previous是上一次调用包函数时使用的参数集。这种写法在需要"保留原有依赖、只改其中某一项"的场景非常有用。
在包集合中全局应用
override往往与 Overlay 配合,在整个包集合层面生效。例如通过import pkgs.path导入 Nixpkgs 并传入overlays参数:
import pkgs.path { overlays = [ (self: super: { foo = super.foo.override { barSupport = true; }; }) ]; }上述表达式会用barSupport = true重新构建foo,并把结果放回覆盖后的包集合self中。
与callPackage组合
override也经常出现在callPackage的调用中,用于在构造某个包时,向其依赖传入已被覆盖的版本:
{ mypkg = pkgs.callPackage ./mypkg.nix { mydep = pkgs.mydep.override { # ... }; }; }callPackage本身定义于 lib/customisation.nix 的callPackageWith,它从自动参数集autoArgs中按函数签名自动填充缺失参数,并把结果用makeOverridable包装(详见下文)。因此凡是经callPackage构造的包,天然带有override、overrideAttrs等方法。
语义与注意事项
在第一个示例中,pkgs.foo是以一组默认参数调用包函数得到的结果(通常是一个 derivation)。使用pkgs.foo.override会用给定的新参数重新调用同一个函数。
许多包(如示例中的foo)会在参数中提供带默认值的包选项(package options)来方便覆盖。但请注意:
- 由于通常无法测试包在所有选项组合下都能成功构建,你可能会发现某些包在把选项覆盖为非默认值时构建失败;
- 包维护者没有义务修复任意选项组合的问题;
- 如果发现某个选项组合不工作,欢迎提交修复,最好附带回归测试(regression test);
- 如果想确保相关功能持续可用,可以考虑成为该包的维护者。
<pkg>.overrideAttrs:覆盖传给stdenv.mkDerivation的属性集
定位与适用对象
overrideAttrs允许你覆盖传给stdenv.mkDerivation调用的属性集,基于原 derivation 生成一个新 derivation。它对所有由stdenv.mkDerivation产生的 derivation 都可用——而这涵盖了 Nixpkgs 表达式pkgs中的绝大多数包。
基本用法与参数语义
{ helloBar = pkgs.hello.overrideAttrs ( finalAttrs: previousAttrs: { pname = previousAttrs.pname + "-bar"; } ); }在上面的例子中,"-bar"被追加到pname属性,而其他所有属性都会从原始的hello包中保留下来。
关于两个函数参数的约定:
previousAttrs:习惯上指最初传给stdenv.mkDerivation的属性集;finalAttrs:指最终传给mkDerivation的属性集,另外还包含一个finalPackage属性,它等于mkDerivation的结果或后续overrideAttrs调用的结果;- 如果只写一个参数的函数,那么这个参数具有
previousAttrs的含义; - 如果不需要访问
previousAttrs或finalAttrs,两个参数都可以完全省略。
只传属性集的形式
{ helloWithDebug = pkgs.hello.overrideAttrs { separateDebugInfo = true; }; }在上述例子中,separateDebugInfo属性被覆盖为true,从而为helloWithDebug构建调试信息。此时没有使用函数形式,直接传入属性集,覆盖语义等价于"只写一个参数且仅使用 previousAttrs"。
为什么overrideAttrs优于overrideDerivation
文档中特别强调了一个关键点:separateDebugInfo只会被stdenv.mkDerivation函数处理,而不会被生成后的"原始 Nix derivation"处理。因此,若改用overrideDerivation,它将只覆盖最终 derivation 的属性,separateDebugInfo根本不会被处理,此例中也就无法生效。
这就是在(几乎)所有情况下都应优先使用overrideAttrs而非overrideDerivation的原因:
- 让
stdenv.mkDerivation继续处理输入参数(如separateDebugInfo这类高阶选项); - 使用起来更容易:可以直接使用你在 Nix 代码中看到的属性名(例如
buildInputs),而不必使用生成后的名字(如nativeBuildInputs对应的生成形态); - 输入更少、代码更简洁。
源码层面的实现印证
overrideAttrs的真正实现在 pkgs/stdenv/generic/make-derivation.nix 中(参见makeDerivationExtensible,约 L228-L311)。其核心逻辑是:
overrideAttrs = f0: makeDerivationExtensible ( final: let prev = rattrs final; thisOverlay = if isFunction f0 then let fPrev = f0 prev; in if isFunction fPrev then f0 final prev # f 是 (final: prev: { ... }) 形式 else fPrev # f 是 (prev: { ... }) 形式 else f0; # f 不是函数,大概率是 { ... } in ... (prev // (removeAttrs thisOverlay [ "__intentionallyOverridingVersion" ])) );从源码可以看到overrideAttrs的三种输入形态是如何被统一处理的:属性集、单参数函数(prev: ...)、双参数函数(final: prev: ...),这与文档中的说明完全一致。同时,finalPackage与overrideAttrs会被注入递归属性集args中(args = rattrs (args // { inherit finalPackage overrideAttrs; })),从而支持finalAttrs引用最终结果。
此外,源码中还内置了一个非常实用的告警(warning):当你在overrideAttrs中覆盖了version却没有同时覆盖src时,构建时会打印提示,建议同时覆盖version和src(例如借助rec { version = "1.0.0"; src = pkgs.fetchurl { url = "mirror://gnu/hello/hello-${version}.tar.gz"; ... }; })。如果确属有意为之,可以通过设置__intentionallyOverridingVersion = true来关闭该告警——该属性会在合并前被removeAttrs移除,不会污染最终属性集。
<pkg>.overrideDerivation:覆盖最终 derivation 的属性
定位与警告
在几乎所有情况下你都应优先使用 `overrideAttrs`。`overrideDerivation` 并未被废弃,仍会继续工作,但它使用起来不够方便,能力也不如 `overrideAttrs`。不要在本仓库 Nixpkgs 内部使用该函数:它会在修改前对 derivation 进行求值,破坏了包的抽象(package abstraction)。此外,这种"每次应用函数都要求值"的做法会带来性能开销,当大量覆盖叠加时可能成为问题。它只适用于临时定制(ad-hoc customisation),例如 `~/.config/nixpkgs/config.nix` 这样的用户级配置场景。基本用法
overrideDerivation基于现有 derivation,用指定函数产生的属性集覆盖原 derivation 的属性,从而创建新 derivation。该函数对所有用makeOverridable定义的 derivation都可用;而大多数标准的 derivation 生成函数(如stdenv.mkDerivation)都是用makeOverridable定义的,因此pkgs中大多数包都带有这个函数。
{ mySed = pkgs.gnused.overrideDerivation (oldAttrs: { name = "sed-4.2.2-pre"; src = fetchurl { url = "ftp://alpha.gnu.org/gnu/sed/sed-4.2.2-pre.tar.bz2"; hash = "sha256-MxBJRcM2rYzQYwJ5XKxhXTQByvSg5jZc5cSHEZoB2IY="; }; patches = [ ]; }); }在上述例子中,derivation 的name、src、patches会被覆盖,而其他所有属性会从原 derivation 保留。参数oldAttrs用来引用原 derivation 的属性集。
关键陷阱:属性求值先于覆盖
包的属性是在被 `overrideDerivation` 修改**之前**求值的。例如,`url = "mirror://gnu/hello/${name}.tar.gz";` 中的 `name` 引用会在 `overrideDerivation` 修改属性集之前被填入。这意味着在此例中,仅覆盖 `name` 属性**不会**改变 `url` 的值——你必须同时覆盖 `name` 和 `url` 两个属性。这个求值时序问题正是overrideDerivation相对难用的根源之一,也是文档建议在绝大多数场景转向overrideAttrs的重要原因。
源码实现
overrideDerivation定义于 lib/customisation.nix(L99-L112):
overrideDerivation = drv: f: (extendDerivation (seq drv.drvPath true)) ( { meta = drv.meta or { }; passthru = drv.passthru or { }; } // (drv.passthru or { }) // { ${if drv ? __spliced then "__spliced" else null} = mapAttrs ( _: sDrv: overrideDerivation sDrv f ) drv.__spliced; } ) (derivation (drv.drvAttrs // (f drv)));从源码可以看出:overrideDerivation直接调用 Nix 内建derivation,把drv.drvAttrs与f drv的结果合并后重新构造底层 derivation——这正是"它只作用于最终 derivation 属性"这一特性的来源。同时它也印证了文档中的性能警告:每次覆盖都会重新对 derivation 求值,并依赖seq drv.drvPath强制保留旧 derivation 的求值路径。
overrideDerivation的另一个应用场景,从源码注释看是build-support/vm:它被用来在 QEMU 虚拟机内构建任意 derivation(通过覆盖替换构建阶段的属性)。
lib.makeOverridable:让任意函数的结果可被覆盖
定位
lib.makeOverridable用来让某个函数的返回结果易于定制。这个工具只对"接受一个属性集、返回一个属性集"的函数有意义。
基本示例
{ f = { a, b }: { result = a + b; }; c = lib.makeOverridable f { a = 1; b = 2; }; }变量c是函数f以默认参数应用后的值,因此c.result为3。同时c上还挂载了额外的函数,例如c.override,它可以用来覆盖默认参数。在本例中,(c.override { a = 4; }).result的值为6。
源码实现:override与overrideAttrs如何被注入
makeOverridable实现于 lib/customisation.nix(L152-L217)。其核心思路是:
- 先以原始参数
origArgs调用f得到result; - 定义
overrideArgs = mirrorArgs (newArgs: makeOverridable f (origArgs // (if isFunction newArgs then newArgs origArgs else newArgs)))——即override支持两种输入:直接属性集,或接收旧参数的函数; - 当
result是属性集时,为其附加:
result // { override = overrideArgs; overrideDerivation = fdrv: makeOverridable (mirrorArgs (args: overrideDerivation (f args) fdrv)) origArgs; ${if result ? overrideAttrs then "overrideAttrs" else null} = fdrv: makeOverridable (mirrorArgs (args: (f args).overrideAttrs fdrv)) origArgs; }值得注意的细节:
overrideAttrs的注入是有条件的:只有当result本身已经带有overrideAttrs(即底层是用stdenv.mkDerivation构建的)时,makeOverridable才会为结果重新添加上overrideAttrs。这样做是为了让override与overrideAttrs可以组合使用——overrideAttrs之后的结果仍然保留override方法。正如源码注释所述:"The real implementation ofoverrideAttrsis provided bystdenv.mkDerivation",而makeOverridable中的overrideAttrs只是一个把override重新挂回结果的胶水层;- 当
result本身是函数时,makeOverridable会通过setFunctionArgs把它变成 functor 并继续传播参数信息(__functionArgs),同时挂上override; - 当
f本身是一个可调用的属性集(callable attrset)且自带override时,makeOverridable会保留f上的其他属性,并对f.override再做一层装饰(L207-L214)。
makeOverridable是整个覆盖体系的"发动机":callPackageWith(同文件 L267-L330)在自动填充缺失参数后正是调用makeOverridable f allArgs来产出包,这解释了为什么通过callPackage构造的包都天然携带override、overrideAttrs、overrideDerivation。makeScope与makeScopeWithSplicing'(同文件)则在包集合层面(如各语言包集)沿用同一套callPackage/newScope/overrideScope机制。
在 Nix REPL 中验证
makeOverridable的文档注释提供了一个可直接在nix-repl验证的交互示例:
nix-repl> x = {a, b}: { result = a + b; } nix-repl> y = lib.makeOverridable x { a = 1; b = 2; } nix-repl> y { override = «lambda»; overrideDerivation = «lambda»; result = 3; } nix-repl> y.override { a = 10; } { override = «lambda»; overrideDerivation = «lambda»; result = 12; }可以看到返回结果同时包含override与overrideDerivation两个 lambda,而result随覆盖参数正确变化。
测试用例与实战验证
Nixpkgs 仓库内提供了专门的测试文件 pkgs/test/overriding.nix,用lib.runTests对覆盖机制做了系统性验证,是理解各函数行为的绝佳参考。摘录几个关键断言:
- 多次叠加
overrideAttrs:repeatedOverrides-pname断言对pkgs.hello连续两次overrideAttrs后pname变为"a-better-hello-with-blackjack",证明覆盖是可叠加、可组合的; - 只传属性集的覆盖:
overriding-using-only-attrset断言(pkgs.hello.overrideAttrs { pname = "hello-overriden"; }).pname生效,验证了文档中"省略函数参数"的写法; finalAttrs引用最终值:测试overrideAttrsFooBar中BAR = finalAttrs.FOO,断言FOO == "a"且BAR == "a",说明finalAttrs中引用的是覆盖后的最终属性集;- 覆盖
version/src的完整实战:buildGoModule-overrideAttrs用overrideAttrs (finalAttrs: previousAttrs: { version = "0.4.0"; src = pkgs.fetchFromGitHub { inherit (previousAttrs.src) owner repo; rev = "v${finalAttrs.version}"; ... }; })把 pet 从 0.3.4 升到 0.4.0,并断言其drvPath、name、vendorHash等与直接用buildGoModule构建的 0.4.0 完全一致——这是"正确升级版本必须同时覆盖version与src"的最佳范例,正好呼应 make-derivation.nix 中的告警逻辑; override覆盖函数参数:buildPythonPackage-override-gccStdenv等测试展示通过override (previousArgs: { buildPythonPackage = previousArgs.buildPythonPackage.override { stdenv = pkgs.gccStdenv; }; })逐层替换依赖中的 stdenv;- 覆盖可交换性(commutation):
overrideAttrs-overridePythonAttrs-test-commutation断言overrideAttrs与overridePythonAttrs两种覆盖顺序的结果相同,验证覆盖操作之间是良构可组合的。
选型建议与总结
面对不同的定制需求,可以参考以下决策路径:
| 需求场景 | 推荐工具 | 理由 |
|---|---|---|
覆盖包函数的入参(如barSupport = true、替换某个依赖) | <pkg>.override | 直接作用于包函数参数,语义最贴合 |
修改构建属性(pname、version、src、separateDebugInfo、patches、buildInputs等) | <pkg>.overrideAttrs | 让stdenv.mkDerivation继续处理输入,支持finalAttrs/previousAttrs,属性名与源码一致 |
针对最终 derivation 属性的临时 hack(如~/.config/nixpkgs/config.nix) | <pkg>.overrideDerivation | 保留能力但注意求值时序陷阱与性能开销 |
| 让自定义函数返回的对象具备可覆盖能力 | lib.makeOverridable | 底层机制,callPackage、stdenv.mkDerivation都构建于其上 |
在包集合层面统一修改多个包并放回pkgs | override/overrideAttrs+ Overlay | 先覆盖单个包,再通过 overlay 组合进固定点 |
覆盖机制是 Nix 声明式软件分发哲学的核心体现:通过不可变数据结构上的受控"改写",让用户无需 fork 整个包集合即可获得定制化软件。掌握了override、overrideAttrs、overrideDerivation与lib.makeOverridable四者的差异与协作方式,再配合 lib/customisation.nix、pkgs/stdenv/generic/make-derivation.nix 与 pkgs/test/overriding.nix 中的实现与测试,你就能在 NixOS 配置、Nix 表达式或自定义包集中自如地完成各种精细定制。
- 包管理器
- 操作系统
【免费下载链接】nixpkgs
Nix Packages collection & NixOS
相关推荐
定制 nixpkgs 包:NixOS 中 nixpkgs.config、override、overrideAttrs 与 packageOverrides 全实战
定制 nixpkgs 包:NixOS 中 nixpkgs.config、override、overrideAttrs 与 packageOverrides 全实
包管理器操作系统nixpkgs 中的 Nim 包构建指南:buildNimPackage、buildNimSbom 与 lockfile 覆盖机制
nixpkgs 中的 Nim 包构建指南:buildNimPackage、buildNimSbom 与 lockfile 覆盖机制 本文基于 nixpkgs 官
包管理器操作系统NixOS与Flakes技术手册:Nixpkgs包覆写机制详解
NixOS与Flakes技术手册:Nixpkgs包覆写机制详解 前言 在Nix生态系统中,包覆写 Overriding 是一项强大的功能,它允许开发者在不修改原
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考