nixpkgs-multiverse:收录所有曾经存在过的 Nixpkgs 版本
欢迎来到 Nixpkgs 多元宇宙。所有曾经存在过的版本,尽收一处。
我升级了 NixOS 配置中的 nixpkgs 版本以刷新众多软件包,结果发现我所依赖的某个特定版本的软件包已经不可用了。
那个软件包被"向前升级"了,导致我的部分工具链出了问题。夜已深,我也不想修,于是就再加一个 nixpkgs 输入,固定到包含所需版本的那个提交上。这样虽然能跑,但体验相当糟糕,而且会越来越糟。
{
inputs = {
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs-25_11.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
};
}
对最新版软件包的需求太常见了,我不得不长期维护一个 overlay,把 unstable 注入为一个独立的 package set,方便随时取用。
unstable-packages = final: _prev: {
unstable = import inputs.nixpkgs-unstable {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
overlays = [inputs.nix-vscode-extensions.overlays.default];
};
};
如果我需要某个特定版本的软件包,而当前的 nixpkgs 里没有,那就只能去翻找对应的提交然后固定它。11还好有 nixhub.io 和 lazamar 的版本搜索 这类网站,让这件事稍微轻松一点。
每多固定一个版本,配置文件里就多出一整个 nixpkgs。Flake 的 inputs 是急切获取的,哪怕实际上没用上也会被拉取。一个 flake 即便只引用了三个 nixpkgs inputs 中的第一个,另外两个也照样会被完整物化。
Nix 让我们能轻松构建一个闭包来复现软件包的某个特定版本,但 Nixpkgs 却很难让你在万物更新的同时,单独把某一个包钉死在原地。
flake 中的每个 Nixpkgs input 都是一个独立的小宇宙。既然我们已经能用多个 Nixpkgs 作为输入来获取某个特定版本的包,那为什么不干脆让所有曾经存在过的版本随时可用呢?🤯
§nixpkgs-multiverse
nixpkgs-multiverse 就是这样一个 flake input —— 一个输入,通通打包给你。
$ nix run 'github:fzakaria/nixpkgs-multiverse#versions.python3."3.6.2"' -- --version
Python 3.6.2
$ nix run 'github:fzakaria/nixpkgs-multiverse#versions.python3."3.8.9"' -- --version
Python 3.8.9
# 也可以获取某个包的最新版本
$ nix run 'github:fzakaria/nixpkgs-multiverse#latest.python3' -- --version
Python 3.14.6
我们可以查询该 flake,获取**曾在 Nixpkgs 中出现过的**该包的所有版本。
$ nix eval --json --apply 'f: f "python3"' \
github:fzakaria/nixpkgs-multiverse#multiverse.x86_64-linux.versionsOf
[
"3.5.3",
"3.6.2",
# 为简洁起见省略 53 个其他版本
# ...
"3.13.13",
"3.14.6"
]
如果我们想要某个特定的 Nixpkgs 完整版本,可以使用 at 函数。
let
mv = multiverse.multiverse.x86_64-linux;
# 索引中已知的最新版本,作为真实的 Nixpkgs
pkgs_tip = mv.tip;
# 按发布版本
pkgs_24_11 = mv.at "24.11";
# 该日期之前最新的版本
pkgs_2022_03_15 = mv.at "2022-03-15";
# 按 commit
pkgs_aae12a743f75 = mv.at "aae12a743f75";
in {
packages = [
pkgs_tip.python3
pkgs_24_11.python3
pkgs_2022_03_15.python3
pkgs_aae12a743f75.python3
];
}
这样就能访问 Nixpkgs 中曾出现过的所有包的所有版本。你可以随意将它们混用在同一个 shell、同一个包或同一个构建环境中。

