Nix 需要可重定位的二进制文件

Lobsters Hottest 工具

摘要

文章指出了一个问题是 Nix 二进制文件不可重定位,当存储前缀改变时会导致哈希变化和重新编译,并提出使用带有 $ORIGIN 的相对路径在 RUNPATH 中实现可重定位,而不会使缓存失效。

<p><a href="https://lobste.rs/s/pa1atu/nix_needs_relocatable_binaries">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/22 13:34

# Nix 需要可重定位的二进制文件 来源:https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries > 这是我在 aTacoSprint 2026 (https://tacosprint.org/) 项目中提出的问题陈述和提案 🏄。 Nix,或者更广义的**基于存储的系统**,是一类使用明确定义的前缀来存储所有包的包管理器。对于 Nix 来说是 `/nix/store`,对于 Guix 来说是 `/gnu/store`。 这很简单。它让**重写**指向二进制文件或库的路径变得容易。派生只需要使用 `sed` 替换带有完整存储路径的字符串;例如 `/bin/bash` 变成了 `/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash`。 如果你想要一个不同的路径,一个不以根目录 `/` 为前缀的路径呢? 如果你还没有安装 Nix,或者缺少必要的权限——比如“无根 Nix”——这可能会很理想。 嗯,Nix 目前确实允许你指定不同的存储路径,**但有一个问题!** 让我们看一个简单的例子。我们可以用两种方式构建 `hello`。 ``` > nix build nixpkgs#hello > nix build --store /tmp/fzakaria/store nixpkgs#hello ``` 第一个命令构建并安装 `hello` 到 `/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/`,第二个命令则使用 `chroot` 和挂载命名空间安装到 `/tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/`。 注意两者具有相同的哈希值 `zi2bj2hlavv8q743li2s9diqbcpmrf9b`。 **这一点很重要。** 通过保持哈希值相同,我们可以利用来自二进制替换服务(如 https://cache.nixos.org/)的预计算派生。 那么,缺少什么呢? 如果你正在使用像 Bazel (https://bazel.build/) 或 Buck2 (https://buck2.build/) 这样的工具,它们很可能已经通过命名空间进行自己的沙盒构建。将 Nix 集成到这些生态系统中变得极其不切实际,因为我们遇到了嵌套用户命名空间和挂载限制。 我们可以要求 Nix 使用替代的存储前缀,**无需 chroot 和挂载命名空间**,但存在一个巨大的差距。 ``` > XDG_CACHE_HOME=/tmp/fzakaria/cache \ nix eval --store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log' \ --raw nixpkgs#hello.outPath /tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3 ``` 现在哈希值是 `qv3fhi1j9gh27fyds5n5b16yia8i6zn5` 😭 情况更糟。仅仅修改这个简单字符串就会级联失效整个依赖图。现在你需要等待 4 小时编译 GCC,只为了从不同的文件夹打印“Hello World”。🫠 这意味着我们无法利用公共缓存。这个差距在目前的 Nix 文档 (https://nix.dev/manual/nix/2.24/store/types/local-store) 中已被指出。 它非得这样吗? 如果我们可以将 Nix 二进制文件安装到 **任何地方**,而不需要使用命名空间或 `chroot` 呢?我们能鱼与熊掌兼得吗?🍰 Nix 需要 **可重定位的二进制文件**。 问题在于存储前缀本身是派生的一部分,因此它影响了哈希计算。 我们不必在每个地方都指定完整的存储前缀。如果我们使用相对路径呢?🤔 让我们看看当前二进制文件中完整路径写在一个地方的情况:通过 `RUNPATH`。 ``` > patchelf $(nix build --no-link --print-out-paths nixpkgs#hello)/bin/hello \ --print-rpath /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib ``` 当这个程序运行时,动态链接器会查看 `RUNPATH` 来找到它的共享依赖。 然而,Linux 的加载器原生支持变量 `$ORIGIN`,它表示“包含可执行文件的目录”。[参考 (https://man7.org/linux/man-pages/man8/ld.so.8.html)] 我们可以将 `RUNPATH` 写为 `$ORIGIN/../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib`。 如果这样做,那么更改存储路径就不会导致任何哈希值发生变化。无需重新编译。🥳 好了,那么我们完成了吗? 嗯,像大多数事情一样,魔鬼在于细节。😈 在动态链接器能够读取 `RUNPATH` 来找到必要的库之前,Linux 内核必须加载动态链接器本身。这个路径存储在另一个名为 `PT_INTERP`(程序解释器)的 ELF 头部中。 ``` > patchelf $(nix build --no-link --print-out-paths nixpkgs#hello)/bin/hello \ --print-interpreter /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2 ``` 不幸的是,Linux 内核目前 **尚不支持** 在这个字段中使用 `$ORIGIN`。 我们也在脚本的 shebang 行中遇到了完全相同的内核限制。 ``` #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash echo "Hello!" ``` 当我们执行一个脚本时,内核会解析 `#!`(shebang)并期望一个绝对路径。对 `$ORIGIN` 的支持 **目前也缺失**。 我们不能可靠地在这里使用相对路径,除非它们相对于当前工作目录,而一旦你从其他任何地方运行脚本,这就会失效。 ### 我们如何实现?🗺️ 为了实现真正的可重定位二进制文件,我们需要绕过这些内核限制。`$ORIGIN` 在历史上对于 Linux 内核中的 `PT_INTERP` 来说是没有意义的,因为“为什么你会希望你的动态链接器相对于文件来查找?”。 Nix 改变了这种评估。有几种方法可以解决这个问题: 1. 我们可以修补 Linux 内核,使其在 `PT_INTERP` 和 shebang 中支持 `$ORIGIN`。 2. 我们用一个小型的静态二进制文件包装每一个二进制文件,该文件计算自己的位置,然后调用动态链接器。 3. 我们需要替换文件位置,以便也利用特定于语言的特性来实现相对路径。例如,在 Python 中,我们可以利用 `__file__` 来访问相对于自身的文件,类似于 `$ORIGIN`。 我相信向 Linux 内核增加支持是正确的做法。Nix 的美妙之处在于,我们甚至可以在任何 NixOS 机器上修补当前的内核以获得这种支持。 作为最后的锦上添花,我们可以在每个派生中添加额外的元数据 `relocatable = true;`,指示它是否 **可重定位**。🍒

相似文章

Nix 的 Substituter 列表不是路由表

Lobsters Hottest

一篇博客文章,解释了 Nix 的序列化二进制缓存查找的性能限制,并介绍了 ncro,一个用 Rust 编写的小型代理,它通过并行竞争上游缓存来减少延迟。

使用Nix构建系统软件

Lobsters Hottest

一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。

Nix for Haskell: 静态构建

Lobsters Hottest

本教程介绍如何使用 Nix 为 Haskell 项目创建静态链接的可执行文件,涵盖 GHC 的静态构建配置以及与 Docker 的集成。

不到100行代码实现nix-build

Lobsters Hottest

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