Guix Nix 的怪异融合:在 Nix 中利用 Guix 派生

Lobsters Hottest 工具

摘要

一项技术探索,展示了 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 中的对应物

Lobsters Hottest

详细比较 Nix Flakes 与 Guix 包管理系统中的对应物,涵盖依赖声明、锁定、纯净性、输出、开发环境和系统配置。

GuixPkgs:每个 Guix 包,作为 Nix flake

Lobsters Hottest

GuixPkgs 是一个项目,它将每个 GNU Guix 包以 Nix flake 的形式提供,使用户能够在单个 flake 中混合使用 Guix 和 Nixpkgs 包。它使用 guix-transfer 将 Guix 派生转换为 Nix 派生,并提供二进制缓存以避免完全重建 Guix 引导过程。

不到100行代码实现nix-build

Lobsters Hottest

本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。

Nixmac

Product Hunt

Nixmac 是一个为 Nix-darwin 提供简洁英文界面的工具,简化了 macOS 上的 Nix 配置。

后现代构建系统

Lobsters Hottest

一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。