为什么可以同时存在多个 Python 版本?这正是 Nix 本身的精髓所在。每个包都通过意图模型,借助哈希精确地描述其依赖关系。22哈希是用于标识构建该包时所使用输入集的唯一标识符。只要修改了任何输入,哈希就会变化,你将得到一个新包。
Nixpkgs 已经支持在同一个版本中以不同属性(如 python39、python312、gcc12)的形式提供同一个包的多个版本。我们则把这个思路推到了它的逻辑终点——让所有版本都能轻松获取。
§这背后的魔法是什么?
我们的 flake.nix 故意不设任何输入:inputs = { }。输入会被急切地(eagerly)拉取,而我们有多达 1,393 个。我们需要按需惰性拉取,仅在实际引用某个版本时才去获取。具体做法是:用 narHash 固定版本,通过 builtins.fetchTree 按需获取。
所有工作都由两个文件完成:revisions.json 和 versions.json。
revisions.json 是一个有序数组,包含了 Nixpkgs 的每一个版本——本文撰写时共 1,393 个,时间跨度从 2017 年到 2026 年。33 NixOS 的发布版本并无特别之处;它就是一个恰好带有 release 标签的提交。
[
{
"rev": "0eeebd64de89…",
"date": "2023-06-12",
"channel": "nixos-unstable",
"narHash": "sha256-2xT+Jmk3m…"
},
{
"rev": "afb2b21ba489…",
"date": "2025-05-23",
"channel": "release",
"release": "25.05",
"narHash": "sha256-rWtXrcIzU5wm…"
}
]
我们只挑选那些被 Hydra 实际构建并缓存过的提交,所以入选的要么是发布版本,要么是 nixos-unstable 渠道的更新节点。
那么怎么知道哪些版本对应 nixos-unstable 渠道呢?
我们借助 nix-releases S3 桶 来确认哪些提交真正发布了构建产物。该 S3 桶以提交哈希作为目录名,因此只需列出桶内容,就能拿到所有实际被构建过的版本的完整清单。
index/versions.json 是一个从 (属性, 版本) 到版本的映射:
{
"revisionCount": 1393,
"attrs": {
"python3": { "3.8.9": 412, "3.12.10": 1204 }
}
}
其中的整数是 revisions.json 数组中的偏移量,表示发布该版本的最新版本(revision)。
§稀疏数据
版本数量如此庞大时,数据的编码方式就显得至关重要。我最初的编码方式是把每个版本出现过的每一个版本都记录下来。
虽然写法简单,但 JSON 文件体积膨胀得吓人。正如你所料,大多数包的多数版本在跨多个版本时并没有变化。我们的 versions.json 文件大小随着版本数量线性增长。
通过只存储每个版本的最新修订版本,文件可以保持小巧,同时仍然能回答"哪个修订版本包含该版本"的问题。下面是随着被索引的修订版本增多,文件大小的实际增长曲线:
5.18 MB,覆盖了 1,393 个修订版本以及 289,521 个不同的 (属性, 版本) 配对。
§性能
我们这个 flake 的核心设计原则是:
开销按触及的修订版本计算,而非按包计算。
如果把各个修订版本作为输入加入,flake 的求值过程会爆炸式膨胀。下面测量中的每个 flake 都包含 N 个 nixpkgs 输入,但输出只引用了第一个输入;计时的是从开始到该输出完成求值所花费的时间。44全部使用 git+file 方式从本地克隆读取,因此不存在网络延迟。
五个未使用的 nixpkgs 钉版本在输出求值前就消耗了 26 秒。每个输入大约花 5 秒,即使从未使用,输入依然会被获取并物化。相比之下,绿线是含有 1,393 个可用修订版本的 nixpkgs-multiverse,解析 JSON 只需平稳的 0.20 秒。🤩
修订版本的结果有记忆化(memoised),所以从一个修订版本中取出 3 个包和取出 1 个包的开销是一样的。
§为什么我喜欢这个方案
/nix/store 能够容纳同一个包的多个版本图,这是理解 Nix 的核心理念之一。flakes 的流行和兴起让我们更加清楚地认识到,可以把 Nixpkgs 的多个修订版本混合在一起使用。
我反复回味的观点是:Nixpkgs 的历史已经就是多重宇宙了。每一个曾经存在过的版本都已经构建过、都已经缓存好、都可以触达。它只是通过 commit 哈希而不是版本来寻址,而这恰好跟人们自然的思维方式反了。
整个项目就是 5 MB 的 JSON 加大约 200 行 Nix 代码。它不构建任何东西,不镜像任何东西,也不托管任何东西。它就是一本电话簿。
inputs.multiverse.url = "github:fzakaria/nixpkgs-multiverse";