Nixpkgs 的便捷覆盖

Lobsters Hottest 工具

摘要

Gabriella439 介绍了 override-utils,这是一个简化 Nixpkgs 覆盖和叠加的新包,旨在提升 Nix 生态系统的易用性。

<p><a href="https://lobste.rs/s/yfj5qc/ergonomic_overrides_for_nixpkgs">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/08 03:19

# 面向 Nixpkgs 的人性化覆盖机制 来源:https://haskellforall.com/2026/06/ergonomic-overrides-for-nixpkgs 我创建了一个新的 `override-utils` 包(https://github.com/Gabriella439/override-utils),用于简化 Nixpkgs 的覆盖/叠加层,这是我改善 Nixpkgs 可用性的一个更宏大项目的一部分。本文主要聚焦整体构思,但也会介绍 `override-utils` 包的动机。 ## 背景 几年前我写了《类型检查 Nix 的难点》(https://haskellforall.com/2022/03/the-hard-part-of-type-checking-nix),其中阐述了我对困扰 Nix 生态系统的可用性问题的看法。文中相关的摘录如下: > 困扰所有 Nix 类型检查尝试的根本问题是:实际上没有人真正大规模使用 **Nix 那门语言**。相反,社区采用了嵌入在 Nix 中的两种子语言来进行“大型”编程: > - Nixpkgs overlays(覆盖层):一种模拟面向对象编程(继承/延迟绑定/动态作用域)的嵌入语言。 > - NixOS modules(模块):一种大致模拟 Terraform(https://github.com/hashicorp/terraform)的嵌入语言。 > 请注意,这些并非 Nix 内置的语言特性;而是用 Nix 实现的嵌入式领域特定语言。因此,针对“Nix 语言”的类型检查器未必能检查这两种子语言。换句话说,Nix 的问题不在于它没有类型;而是即使 Nix **有了**类型,它们也像当前堆栈跟踪一样难以理解,因为 Nix 的抽象层次不对。我们需要先设计更好的抽象,才能构建一个在非专家手中也能良好工作的类型系统。 ## 设计 为此,我静下来问自己:“如果我能构建一门专为 Nixpkgs 设计的编程语言,那门理想语言会是什么样?”我觉得这是个有用的思想实验,同时我也有足够的编程语言实现经验,如果想法足够有说服力,我或许可以做出一个概念验证。 在设计这种“Nixpkgs 语言”时,我意识到最让我头疼的是覆盖函数和叠加层,所以我从那里开始。来说明一下,假设我需要将 `libvirt` 包作为原生依赖添加到 Haskell 的 `libvirt-hs` 包[^1]中。我不得不这样做: ``` final: prev: { haskellPackages = prev.haskellPackages.override (old: { overrides = hfinal: hprev: { libvirt-hs = final.haskell.lib.overrideCabal hprev.libvirt-hs (old: { libraryPkgconfigDepends = (old.libraryPkgconfigDepends or []) ++ [ final.libvirt ]; }); }; }); } ``` 真恶心。而且,如果我想为非默认的 GHC 版本做同样的事情,那就更麻烦了: ``` final: prev: { haskell = prev.haskell // { packages = prev.haskell.packages // { ghc98 = prev.haskell.packages.ghc98.override (old: { overrides = hfinal: hprev: { libvirt-hs = final.haskell.lib.overrideCabal hprev.libvirt-hs (old: { libraryPkgconfigDepends = (old.libraryPkgconfigDepends or []) ++ [ final.libvirt ]; }); }; }); }; }; } ``` 我使用 Nixpkgs 已久,已经习惯了这类写法,但这不是那种我有信心向犹豫不决的同事推荐的用户体验。在我看来,这种糟糕的用户体验是 Nix 在企业发展中不断被边缘化的重要原因[^2]。 所以我问自己:我**更希望**上一个例子怎么写?我最终得出的答案大致如下: ``` haskell.packages.ghc98.override.overrides = libvirt-hs.overrideCabal.libraryPkgconfigDepends ++= [ libvirt ]; ``` 我们当然可以继续打磨,但我认为这样的写法对新用户来说会友好得多。此外,这种语法对自动补全非常友好!用户可以这样写: ``` haskell.packages.ghc98.override. ``` 然后编辑器可以建议所有可用的覆盖来补全,这很容易做到,因为即使没有类型系统,你也可以查询可用的覆盖列表[^3]: ``` nix-repl> haskell.packages.ghc98.override.__functionArgs { all-cabal-hashes = false; buildHaskellPackages = false; compilerConfig = true; configurationArm = true; configurationCommon = true; configurationDarwin = true; configurationJS = true; configurationNix = true; configurationWindows = true; ghc = false; haskellLib = false; initialPackages = true; lib = false; nonHackagePackages = true; overrides = true; packageSetConfig = true; pkgs = false; stdenv = false; } ``` 此外,这种更简单的语法也暗示了一种更简单的类型系统。我认为我们不应该用覆盖函数和叠加层来思考,而应该用属性路径和针对属性路径的安全操作来思考。以这种方式组织所有覆盖将极大简化类型系统(无论是实现还是用户体验)。 ## 实现 我还没有构建出这样一门编程语言。不过,我确实发布了一个 `override-utils` 包(https://github.com/Gabriella439/override-utils),它用纯 Nix 接近了那个理想化接口。我创建这个包作为起点,在考虑更大的编程语言项目之前先验证这个想法。 例如,使用 `override-utils`,上面的例子可以写成: ``` final: override { haskell.packages.ghc98.override.overrides = set (hfinal: override { libvirt-hs.overrideCabal.libraryPkgconfigDepends = append [ final.libvirt ]; }); } ``` 这已经和我最初提出的理想化接口非常相似了: ``` haskell.packages.ghc98.override.overrides = libvirt-hs.overrideCabal.libraryPkgconfigDepends ++= [ libvirt ]; ``` 但有几个重要差异需要深入探讨。 ### 操作 像 `attribute ++= list` 这种写法在 Nix 中无效,所以 `override-utils` 的变通方法是写成 `attribute = append list`。另一方面,`attribute = value` 在 Nix 中是有效的,但 **仍然** 不被 `override-utils` 支持,你必须改用 `attribute = set value`。为什么?因为否则无法区分以下两者: ``` { foo = set { bar = 1; }; } # 将 `foo` 替换为 `{ bar = 1; }` ``` ……和这个: ``` { foo.bar = set 1; } # 将 `foo.bar` 替换为 `1` ``` 这两者含义不应该相同,通过这个最小示例可以看出: ``` nix-repl> :print override { foo = set { bar = 1; }; } { foo.baz = 2; } { foo = { bar = 1; }; } nix-repl> :print override { foo.bar = set 1; } { foo.baz = 2; } { foo = { bar = 1; baz = 2; }; } ``` 前者完全替换了 `foo` 属性(包括老的 `baz` 子属性),而后者则不会。如果去掉 `set`,两个操作都变成 `{ foo.bar = 1; }`,我们就无法区分用户的本意了。 如果我们在一个独立的领域特定语言(即不是 Nix)中实现同样的功能,那么我们可以保留这种区分,并去掉对 `set` 的需求。 ### 限定 `override-utils` 还要求你在创建覆盖层时将 `final` 包集引入作用域,以便引用包集中的其他包。例如,这个覆盖层: ``` final: prev: { x = prev.x + 1; y = final.x } ``` ……对应以下 `override`: ``` final: override { x = add 1; y = set final.x; } ``` 在理想化的接口中,我倾向于让你在默认情况下省略 `final`,并在没有歧义时直接裸引用其他属性。例如,我希望可以这样写: ``` x += 1; y = x; ``` 但是,当作用域中有多个最终包集时(例如我们原始例子中的 `final` 与 `hfinal`),你仍然可以选择限定这些引用,以消除歧义。 为了进一步说明最后一点,让我们重新审视原始的理想化示例: ``` haskell.packages.ghc98.override.overrides = libvirt-hs.overrideCabal.libraryPkgconfigDepends ++= [ libvirt ]; ``` 我特意选择这个作为起点,因为在没有限定符的情况下,我希望理想化代码中的 `libvirt` 引用在 Nix 中翻译成 `hfinal.libvirt or final.libvirt`(由于没有 `hfinal.libvirt`,最终会求值为 `final.libvirt`)。但我仍然希望用户可以选择不使用隐式限定,并像这样显式声明: ``` final: haskell.packages.ghc98.override.overrides = libvirt-hs.overrideCabal.libraryPkgconfigDepends ++= [ final.libvirt ]; ``` ### 与 Nix 兼容的特性 好消息是,我想到的大多数其他特性完全可以用纯 Nix 实现,这还挺酷的!实际上,核心实现非常简单。如果放弃错误消息和默认值,你可以用几行 Nix 代码实现一个“一元店 `override-utils`”: ```nix rec { override = argument: let adapt = name: value: let default = old: old // { "${name}" = override value old."${name}"; }; in { modify = value; "*" = map (override value); override = old: old.override (override value); overrideAttrs = old: old.overrideAttrs (override value); overrideDerivation = old: old.overrideDerivation (override value); overrideCabal = old: haskell.lib.overrideCabal old (override value); }."${name}" or default; in prev: lib.pipe prev (lib.mapAttrsToList adapt argument); modify = f : { modify = f; }; set = value: modify (_: value); add = value: modify (x: x + value); subtract = value: modify (x: x - value); append = suffix: modify (prefix: prefix ++ suffix); prepend = prefix: modify (suffix: prefix ++ suffix); # ... 根据需要添加其他工具函数 ... } ``` 真正的实现(https://github.com/Gabriella439/override-utils/blob/4f1aaa5384b99eae652a62f2d0e83e90e2eea63b/flake.nix#L17-L245)要更完整和全面,但上面的片段应该能让你了解内部工作原理。熟悉函数式编程历史的人可能会认出,这相当于语义编辑器组合子(semantic editor combinators)(http://conal.net/blog/posts/semantic-editor-combinators)(van Laarhoven lens 的前身(https://www.twanvl.nl/blog/haskell/cps-functional-references))。另一种理解方式是,这一切等价于 Haskell 的 `lens` 包(https://hackage-content.haskell.org/package/lens)中的 `Setter` 类型(https://hackage-content.haskell.org/package/lens-5.3.6/docs/Control-Lens-Setter.html#t:Setter)。属性路径中的属性相当于 `Setter`,而链式组合属性相当于组合 `Setter`(两者都用“`.`”)。 ## 已有工作 > **编辑:** 我惭愧地发现,在研究已有工作时,我错过了 infuse.nix(https://codeberg.org/amjoseph/infuse.nix)。有人指出后,我在此处(https://discourse.nixos.org/t/ergonomic-overrides-for-nixpkgs/78081/3?u=gabriella439)快速比较了 `override-utils` 和 `infuse.nix`。 这个领域中最接近的现有项目是`dream2nix`(https://dream2nix.dev/),它(除了其他功能)也提供了改进的 Nixpkgs 接口(基于原始的 `drv-parts` 项目(https://github.com/DavHau/drv-parts))。具体来说,`dream2nix` 暴露了一个 NixOS 模块接口给 Nixpkgs,你可以这样写代码: ```nix { config, lib, ... }: { pip.overrides.opencv-python = { env.autoPatchelfIgnoreMissingDeps = true; mkDerivation.buildInputs = [ pkgs.libglvnd pkgs.glib ]; }; } ``` ……进行比较,使用 `override-utils` 的等效覆盖层是: ```nix final: override { pythonPackageExtensions = append (pfinal: override { opencv-python.overridePythonAttrs = { env.autoPatchelfIgnoreMissingDeps = set true; buildInputs = append [ final.libglvnd final.glib ]; }; }); } ``` 有点像,对吧?所以我在附录中为感兴趣的人比较了这两个项目。 ## 结论 我认为 `override-utils` 本身就是对 Nix 生态系统的一个有用贡献,但我也认为我们还可以做得更好。特别是,在我那篇关于《类型检查 Nix 的难点》(https://haskellforall.com/2022/03/the-hard-part-of-type-checking-nix)文章的结论中,我总结了语言未来发展的几种路径: > - **方案 A:** 不为 Nix 实现类型系统…… > - **方案 B:** 只对 Nix “小规模”进行类型检查…… > - **方案 C:** 使用支持行多态的类型系统对 Nixpkgs overlays 进行类型检查…… > - **方案 D:** 在外部语言中实现这两种子语言。即,在一种非 Nix 的独立语言中实现 Nixpkgs overlay 系统和 NixOS 模块系统,这样该语言及其类型系统原生支持这些特性。然后可以将这种外部语言编译成普通 Nix 代码,与现有 Nixpkgs overlay 系统或 NixOS 模块系统兼容。 > - **方案 E:** 类似方案 D,但将这些特性加入 Nix 语言本身…… > - **方案 F:** 类似方案 D,但不使用 Nix 作为中间语言…… 我目前认为“方案 D”是正确的方向。也就是说,我认为我们需要构建一个针对 Nixpkgs(和 NixOS)的领域特定语言,它能编译成 Nix。这个语言不仅要有类型,还应该运行在更高的抽象层次上,使得像 overrides/overlays 和 modules 这样的抽象得到语言和类型系统的内置支持。 `override-utils` 只是朝这个方向迈出的一小步:我创建这个包是为了验证一个想法,即 overrides/overlays 的正确抽象是可组合的 `Setter` 以及针对这些 `Setter` 的安全操作。如果你想了解更多关于 `override-utils` 包的信息,我建议查看 `README`(https://github.com/Gabriella439/override-utils#override-utils),其中包含该包的完整文档(并不复杂)。 ## 附录:与 `dream2nix` 的比较 本节介绍 `override-utils` 和 `dream2nix` 之间最大的差异。 ### 增量性 就我所知,`dream2nix` 是一个“全有或全无”的接口,这意味着 `dream2nix` 完全用其自己的接口(例如 `dream2nix.lib.importPackages` 或 `dream2nix.lib.evalModules`)替换了你项目的 Nix 入口点。这意味着你不能将现有的 Nix 项目逐步迁移到 `dream2nix`:你必须从一开始就围绕 `dream2nix` 设计整个项目,或者一次性全部迁移。 相比之下,`override-utils` 生成的是 overrides 和 overlays,这意味着你可以在任何需要 override 或 overlay 的地方使用 `override-utils`,无需额外仪式。此外,overlays 是可组合的(https://haskellforall.com/2022/01/nixpkgs-overlays-are-monoids),这意味着你可以逐步将项目迁移到 `override-utils`。例如,如果你有一个 overlay 列表: ```nix import nixpkgs { inherit system; overlays = [ (import ./lib-overlay.nix) (import ./rust-overlay.nix) (import ./firefox-overlay.nix) (import ./git-cinnabar-overlay.nix) ]; } ``` 那么其中任何一个单独的 overlay 都可以独立地迁移到 `override-utils` 中,而不影响其他部分。 [^1]: 这只是一个虚构的例子,不要当真。 [^2]: 即使没有更广泛的数据支持,仅从大量公司从 Nix 转向其他工具(通常回归到基于容器的工作流)这一现象就可见一斑。 [^3]: 实际上,用户理想情况下应该能看到一个可补全的属性路径,而不是函数参数。`__functionArgs` 只是在当前缺乏 IDE 支持的情况下提供的一种帮助。

相似文章

自行过期的 Nix 覆盖

Lobsters Hottest

一篇博客文章,演示了一种 Nix 模式,使包覆盖在过时时发出警告,基于评估期间的条件版本或元数据检查。

nixpkgs 的正当程序问题

Lobsters Hottest

Domen Kožar 认为 Nixpkgs 存在正当程序问题,他讲述了自己的提交权限如何在模糊定义的规则下被移除,且没有明确的申诉流程;他还提到了近期治理方面的改进。

GuixPkgs:每个 Guix 包,作为 Nix flake

Lobsters Hottest

GuixPkgs 是一个项目,它将每个 GNU Guix 包以 Nix flake 的形式提供,使用户能够在单个 flake 中混合使用 Guix 和 Nixpkgs 包。它使用 guix-transfer 将 Guix 派生转换为 Nix 派生,并提供二进制缓存以避免完全重建 Guix 引导过程。

Nixmac

Product Hunt

Nixmac 是一个为 Nix-darwin 提供简洁英文界面的工具,简化了 macOS 上的 Nix 配置。