# Nix存储的本质是三个函数 来源:https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions 在构建trynix (https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser)的过程中,我需要托管一个在cache.nixos.org (https://cache.nixos.org/)上不存在的store-path。同时我还在等待@domenkozar (https://github.com/domenkozar)为cache.nixos.org (https://cache.nixos.org/)启用CORS,以便使用它。我想证明非Nixpkgs的store-path也能同样轻松地启动。唯一的要求似乎是宽松的跨源资源共享(CORS)(https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)策略——`access-control-allow-origin: *`,因为获取操作是在JavaScript中进行的。结果发现GitHub Pages在其托管的每个文件上都设置了该头信息。😈 我将`nix copy --to file://`的输出提交到了我的Git仓库 (https://github.com/fzakaria/trynix/tree/main/site/examples/cache),瞧,我就有了一个*免费的*Nix替换器。关于这个发现,我似乎有些后知后觉了。tomberek的github-store (https://github.com/tomberek/github-store)就是一个由GitHub release资源组合而成的缓存。为了成为Nix二进制缓存,需要从narinfo的`URL`字段中去除`nar/`前缀,因为GitHub releases是扁平的命名空间。
```bash
$ B=https://github.com/tomberek/github-store/releases/latest/download
$ curl -sL $B/nix-cache-info
StoreDir: /nix/store
$ curl -sL $B/1zy01hjzwvvia6h9dq5xar88v77fgh9x.narinfo
StorePath: /nix/store/1zy01hjzwvvia6h9dq5xar88v77fgh9x-glibc-2.38-44
URL: 0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si.nar.xz
Compression: xz
FileHash: sha256:0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si
FileSize: 6514112
NarHash: sha256:1ikxmc8yxzmm9vzzaa313w9yzrrgm0p6cgscy8arq3z32kynpi94
NarSize: 30244048
References: 1zy01hjzwvvia6h9dq5xar88v77fgh9x-glibc-2.38-44
a3n1vq6fxkpk5jv4wmqa1kpd3jzqhml9-libidn2-2.3.4
...
Deriver: 3fd7s6gjwi6rxfqw00bjq9ghnvazvnnn-glibc-2.38-44.drv
Sig: cache.nixos.org-1:YWkvHMXyvOw1iqWblEdvju+OZbGOvwYXyjyRUxuXluFq17xwJFSzHb0HrIuRr9BXYjV6GxfmGGv+ckVNvAOpDQ==
$ curl -sL $B/0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si.nar.xz | wc -c
6514112
```
GitHub Pages和Releases都是静态文件服务器。它们根本不知道Nix是什么。如果一个普通的文件服务器就能充当Nix二进制缓存,我们还能用什么呢?
## § [接口](https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions#the-interface)
事实证明,要成为一个Nix二进制缓存,只需实现三个简单的函数。Nix客户端并不关心你使用什么媒介来实现它们,尽管HTTP是最常见的方式,并且默认包含在CppNix (https://github.com/NixOS/nix)中。如果你想实现新的协议,也可以编写一个Nix插件。
```
GET nix-cache-info → StoreDir: /nix/store
GET <32-char hash>.narinfo → 命名存档的元数据
GET <url> → 压缩的存档
```
就这些。任何能响应这三个请求的东西都可以用作远程*Nix存储*。我们将看到,它们甚至不需要位于相同的媒介、协议或域名上!
## § [关于签名](https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions#what-about-the-signatures)

我们可以如此随意对待传输媒介的原因在于,Nix并不信任它。narinfo的签名(`Sig`)字段覆盖了`StorePath`、`NarHash`、`NarSize`和`References`。它不包含`URL`、`FileHash`、`FileSize`或`Compression`。一旦获取到存档,Nix会解压它并检查`NarHash`是否匹配。这就是*关键秘诀*,使得由cache.nixos.org (https://cache.nixos.org/)签名的软件包可以从任何其他二进制缓存作为中介获取,并且签名仍然有效。`URL`字段甚至不必与narinfo位于同一主机上。它可以位于互联网上的任何位置,也可以是不同于HTTP的协议。Nix并不在意。唯一重要的是从`URL`获取的存档具有与narinfo相同的`NarHash`。
## § [替代存储](https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions#alternative-stores)
对于未包含在Nix客户端默认支持中的协议,你始终可以编写一个HTTP代理,将这三个函数转换为你想要的任何媒介。在为本文进行调研时,我发现了一些有趣的例子。
**gachix (https://github.com/EphraimSiegfried/gachix)**:将存档放入git的对象数据库。Git已经对内容进行寻址并使用增量压缩存储数据块,因此存储自身就能去重;作者报告其大小比等效的普通缓存小约82%。
**DNS**:我写了一个概念验证,将narinfo和4 KiB的存档分片放入TXT记录。narinfo足够小可以放在一条记录中,但存档需要被分块。
**pastebin (https://pastebin.com/)**:一个paste服务可以存放narinfo和存档。narinfo足够小可以放在一个粘贴中,但存档需要被分块。许多paste服务有过期策略,这可以作为天然的垃圾回收器。
**nixcache-oci (https://github.com/cmspam/nixcache-oci)**:使用OCI注册表来存储Nix存档。
**infinite storage glitch (https://github.com/KKarmugil/Infinite_Storage_Glitch)**:将数据编码到视频中并上传到YouTube。
## § [npm](https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions#npm)
> “一切都可以在npm上找到”——互联网上的某位人士
毫不奇怪,npm是一个*出色的*二进制缓存,并且它具有一些我们可以利用的、与版本管理相关的有趣特性。`nix copy --to file://`会输出一个目录,而npm可以发布目录:天作之合。💑 让我们通过一个小型的`hello`示例来了解。
```nix
packages.${system}.default = pkgs.hello.overrideAttrs (old: {
pname = "hello-npm";
# 上游测试套件会grep原始的问候语。
doCheck = false;
postPatch = (old.postPatch or "") + ''
substituteInPlace src/hello.c \
--replace-fail 'Hello, world!' 'Hello from the npm registry!'
'';
});
```
它动态链接到glibc,因此闭包包含五个路径,大约36 MiB:
```
$ nix path-info -rSh result
/nix/store/yh8rykx8wakl1ccn8rc351f6r2wbg4cn-libunistring-1.4.2 2.0 MiB
/nix/store/nga9d6m9iplygw3iqghk2g840nz7b0gy-libidn2-2.3.8 2.3 MiB
/nix/store/ssvq1r0xd8f7paf6zqgpfql1a4drwhy2-xgcc-15.3.0-libgcc 193.0 KiB
/nix/store/n51dhmdbik1kfrsm62j5knavmigwrl1a-glibc-2.42-84 36.0 MiB
/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3 36.2 MiB
```
我们将其复制到本地缓存,使用我们自己的密钥签名,并添加npm所需的单个文件(`package.json`):
```
$ nix key generate-secret --key-name hello-npm-1 > cache-key.sec
$ nix key convert-secret-to-public < cache-key.sec > cache-key.pub
$ nix copy --to "file://$PWD/cache?secret-key=$PWD/cache-key.sec" .#
$ cd cache && rm -rf log build-trace-v2 && npm init --scope=@fzakaria -y
```
然后`npm publish`会忠实地为我们打包完整的闭包:
```
$ npm publish --access public
npm notice 606B 3ssib4ic89qw7x1gha10s6mdf42fk15v.narinfo
npm notice 767B n51dhmdbik1kfrsm62j5knavmigwrl1a.narinfo
npm notice 7.3MB nar/06fsxd8j9ck4ls5b8xj4r4zk2642xzxzmdj6bl93zfwcm4pqzx0z.nar.xz
npm notice 467.5kB nar/1asdj56kfpvbqh8bpg5wns4v0yd1p3ldl6p2swv6nfwfdgvbwklc.nar.xz
npm notice 21B nix-cache-info
npm notice name: @fzakaria/hello-nix-cache
npm notice version: 1.0.0
npm notice package size: 8.0 MB
total files: 12
```
`@fzakaria/hello-nix-cache` (https://www.npmjs.com/package/@fzakaria/hello-nix-cache)现在是公共npm注册表上的一个真实包。它现在是一个你可以直接将Nix指向的替换器:
```
$ nix copy --from https://unpkg.com/@fzakaria/hello-nix-cache@latest/ \
--to ./npmstore \
--trusted-public-keys 'hello-npm-1:u4SntZm2u3sob9zBw2OG6yePvJGoRRQUk00LQh4pnKA=' \
/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3
copying 5 paths...
copying path '/nix/store/ssvq1r0xd8f7paf6zqgpfql1a4drwhy2-xgcc-15.3.0-libgcc' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...
copying path '/nix/store/n51dhmdbik1kfrsm62j5knavmigwrl1a-glibc-2.42-84' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...
copying path '/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...
$ bwrap --ro-bind ./npmstore/nix/store /nix/store --proc /proc --dev /dev \
/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3/bin/hello
Hello from the npm registry!
```
> **注意**:我们必须使用`bwrap`来运行二进制文件,因为`./npmstore`是一个*chroot存储*,所有路径仍然位于`/nix/store`下。如果我们拥有可重定位的二进制文件 (https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries),我们就可以直接运行它。
这就是Nix从npm获取完整闭包并运行它的过程。🤯 我们可以将Nix软件包分发给非Nix用户,让感染蔓延开来!
作为额外的好处,类似于Nixpkgs和NixOS,我们可以利用npm的dist-tags获得良好的“通道”语义。`latest`标签是可变的,指向最新版本,而每个版本都是不可变的,指向特定的store-path。
```
$ npm dist-tag add @fzakaria/
[email protected] staging
$ npm dist-tag add @fzakaria/
[email protected] production
```
这种方法的主要缺点是npm没有增量发布功能。每个版本都是一个完整的压缩包,因此五十个共享glibc的闭包会上传glibc五十次。我们*可以*通过将每个store-path发布为单独的包,然后让一个小的索引包指向它们来解决这个问题。这样每个store-path就只会被上传一次。但我不会构建这个,因为这对npm生态系统不够尊重。我们还能找到哪些其他的存储实现?