一个 Nix flake 统治一切
摘要
本文介绍 omniflake,这是一款 Nix 工具,它将数千个 flake 合并为一个输入,以简化依赖管理并提升基于 Nix 系统的用户体验。
暂无内容
查看缓存全文
缓存时间: 2026/08/30 15:46
# 一片 Flake 统御万物
来源:https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all
Flakes 毫无疑问将长久存在,而我对此的看法(https://fzakaria.com/2026/07/18/how-to-piss-off-your-nix-friends#flakes-are-meh)已是众所周知:它们“不过如此”。
尽管它们无处不在,添加一个 flake 输入仍然是挥之不去的小烦恼。人们吹捧 flake 的联邦制特性是个优点,但现实是,我想要集中式 flake 的简洁。那才是 nixpkgs(https://github.com/NixOS/nixpkgs/)的美妙*与力量*。
关于内联 vendoring nixpkgs 的表情包
流程是这样的:你想要 disko(https://github.com/nix-community/disko),于是添加一个 `url`,然后添加一个 `follows` 让它不再拖入自己的 nixpkgs,接着为下一个 flake 再重复此过程。这在 Nix 社区已成为一个梗,讽刺每个 flake 都拖入自己的 flake-utils(https://nixcademy.com/posts/1000-instances-of-flake-utils/)。
我拒绝接受这样的用户体验。
于是我想:是否可能由**一个** flake 承载所有其他的 flake,你只需按需取用所需的部分?🤯
## § Omniflake(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#omniflake)
omniflake(https://github.com/fzakaria/omniflake)将**成千上万个 Nix flakes** 封装在一个 flake 输入背后。
```
inputs.omniflake.url = "github:fzakaria/omniflake";
inputs.omniflake.inputs.nixpkgs.follows = "nixpkgs";
```
一旦你添加了 omniflake,就可以像使用任何其他输入一样使用它。
一个包,在 shell 中或系统上:
```
environment.systemPackages = [
omniflake.flakes.nh.packages.${system}.default
];
```
一个 overlay:
```
nixpkgs.overlays = [ omniflake.flakes.rust-overlay.overlays.default ];
```
一个 NixOS 模块:
```
imports = [ omniflake.flakes.disko.nixosModules.disko ];
```
或者完全不用在 flake 中声明,直接从命令行使用:
```
$ nix run 'github:fzakaria/omniflake#flakes.nh.packages.x86_64-linux.default' -- --version
```
它有一个易于访问的网站 https://omniflake.com/ ,你可以查看它包含的 flake 列表以及一些有用的文档。
添加后,你可以按需惰性访问近**一万两千个 flakes**(截至本文写作时)。你仅为所用付费。
这听起来很荒谬,但它确实有效。一个拥有数千个输入的 flake 本应无法使用,但得益于 Nix 语言的惰性和 flake lock 机制,它完美运行。
如何在一个 flake 中包含几乎所有的 flake?
## § Flake 入门简述(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#a-very-short-primer-on-flakes)
一个 flake 是一个包含 `flake.nix` 的目录,该文件声明两样东西:`inputs`(它依赖的其他 flakes)和 `outputs`(这些输入的函数)。
```
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: {
packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
};
}
```
`inputs` 声明了你依赖的*内容*,笼统地说:“nixos-unstable” 是一个移动的分支。其旁的 `flake.lock` 则*精确*指明了解析到的具体提交,从而使构建可复现。它相当于 npm 的 `package-lock.json` 或 `Cargo.lock`。
锁文件不仅固定了你的输入,它固定了每个 flake 的整个传递图。这意味着如果两个子 flake 都依赖 `nixpkgs`,它们各自的锁文件中会有自己的 `nixpkgs` 副本,并且可能是不同的提交。
```
{
"nodes": {
"root": { "inputs": { "agenix": "agenix", "nixpkgs": "nixpkgs" } },
"agenix": { "inputs": { "home-manager": "home-manager" },
"locked": { "rev": "5182...", "type": "github" } },
"home-manager": { "inputs": { "nixpkgs": "nixpkgs_2" }, ... },
"nixpkgs": { "locked": { "rev": "9fbb...", "type": "github" } },
"nixpkgs_2": { "locked": { "rev": "50ab...", "type": "github" } }
}
}
```
在这个例子中,**有两个 nixpkgs**:`nixpkgs` 和 `nixpkgs_2`。这就是大家抱怨的重复,也是 `follows` 存在的原因:`follows` 将依赖重写为指向你已有的节点,而不是获取另一个副本。这样做减小了图的大小,*但*构建所用的 nixpkgs 不再是作者测试时的精确版本。
这是一个使用 `follows` 统一 nixpkgs 的简单 flake 示例:
```
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
agenix.url = "github:ryantm/agenix";
agenix.inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
}
```
实线箭头表示输入;虚线表示 `follows` 边,它们汇聚到一个共享的 nixpkgs 上。
## § 输入是惰性的(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#inputs-are-lazy)
Nix 许多疯狂特性的美妙之处在于它是一种惰性语言。任何输出未触及的输入都不会被获取。
你可以通过破坏性测试来证明:锁定一个具有两个输入的 flake,然后破坏 `flake.lock` 中的一个条目,使其根本无法解析。
```
$ sed -i 's/2810303efc.../0000000000000000000000000000000000000000/' flake.lock
$ nix eval .#justB
[ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ]
```
这个包含不存在的修订版本的锁文件依然能正常求值。只有强制使用被污染的输入才会报错:
```
$ nix eval .#useA
error: unable to download '.../0000000000000000000000000000000000000000.tar.gz': HTTP error 404
```
当你添加一个 flake 作为输入时,它的整个传递图会作为*元数据*复制到你的锁文件中,不会获取任何内容,也不会进行任何求值。
```
$ time nix flake lock
• Added input 'mega'
• Added input 'mega/a' <- 被污染的那个
• Added input 'mega/a/nixpkgs'
real 0m0.084s
```
## § 一次错误的开始(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#false-start)
我最初尝试创建一个*庞大的单一 flake*,是将每个 flake 都作为输入直接添加到 `flake.nix` 并锁定。
```
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
# ... 还有 11,000 多个 ...
};
}
```
令人惊讶的是,尽管 Nix 是惰性的,当输入增多时,求值一个不触及任何输出的过程变得**极其缓慢**。开销不在于求值本身。然而,为了首次创建锁文件,Nix 必须对每个输入进行求值。
当 Nix 创建锁文件时,每个节点都需要一个唯一的名称,名称冲突通过添加 `_2`、`_3` 等后缀来解决。代码每次都会从 `_2` 开始搜索,如果有 1,000 次冲突,它会尝试 `_2`、`_3`、... 直到 `_1000`。
一个巨型 flake 必然导致冲突:每个 flake 都带来自己的 `systems` 输入,因此 4,000 个输入产生了 3,999 个名为 `systems_2` 到 `systems_4000` 的节点,每次求值约有 800 万次字符串格式化操作。这导致了输入数量的二次时间复杂度,是速度变慢的原因。
修复方法相对简单:记住每个名称已使用的最高后缀,并从此处继续。我提交了 NixOS/nix#16387(https://github.com/NixOS/nix/pull/16387),它生成的锁文件与之前完全相同。
之前没有人写过如此巨大的 flakes,因此这种二次开销未被察觉。修复相对简单,性能提升显著:对于 4,000 个输入,速度提高了约 21 倍。
尽管有了这个修复,这仍然是一次错误的开始。为了完成 `flake.lock` 文件的创建,Nix 必须对每个输入进行求值。Nix 一次锁定一个输入,获取每个树以读取其 `flake.nix`,而一个无法锁定的输入会导致整个过程终止。[11] 我尝试在 Nix 外部从每个输入的 `flake.lock` 文件直接组装锁文件,但它总是与 `nix flake lock` 不一致,并导致 Nix 重新执行整个锁定过程。
于是我深入源码,想弄清楚消费者如何处理继承的锁文件。答案比我想象的要简单。当你添加一个 flake 作为输入时,Nix 会根据其 `flake.nix` 检查该 flake 的*直接*输入,并将更深层的所有内容原样复制到你的锁文件中。求值时,将锁文件转换为 flake 的代码对每个节点执行一个操作:获取被固定的树,导入其 `flake.nix`,并使用锁文件中命名的输入调用 `outputs`。
这只是一个表查找和一次获取。它不需要这些 flakes 成为*输入*。
## § 这些 flakes 不是输入(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#the-flakes-are-not-inputs)
事实证明,我们完全可以避免使用 `inputs`。omniflake 的 `flake.nix` 仅声明了五个输入:nixpkgs 和另外四个小型库。
它包含一个所有 flakes 的固定索引(JSONL 格式)。`index.json` 的每一行都是与 `flake.lock` 条目中相同的 `locked` 对象,这正是 `builtins.fetchTree` 在纯求值模式下获取一棵树所需的内容:
```
{
"disko": {
"locked": {
"narHash": "sha256-RxWs...",
"owner": "nix-community",
"repo": "disko",
"rev": "ff8702b4...",
"type": "github"
}
}
}
```
一万两千行,每一行代表一个不同的 flake,在你明确请求之前,读取它们没有任何开销。
当一个 flake 被请求时,例如 `omniflake.flakes.disko`,库会运行一个小加载器,复制 Nix 本身处理锁文件的行为。
库获取该固定版本,读取 disko 自己的 `flake.lock`,获取并导入它命名的每个输入,然后调用 `outputs`。它支持 `follows` 和 flake 的任何其他特性,因为它就是按照 Nix 的方式来求值该 flake。[22] 有些 flakes 没有 `flake.lock` 文件。对于这些 flakes,我们在 omniflake 中直接存储一个 `flake.lock`,以便它们可以像有一样被求值。这是一种变通方法,但它有效。
保持图的小巧和避免重复的愿望通过 `overrides` 属性来满足,它允许你用自己的任何输入替换任何输入。
omniflake 提供了一个 `flakes` 属性,该属性已在许多流行的 flakes 之间进行了高度统一,例如 `nixpkgs` 或一个 `pinned` 属性集,其中包含每个 flake 作者原始意图的精确版本。
```
# nixpkgs 和那四个库是你的
omniflake.flakes.disko
# 完全按照 disko 作者锁定的版本
omniflake.pinned.disko
# 你的策略
omniflake.lib.withOverrides { nixpkgs = nixpkgs-stable; }
```
包含的 flakes 是从 GitHub 抓取并自动更新的。每个 flake 也会定期更新。
## § 这是否被诅咒?(https://fzakaria.com/2026/08/28/one-flake-to-rule-them-all#is-this-cursed)
可能吧。这种机制有效,而且符合我想要的用户体验。我可以永远不必再考虑添加单个 flake 输入。
性能出奇地好,因为我们仅为所用付费。添加 omniflake 使 `nix flake lock` 耗时约 1.5 秒,仅继承六个节点,并且不下载其背后的数千个 flakes 中的任何一个:
```
$ time nix flake lock
real 0m1.5s
$ nix eval github:fzakaria/omniflake#lib.count
11975
```
我热爱联邦制的理念,但我想要集中管理的简洁性。作为 Nix 用户,我想要鱼与熊掌兼得:omniflake 就是我的蛋糕。
相似文章
Nix Flakes 及其在 Guix 中的对应物
详细比较 Nix Flakes 与 Guix 包管理系统中的对应物,涵盖依赖声明、锁定、纯净性、输出、开发环境和系统配置。
GuixPkgs:每个 Guix 包,作为 Nix flake
GuixPkgs 是一个项目,它将每个 GNU Guix 包以 Nix flake 的形式提供,使用户能够在单个 flake 中混合使用 Guix 和 Nixpkgs 包。它使用 guix-transfer 将 Guix 派生转换为 Nix 派生,并提供二进制缓存以避免完全重建 Guix 引导过程。
使用Nix构建系统软件
一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。
使用Nix的开发环境:四个快速示例
本教程演示了使用Nix设置开发环境的四种方法,包括交互式一次性使用、配置文件以及密封的Nix Flakes,并以GoCV和OpenCV为例。
NNN Stack: NixOS, Niri, Noctalia
NNN Stack 结合了 NixOS、Niri 组合器和 Noctalia shell,创建了一个声明式、可滚动且可复现的桌面环境,并邀请用户贡献他们的 dotfiles。