缓存时间:
2026/10/13 20:01
# 面向前端残疾人的 Nixpkgs
来源:https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired
> **tl;dr** jinx (https://fzakaria.github.io/jinx) 是一个将 Nix 编译为 TypeScript 的工具。整个 nixpkgs,每一个属性,共 231,777 个,都生成与 Nix **完全一致的** drvPath。打开 playground (https://fzakaria.github.io/jinx),或者直接在你的浏览器标签页中求值 hello (https://fzakaria.github.io/jinx/nixpkgs/#attr=hello),无需在任何地方安装 Nix。😈
尽管我对 Nix 这门语言和工具充满钦佩和喜悦,但真正神奇的并不是它们。Store 模型才是那个伟大的创意。任何基于同一模型构建的类似工具,都将拥有同样的可复现性和封闭性等神奇特性。
我写了很多 (https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions) 文章 (https://fzakaria.com/2026/09/24/every-package-is-already-installed) 来探索 Nix 生态系统所派生出的一切,但这门语言本身只是一个带有派生(derivation)原语的小型惰性函数式语言。你可以在一个周末内设计出另一个。
但你在周末内设计不出来的是 nixpkgs (https://github.com/NixOS/nixpkgs):二十年的修复、每一个交叉编译的变通方案、每一个 `stdenv` 阶段、每一个被修补过的 `configure` 脚本、每一个"这个库在 aarch64-darwin 上需要这个标志位",这些都已经有人解决了。截至 2026 年 10 月,它包含 14 万多个可以构建的包。这就是秘方 🥫,而且它是用 Nix 写的,所以如果你想要它,你就得写 Nix。
不幸的是,这也是陷阱所在。任何想从不同方向探索 store 模型的人——无论是新语言、新求值器还是新的编辑器体验——都必须从零个包开始。这就是其他 Nix 重新实现或分叉走向消亡的坟场。
然而,如果你能把整个 nixpkgs 的树转换成另一个生态系统已经能"说"的东西,你就可以在不放弃 nixpkgs 二十年积累的前提下探索 store 模型。💡
我选择了 TypeScript。为什么?为什么不呢?
《侏罗纪公园》中的 Ian Malcolm:你的科学家们太专注于能不能做,而没有停下来想想该不该做 (https://fzakaria.com/assets/images/jurassic_park_jinx_meme.jpg)
TypeScript 拥有丰富的工具生态,且被*几乎*所有浏览器支持。它有一个极其灵活的类型系统,这意味着我们可以表达派生(derivation)的形态以及 Nix 惰性求值模型中的所有奇特之处。
我是一个前端残疾的"菜鸟",但我认为这个想法值得探索。
jinx (https://fzakaria.github.io/jinx) 就是结果。它是一个转译器,接受一个 `.nix` 文件并生成一个 `.ts` 文件,该文件求值得到相同的值,拥有相同的 thunk、相同的派生字符串和相同的 drvPath。
然后我用它编译了整个 nixpkgs。每一个属性、每一个包、每一个模块、每一个 `stdenv` 和 `lib` 函数:总共 231,777 个属性。
编译后的树位于 fzakaria/jinx-nixpkgs (https://github.com/fzakaria/jinx-nixpkgs),每个 nixpkgs 修订版对应一个 commit。
## § 它做了什么 (https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired#what-it-does)
每个 `.nix` 文件变成一个同名的 `.ts` 模块,生成在一个与源码镜像的目录树中。lambda 是箭头函数,`let` 是一组 const 块,属性集是对象字面量,`import` 是 `require`。你*可以读懂它*。
```
$ jinx compile pkgs/by-name/he/hello/package.nix
```
```
// Generated by jinx from .../pkgs/by-name/he/hello/package.nix. Do not edit.
import * as nix from "../../../../.jinx/runtime/index.ts";
const here = nix.here(import.meta.url);
const formals$1 = { args: { callPackage: false, fetchurl: false, hello: false, lib: false, nixos: false, stdenv: false, testers: false }, ellipsis: false };
export default nix.lambda((args) => {
const $ = nix.formals(args, formals$1);
const callPackage = nix.arg($, "callPackage");
const fetchurl = nix.arg($, "fetchurl");
...
return nix.app(nix.sel(stdenv, "mkDerivation"), (finalAttrs) => ({
pname: "hello",
version: "2.12.2",
src: nix.thunk(() => nix.app(fetchurl, {
url: nix.thunk(() => nix.str`mirror://gnu/hello/hello-${nix.sel(finalAttrs, "version")}.tar.gz`),
hash: "sha256-...",
})),
...
}));
}, formals$1);
```
Nix 是惰性的,所以生成的代码在 Nix 会分配一个 thunk 的地方,用 `nix.thunk(() => ...)` 包裹表达式。🤮
`jinx` 求值不会向 `/nix/store` 写入任何东西。`jinx instantiate` 写入 `.drv` 闭包,然后 `nix-store --realise` 构建派生。
```
$ jinx eval hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
$ nix-instantiate --eval --strict hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
```
我们生成的 `.drv` 和 `drvPath` 与 Nix 产生的**完全相同**。这就是在另一个生态系统中解锁 nixpkgs 的关键:store 无法分辨差异。
## § 翻译无损 (https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired#nothing-lost-in-translation)
我们可以证明翻译是忠实的。我们用 `jinx` 和 nix-eval-jobs (https://github.com/NixOS/nix-eval-jobs) 分别求值 nixpkgs 自身 CI 配置中的每个属性,即 ci/eval/outpaths.nix (https://github.com/NixOS/nixpkgs/blob/6510408d147d8c5e6d76043e899e9251756f6a75/ci/eval/outpaths.nix),然后比较所有 `drvPath`。
在 nixpkgs 6510408d (https://github.com/NixOS/nixpkgs/commit/6510408d147d8c5e6d76043e899e9251756f6a75) 上,在 x86_64-linux、aarch64-linux 和 aarch64-darwin 上:
零差异或单侧失败。^1^ 在两侧都失败的 2,714 个是 Nix 拒绝求值的属性,比如不支持的平台、broken 或 insecure 的包。
## § 但说真的,为什么?(https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired#but-seriously-why)
在 TypeScript 中求值 nixpkgs 慢了约 3-5 倍。如果它只是这样,那就没什么意思了。重点不是让 Nix 变快,而是让 nixpkgs 对 TypeScript 生态系统*可用*。
以下是一些你可以用它做的"有趣"的事。
**对求值进行性能剖析。** 对 `firefox.drvPath` 运行 `node --cpu-prof`,通过 source map 折叠后,每一帧都对应 nixpkgs 中的一行。点击查看完整 SVG。
火焰图顶部,求值 firefox.drvPath 的过程,每一帧都是 nixpkgs 中的一行 (https://fzakaria.com/assets/images/jinx_flamegraph_firefox.svg)
**在编辑器中补全 nixpkgs。** 为 nixpkgs 中的每个属性提供自动的语言服务器协议(LSP)支持,并带有指向原始 `.nix` 文件的 source map。TypeScript 的类型系统表现力足够强,可以建模派生的形态,因此你可以免费获得补全和类型检查。
pkgs.neo 补全出 neovim、neofetch 及其余匹配项;pkgs.rigrep 被标记为错误 (https://fzakaria.com/assets/images/jinx_typed_nixpkgs.png)
**单步调试一个包。** Source map 可以将断点放在某一行 `.nix` 代码上。在 VS Code 或 Chrome DevTools 中打开编译后的树,在 `pkgs/by-name/he/hello/package.nix` 中断点,然后在 `stdenv.mkDerivation` 构建属性集时单步进入。
```
$ node --inspect-brk --enable-source-maps src/cli/main.ts eval hello.nix
```
**在循环中向 nixpkgs 提问。** 在 REPL 或脚本中查询和探索 nixpkgs,无需记住那些获取整个树的 JSON 转储并用 `jq` 处理的咒语。
```
import { loadNixpkgs } from "jinx";
const pkgs = loadNixpkgs("/path/to/nixpkgs");
const gpl = [];
for (const name of Object.keys(pkgs.lib.licenses)) {
if (pkgs.lib.licenses[name].spdxId?.startsWith("GPL")) {
gpl.push(name);
}
}
console.log(JSON.stringify(gpl));
```
**用 node:test 为 Nix 编写单元测试。** 一个 `lib` 函数或包定义就是模块中的一个函数,所以它可以像任何其他语言中的任何其他函数一样编写单元测试。
```
test("hello builds the derivation Nix would", () => {
assert.equal(
pkgs.hello.drvPath,
"/nix/store/cnraiw0hbdl6jzdhbkn8dah2hqj7xgij-hello-2.12.3.drv",
);
});
```
**用 TypeScript 编写一个包。** `jinx` 的编译产物忠实到每一个 thunk 都与 Nix 一致,这使得它精确但手工编写极其痛苦。还有一种更好看的手写形式。它可以与 nixpkgs 互操作,因此 TypeScript 中的包可以调用 `stdenv.mkDerivation` 并依赖 `pkgs` 中的任何东西。
```
const hello = pkgs.stdenv.mkDerivation({
pname: "hello",
version: "2.12.2",
src: pkgs.fetchurl({
url: "mirror://gnu/hello/hello-2.12.2.tar.gz",
hash: "sha256-WpqZbcKSzCTc9BHO6H6S9qrluNE72caBm0x6nc4IGKs=",
}),
meta: { license: pkgs.lib.licenses.gpl3Plus },
});
console.log(hello.drvPath);
```
**它可以在浏览器中运行。** TypeScript 在每个现代浏览器中都被支持。你可以在标签页中求值 nixpkgs,它会在需要时从 GitHub 获取编译后的模块。^2^ 也许可以搞些有趣的*code golf*,生成输出路径,然后用 trynix (https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser) 引导启动,全程不使用 Nix。
## § 它慢吗?(https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired#is-it-slow)
是的,它很慢,但关键是没有*太*慢。
在热缓存状态下,模块已编译且 V8 代码缓存已填充,在我的笔记本上每个属性:
首次求值(需要编译它触及的每个模块)速度慢 3 到 10 倍,内存约为 2-4 倍,因为 thunk 对垃圾回收器来说非常昂贵。^3^ 完整的基准测试表和原始数据可以在 benchmark 页面 (https://fzakaria.github.io/jinx/bench/) 找到。
让我这样说:它比一个被人们优化了二十年的 C++ 解释器慢五倍,而它是一个全新的编译器,运行在 JavaScript 引擎上。在我看来,这是一个不错的起点。🤷
## § 我可以试试吗?(https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired#can-i-try)
源码位于 github.com/fzakaria/jinx (https://github.com/fzakaria/jinx)。
它没有依赖,没有本地代码,没有守护进程。你可以在 Node 24 中运行它,或者在浏览器中运行它。编译后的 nixpkgs 树位于 github.com/fzakaria/jinx-nixpkgs (https://github.com/fzakaria/jinx-nixpkgs)。
我有点恼火的是我必须不断声明我在代码中使用了"AI"。人们似乎很享受"揭发"你这件事,好像这是一种道德缺陷。让我省去他们的麻烦:一个 AI 与我共同编写了大量软件。
一个红色橡胶印章,风格类似 100% 纯牛肉标签:优质品质,100% AI,始终新鲜 (https://fzakaria.com/assets/images/100_percent_ai_stamp.png)
这是给你的印章。如果这让你无法接受,你可以停止阅读了,派生不会为你改变它们的哈希值。
nixpkgs 现在是一个 TypeScript 程序了。去在 stdenv 中设置一个断点吧。(https://fzakaria.github.io/jinx/docs/why)