Guix Nix 的怪异融合:在 Nix 中利用 Guix 派生
摘要
一项技术探索,展示了 Nix 如何构建 Guix 派生项,强调了共享底层“输入输出机”架构以及跨生态系统互操作的可能性。
<p><a href="https://lobste.rs/s/jecmzc/guix_nix_abomination_leveraging_guix">评论</a></p>
查看缓存全文
缓存时间: 2026/06/08 03:17
# Guix Nix 怪胎:在 Nix 中利用 Guix 派生
来源:https://fzakaria.com/2026/06/05/the-guix-nix-abomination-leveraging-guix-derivations-in-nix
Nix 和 Guix 看起来像是对立的生态系统,但在底层它们其实是相同的“输入输出机”。
需要证据吗?🕵 不如我们用 Nix 来构建一个 Guix 派生。
首先,在 Guix 中创建一个超级基础的派生:*Hello world*。
``
❯ guix repl -- /dev/stdin <<'EOF'
(use-modules (guix derivations) (guix store))
(with-store %store
(let ((drv (derivation %store "simple" "/bin/sh"
'("-c" "echo Hello World > $out")
#:env-vars '(("PATH" . "/bin"))
#:system "x86_64-linux")))
(format #t "~a\n" (derivation-file-name drv))))
EOF
/gnu/store/zr0q11srv4yir8a6wrz582js7zsi17ij-simple.drv
``
然后我们让 Nix 来构建它。🪄
我们要求使用 `/gnu/store` 作为 Nix 存储,并将它的状态、数据库和日志文件写入其他目录,这样就不会与 Guix 冲突或干扰。
> **注意** 实际上会更复杂一点。Nix 会检查它的 SQLite 数据库中是否有这个派生,所以我们需要先注册它。我使用的 Guix 版本(v1.5.0)利用了运行在私有挂载命名空间内的 `guix-daemon` 用户,在这个命名空间中 `/gnu/store` 是可写的,但其他人(包括我)都只读。`unshare --mount` 会创建一个新的私有挂载命名空间,这样我就可以把它挂载为读写模式,然后对其运行 Nix 命令。
``
❯ cat > /tmp/register.txt <<'EOF'
/gnu/store/zr0q11srv4yir8a6wrz582js7zsi17ij-simple.drv
822a79886102e5ca392cd14358aef0866c36ca526ff1b156f1ded2808a2095df
336
0
EOF
❯ NIX_STORE=/nix/store/fla7gi1dvkw4hvwxar8m7z25p2yv7r40-nix-2.34.7/bin/nix-store
❯ NIX_STORE_DIR=/gnu/store NIX_STATE_DIR=/tmp/nix-gnu/var/nix \
$NIX_STORE --load-db < /tmp/register.txt
❯ sudo unshare --mount bash -c '
mount -o remount,rw /gnu/store
su -s /bin/bash guix-daemon -c \
"NIX_STORE_DIR=/gnu/store NIX_STATE_DIR=/tmp/nix-gnu/var/nix \
NIX_LOG_DIR=/tmp/nix-gnu/var/log/nix \
/nix/store/fla7gi1dvkw4hvwxar8m7z25p2yv7r40-nix-2.34.7/bin/nix-store \
--realise /gnu/store/zr0q11srv4yir8a6wrz582js7zsi17ij-simple.drv"
'
warning: creating directory "/var/empty/.cache/nix": Permission denied
this derivation will be built:
/gnu/store/zr0q11srv4yir8a6wrz582js7zsi17ij-simple.drv
building '/gnu/store/zr0q11srv4yir8a6wrz582js7zsi17ij-simple.drv'...
warning: you did not specify '--add-root'; the result might be removed by the garbage collector
/gnu/store/kd5szqbl9asz5hravhnxgd9plm4a9gzh-simple
❯ cat /gnu/store/kd5szqbl9asz5hravhnxgd9plm4a9gzh-simple
Hello World
``
我们刚刚用 Nix 构建了一个 Guix 派生。🔥
这是怎么做到的?
两者都采用一个语言前端(Nix 或 Guile (Scheme)),将其编译成**派生**(配方),然后传递给一个**构建器**(守护进程)执行并生成输出。
它们之所以特殊,是因为它们都承诺了同一件事:**封闭构建**。构建输出所需的一切都在配方中声明:源代码、环境变量、依赖关系等等。
> “在 Nix 中,构建过程只能找到那些被明确声明为依赖的资源。除非一切所需都已被正确声明,否则它无法构建。如果它能构建,你就知道你已经提供了完整的声明。” – Nix OS 网站 (https://nixos.org/guides/how-nix-works/)
Guix Nix 概述
Guix,特别是它的守护进程,是早期从 Nix 分支出来的,因此两者非常相似;例如,它们共享相同的派生格式 ATerm (https://nix.dev/manual/nix/2.25/protocols/derivation-aterm)。
> Guix 基于 Nix 包管理器 – Guix 网站 (https://guix.gnu.org/manual/devel/en/guix.html#Acknowledgments)
这就是为什么我们之前的例子中,用 Nix 构建 Guix 派生几乎不需要转换就能实现。
如果我们能在 Nix 的传统 `/nix/store` 中利用 Guix 现有的配方呢?
如果我们可以将一种配方文件转换成另一种,那么我们就可以在 Nix 中使用 Guix 的现有配方,反之亦然。
事实证明,这远比你以为的可行,因为*Guix 就是 Nix*,或者至少是它的超集。
在 Claude 的帮助下,我构建了一个工具来实现这一点:guix-transfer (https://github.com/fzakaria/guix-transfer) 🤯。
> **guix-transfer** 是一个 CLI 工具,用于将 GNU Guix 派生自底向上翻译成 Nix 派生。
有点困惑?让我们看看它的实际效果:
``
# 生成一个 Guix 派生
❯ guix build hello --derivations
/gnu/store/2nfg943asrl9dv64zrr1a4kpb25mfafd-hello-2.12.2.drv
# 翻译它
❯ /guix-transfer /gnu/store/2nfg943asrl9dv64zrr1a4kpb25mfafd-hello-2.12.2.drv
Loading Guix derivation graph from /gnu/store/2nfg943asrl9dv64zrr1a4kpb25mfafd-hello-2.12.2.drv ...
Loaded 228 derivations.
Translating bottom-up ...
[228/228] done
Done. Final Nix derivation:
/nix/store/brdd8zw3j9hhq8zf27ixqyi3l61nwppn-hello-2.12.2.drv
Realise it with: nix-store --realise --option filter-syscalls false /nix/store/brdd8zw3j9hhq8zf27ixqyi3l61nwppn-hello-2.12.2.drv
# 用 Nix 构建它
# 这是一个漫长的构建过程,因为我们要从源码构建所有内容
❯ nix-store --option filter-syscalls false --realise \
/nix/store/brdd8zw3j9hhq8zf27ixqyi3l61nwppn-hello-2.12.2.drv
❯ /nix/store/j3940mdzr6qmw4ydhyla663s501vb8ns-hello-2.12.2/bin/hello
Hello, world!
``
> **注意** 当你解压 tarball 时,tar 会恢复每个文件的原始权限,包括 setuid/setgid 位。Nix 的沙箱会安装一个 seccomp 过滤器,阻止任何设置这些位的 `chmod` 调用,并返回“操作不允许”。Guix 的早期引导过程使用基于 Scheme 的 `tar`(gash-utils),它将此错误视为致命错误,而 GNU tar 则会静默跳过。解决方法是使用 `--option filter-syscalls false` 来禁用该过滤器。
如果我们还不清楚刚才做了什么:我们拿了一个 Guix 派生及其所有依赖(一直到引导种子),将其翻译成一个 Nix 派生,然后用 Nix 构建了它。😲
这个**怪物**是什么,它是怎么做到的?!
为了更好理解这是如何实现的,重新审视一下派生是什么,以及它在 Nix 和 Guix 中如何使用是很重要的。让我们看看前面那个相同的简单派生,*Hello World*。
> 如果你对这个感兴趣,可能还想看看我关于[手动创建 Nix 派生](https://fzakaria.com/2025/03/23/nix-derivations-by-hand)的另一篇文章 🤓。
``
derivation {
name = "simple";
builder = "/bin/sh";
system = builtins.currentSystem;
args = ["-c" ''
echo "Hello World" > $out
''];
}
``
当我们求值(nix-instantiate)这个派生时,我们会得到一个文件的路径,该文件包含 ATerm (https://nix.dev/manual/nix/2.25/protocols/derivation-aterm) 格式的派生:
``
❯ nix-instantiate - << 'EOF'
derivation {
name = "simple";
builder = "/bin/sh";
system = builtins.currentSystem;
args = ["-c" ''
echo "Hello World" > $out
''];
}
EOF
/nix/store/w4mcfbibhjgri1nm627gb9whxxd65gmi-simple.drv
``
如果我们查看文件内容,可以看到派生的 ATerm 表示:
``
Derive(
[("out", "/nix/store/r4c710xzfqrqw2wd6cinxwgmh44l4cy2-simple", "", "")],
[],
[],
"x86_64-linux",
"/bin/sh",
["-c", "echo \"Hello World\" > $out\n"],
[
("builder", "/bin/sh"),
("name", "simple"),
("out", "/nix/store/r4c710xzfqrqw2wd6cinxwgmh44l4cy2-simple"),
("system", "x86_64-linux")
]
)
``
这**包含了构建器构建输出所需的所有信息**。此时,它就不再是 Nix 特有的了。对于 Guix 派生也是如此。
派生并不“知道”它们来自 Scheme 还是 Nix。它只是一个配方。关键在于,如果我们将存储路径从 `/gnu/store` 重写为 `/nix/store`,并替换一些内建函数(例如 `builtin:download` 换成 `builtin:fetchurl`),我们就可以让 `nix-daemon` *完全相同地*构建它。💡
更复杂的派生唯一的区别是它们包含依赖关系,而这些依赖关系本身也是派生,构建器会引用它们,从而形成一个派生图,每个派生按照拓扑顺序由构建器构建。
对于任何非平凡的派生,这棵树的叶子就是引导种子:`gcc`、`awk`、`bash`、`tar` 等。Guix 以其从**一个 357 字节的二进制文件作为源码** [参考](https://guix.gnu.org/manual/1.5.0/en/html_node/Full_002dSource-Bootstrap.html) 自我引导而闻名。由于引导种子从未依赖 `/gnu/store` 作为前缀,因此翻译后的链在 Nix 下可以完全相同地构建。
`guix-transfer` 以后序遍历的方式遍历 Guix 的 `.drv` 图,对于每个派生:
1. Guix 的 `builtin:download` 替换为 Nix 的 `builtin:fetchurl`。概念相同,名称不同。
2. 源文件被添加到 Nix 存储中,嵌入的 `/gnu/store` 路径被重写为对应的 `/nix/store` 路径。
3. 每一个 `/gnu/store` 引用:输入 drv、构建器路径、参数、环境变量都被重写为对应的 `/nix/store` 路径。
4. 输出路径被清空,因为 Nix 通过 `hashDerivationModulo` (https://fzakaria.com/2025/10/29/nix-derivation-madness) 重新计算它们。
5. 结果序列化为 JSON,并通过 `nix derivation add` 注册。
就是这样。没有生成任何 Nix 表达式。没有 `stdenv`。没有将 Guix 包映射到 nixpkgs 对应包。Guix 派生图被*忠实地*翻译,然后 `nix-daemon` 构建它。
> **注意** 有趣的是,`builtin:fetchurl` 只接受一个 URL,并且没有备用回退。Guix 派生会携带多个镜像列表,其中很多不稳定或已失效。与 Nix 类似,Guix 在 `bordeaux.guix.gnu.org` 运行一个内容寻址镜像,服务其 CI 曾经见过的*任何*源码。我们在 `fetchurl` 中利用这个镜像,而不是使用原始源 URL。
现在,既然我们有了一种将 Guix 包“吸入”Nix 的方法,我们就可以通过组合*原生* Nix 和 Guix 包来做一些邪恶的组合了!
我们可以用我们之前在 Nix 中构建的 `hello` 包,在 Nix 派生中使用它。
``
let
guixHello = /nix/store/...-hello-2.12.2;
in
derivation {
name = "nix-hello-world";
system = "x86_64-linux";
builder = "/bin/sh";
args = [ "-c" "echo \"$(${guixHello}/bin/hello) from Nix\" > $out" ];
}
``
Nix 会自动扫描你的派生,查找任何以 `/nix/store` 前缀开头的内容,并将其跟踪为输入依赖。这类似于当你执行类似 `${pkgs.hello}/bin/hello` 时存储路径的插值方式。
``
❯ nix-build --no-out-link hello-from-nix.nix
❯ cat /nix/store/...-run-guix-hello
Hello, world! from Nix
``
如果直接在 Nix 表达式中手动输入 `/nix/store` 路径对你来说*太原始*了,我们也可以很容易地构建更符合人体工程学的东西。`guix-transfer` 有一个 `--emit-nix` 模式,改为输出翻译后的 `.drv` 的 Nix 表达式。
让我们看一个稍微复杂一点的例子,使用 Guix 的 `guile` 来构建一个有依赖的派生:
``
❯ guix repl -- /dev/stdin <<'EOF'
(use-modules (guix derivations) (guix store) (guix packages)
(gnu packages bootstrap))
(with-store store
(let* ((guile-drv (package-derivation store %bootstrap-guile))
(guile-out (derivation->output-path guile-drv))
(drv (derivation store "demo" (string-append guile-out "/bin/guile")
`("--no-auto-compile" "-c"
,(string-append
"(call-with-output-file (getenv \"out\") "
" (lambda (p) (display \"Hello from Guix!\\n\" p)))"))
#:inputs (list (derivation-input guile-drv))
#:system "x86_64-linux")))
(format #t "~a\n" (derivation-file-name drv))))
EOF
❯ guix build /gnu/store/fln2d17fyqka3gafcdqyhfyl1nzml5jn-demo.drv
successfully built /gnu/store/fln2d17fyqka3gafcdqyhfyl1nzml5jn-demo.drv
/gnu/store/l66zvywi60ljhk3kwwaay156cgsc2ahg-demo
❯ cat /gnu/store/l66zvywi60ljhk3kwwaay156cgsc2ahg-demo
Hello from Guix!
``
现在我们可以使用 `guix-transfer` 将其转换为 Nix 表达式。
``
❯ guix-transfer /gnu/store/fln2d17fyqka3gafcdqyhfyl1nzml5jn-demo.drv --emit-nix /tmp/demo.nix
...
Realise it with: nix-store --realise --option filter-syscalls false /nix/store/fkj4vz6vs85s2x3dwhg5ysfwyr8rv4a5-demo.drv
Emitted Nix expression: /tmp/demo.nix
``
我们可以用 `nix-store --realise` 来实现这个派生,或者直接 `nix-build` 这个 Nix 表达式。请注意,两者**产生的哈希完全相同**:`/nix/store/rq5bc9crsg1hrr7afllzjgi7z8bl21zy-demo`。
``
❯ nix-build /tmp/demo.nix
/nix/store/rq5bc9crsg1hrr7afllzjgi7z8bl21zy-demo
❯ nix-store --realise --option filter-syscalls false /nix/store/zgwdbfpigl8cwy5d85p0rdcl21x3bszm-demo.drv
/nix/store/rq5bc9crsg1hrr7afllzjgi7z8bl21zy-demo
❯ cat /nix/store/rq5bc9crsg1hrr7afllzjgi7z8bl21zy-demo
Hello from Guix!
``
现在我们可以像使用任何普通的 Nix 表达式一样使用这个 Guix 派生,比如你在 Nixpkgs 中可能遇到的那些。
``
let
guixDemo = import /tmp/demo.nix;
in
derivation {
name = "use-guix-demo";
system = "x86_64-linux";
builder = "/bin/sh";
args = [ "-c" "echo \"Nix says: $(cat ${guixDemo})\" > $out" ];
}
``
这意味着我们甚至可以构建一个包含所有可用 Guix 包的 `flake`。
``
{
outputs = { self }: {
packages.x86_64-linux.demo = import ./demo.nix;
};
}
``
我的脑子炸了。🤯
Nixpkgs (https://github.com/nixos/nixpkgs) 被称为世界上最大的软件包仓库,而现在我们找到了一种方法,通过借用 Guix 中的**任何**派生,让它突然变得更大!
Nix 真正的*力量*在于派生,以及它们声明了所有依赖的封闭性。我们已经看到,我们可以将这些配方迁移到任何具有类似特性的*基于存储*的系统中,并保持可重现性。
相似文章
Nix Flakes 及其在 Guix 中的对应物
详细比较 Nix Flakes 与 Guix 包管理系统中的对应物,涵盖依赖声明、锁定、纯净性、输出、开发环境和系统配置。
GuixPkgs:每个 Guix 包,作为 Nix flake
GuixPkgs 是一个项目,它将每个 GNU Guix 包以 Nix flake 的形式提供,使用户能够在单个 flake 中混合使用 Guix 和 Nixpkgs 包。它使用 guix-transfer 将 Guix 派生转换为 Nix 派生,并提供二进制缓存以避免完全重建 Guix 引导过程。
不到100行代码实现nix-build
本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。
Nixmac
Nixmac 是一个为 Nix-darwin 提供简洁英文界面的工具,简化了 macOS 上的 Nix 配置。
后现代构建系统
一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。