你好!出于某种原因,在上一篇nix 帖子之后,我试图了解 Nix 构建的幕后工作原理,结果被书呆子嘲笑了,所以这是我今天所做的快速探索。
我从抱怨 Mastodon开始:
是否有任何自下而上的 nix 指南(例如从这个 bash 脚本开始,然后处理抽象层)而不是自上而下?
我看过的所有指南都是从描述 nix 编程语言或其他抽象概念开始的,我希望看到一个指南以我已经理解的概念开始,比如编译器标志、链接器标志、Makefile、环境变量和 bash 脚本
Ross Light 写了一篇很棒的博文作为回应,名为Connecting Bash to Nix ,展示了如何在不使用 Nix 的大部分标准机制的情况下编译基本的 C 程序。
我想更进一步,编译一个稍微复杂一点的 C 程序。
目标:编译一个 C 程序,而不使用 Nix 的标准机器
我们的目标是编译一个名为paperjam
的 C 程序。这是一个真正的 C 程序,它不在 Nix 存储库中。我已经在这篇文章中通过复制和粘贴一堆我不理解的东西来弄清楚如何编译它,但这次我想以一种更原则的方式来做,让我真正理解更多的步骤。
我们将避免使用 Nix 的大部分帮助程序来编译 C 程序。
计划是从一个几乎是空的构建脚本开始,然后解决错误,直到我们有一个工作构建。
第一:什么是推导?
我说过我们不会谈论太多的 Nix 抽象(我们不会!),但理解什么是推导确实对我有帮助。
我读到的关于 Nix 的所有内容都一直在谈论推导,但我真的很难弄清楚什么是推导。事实证明, derivation
是 Nix 语言中的一个函数。但不仅仅是任何功能! Nix 语言的全部要点似乎就是调用此函数。 derivation
函数的官方文档其实写的非常清楚。这是我拿走的:
derivation
将一堆键和值作为输入。有 3 个必需的键:
-
system
: 系统,例如x86_64-darwin
-
name
:您正在构建的包的名称 builder
:运行构建的程序(通常是 bash 脚本)
每个其他键都是一个任意字符串,它作为环境变量传递给builder
shell 脚本。
推导自动构建所有输入
推导不只是调用 shell 脚本!假设我在我的脚本中引用了另一个名为pkgs.qpdf
的推导。
尼克斯将:
- 自动构建
qpdf
包 - 将生成的输出目录放在
/nix/store/4garxzr1rpdfahf374i9p9fbxnx56519-qpdf-11.1.0
之类的地方 - 将
pkgs.qpdf
扩展到该输出目录(作为字符串),以便我可以在我的构建脚本中引用它
派生函数还做了一些其他事情(在文档中有描述),但“它构建所有输入”是我们现在真正需要知道的。
第一步:编写推导文件
让我们编写一个非常简单的构建脚本并调用derivation
函数。这些还没有用,但我发现检查所有错误,一次修复一个错误,并通过修复它们更多地了解 Nix 的工作原理非常有趣。
这是构建脚本 ( build_paperjam.sh
)。这只是解压缩 tarball 并运行make install
。
#!/bin/bash tar -xf "$SOURCE" cd paperjam-1.2 make install
这是调用derivation
函数的 Nix 代码(在paperjam.nix
中)。这就调用了核心的derivation
函数,没有太多魔法。
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/4d2b37a84fad1091b9de401eb450aae66f1a741e.tar.gz") {}; builtins.derivation { name = "paperjam-fake"; builder = ./build-paperjam.sh; system = builtins.currentSystem; SOURCE = pkgs.fetchurl { url = "https://mj.ucw.cz/download/linux/paperjam-1.2.tar.gz"; hash = "sha256-0AziT7ROICTEPKaA4Ub1B8NtIfLmxRXriW7coRxDpQ0"; }; }
这里的主要内容是:
-
fetchurl
(下载 url 并将路径放入SOURCE
环境变量) -
pkgs
(它让我们依赖于中央存储库中的其他 Nix 包)。我不完全理解这一点,但我已经陷入了一个非常深的兔子洞,所以我们暂时不谈了。
SOURCE
评估为一个字符串——它是下载的源压缩包的路径。
问题 1: tar: command not found
Nix 需要您声明构建的所有依赖项。它通过删除您的PATH
环境变量来强制执行此操作,以便您的 PATH 中根本没有二进制文件。
这很容易修复:我们只需要编辑我们的PATH
。
我将其添加到paperjam.nix
以获取tar
、 gzip
和make
:
PATH = "${pkgs.gzip}/bin:${pkgs.gnutar}/bin:${pkgs.gnumake}/bin";
问题 2:我们需要一个编译器
接下来,我们遇到了这个错误:
g++ -O2 -Wall -Wextra -Wno-parentheses -std=gnu++11 -g -DVERSION='"1.2"' -DYEAR='"2022"' -DBUILD_DATE='""' -DBUILD_COMMIT='""' -c -o paperjam.o paperjam.cc make: g++: No such file or directory
所以我们需要在我们的 PATH 中放置一个编译器。出于某种原因,我想使用clang++
进行编译,而不是g++
。为此,我需要对paperjam.nix
进行 2 处更改:
- 添加行
CXX="clang++";
- 将
${pkgs.clang}/bin
添加到我的PATH
问题3:缺少头文件
下一个错误是:
> ./pdf-tools.h:13:10: fatal error: 'qpdf/QPDF.hh' file not found > #include <qpdf/QPDF.hh>
有道理:一切都是孤立的,所以它不能访问我的系统头文件。不过,弄清楚如何处理这个问题有点令人困惑。
事实证明,Nix 处理头文件的方式是它有一个围绕clang
shell 脚本包装器。所以当你运行clang++
时,你实际上是在运行一个 shell 脚本。
在我的系统上, clang++
包装器脚本位于/nix/store/d929v59l9a3iakvjccqpfqckqa0vflyc-clang-wrapper-11.1.0/bin/clang++
。我在该文件中搜索了LDFLAGS
,发现它使用了 2 个环境变量:
-
NIX_LDFLAGS_aarch64_apple_darwin
-
NIX_CFLAGS_COMPILE_aarch64_apple_darwin
所以我认为我需要将 clang 的所有参数放在NIX_CLAGS
变量中,并将所有链接器参数放在NIX_LDFLAGS
中。伟大的!让我们这样做吧。
我将这两行添加到我的paperjam.nix
中,以链接libpaper
和qpdf
库:
NIX_LDFLAGS_aarch64_apple_darwin = "-L ${pkgs.qpdf}/lib -L ${pkgs.libpaper}/lib"; NIX_CFLAGS_COMPILE_aarch64_apple_darwin = "-isystem ${pkgs.qpdf}/include -isystem ${pkgs.libpaper}/include";
那奏效了!
问题 4:缺少c++abi
下一个错误是:
> ld: library not found for -lc++abi
不确定这意味着什么,但我在 Nix 包中搜索了“abi”,并通过将-L ${pkgs.libcxxabi}/lib
添加到我的NIX_LDFLAGS
环境变量来修复它。
问题 5:缺少 iconv
这是下一个错误:
> Undefined symbols for architecture arm64: > "_iconv", referenced from: ...
我首先将-L ${pkgs.libiconv}/lib
添加到我的NIX_LDFLAGS
环境变量中,但这并没有解决它。然后我花了一段时间兜圈子,很困惑。
我最终想出了解决这个问题的方法,方法是采用我之前制作的 paperjam 版本的工作版本并编辑我的clang++
包装器文件以打印出它的所有环境变量。工作版本中的 LDFLAGS 环境变量与我的不同:它包含-liconv
。
所以我也将-liconv
添加到NIX_LDFLAGS
并修复了它。
为什么原始 Makefile 没有-liconv
?
不过,我对这个-liconv
有点困惑: libqpdf
和libpaper
中的原始 Makefile 通过传递-lqpdf -lpaper
链接。那么,如果它需要 iconv 库,为什么它不链接到 iconv 中呢?
我认为这样做的原因是原始 Makefile 假定您在 Linux 上运行并使用 glibc,而 glibc 默认包含这些 iconv 函数。但我猜 Mac OS libc 不包含 iconv,所以我们需要显式设置链接器标志-liconv
以添加 iconv 库。
问题 6:缺少codesign_allocate
下一个错误的时间:
libc++abi: terminating with uncaught exception of type std::runtime_error: Failed to spawn codesign_allocate: No such file or directory
我想这是某种 Mac 代码签名的东西。我使用find /nix/store -name codesign_allocate
在我的系统上查找codesign_allocate
。它位于/nix/store/a17dwfwqj5ry734zfv3k1f5n37s4wxns-cctools-binutils-darwin-973.0.1/bin/codesign_allocate
。
但这并没有告诉我们这个包叫什么——我们需要能够将它称为${pkgs.XXXXXXX}
而${pkgs.cctools-binutils-darwin}
不起作用。
我想不出从 Nix 文件夹到包名称的方法,但我最终四处寻找并发现它被称为pkgs.darwin.cctools
。
所以我将${pkgs.darwin.cctools}/bin
添加到PATH
。
问题 7:缺少a2x
很简单,只需将${pkgs.asciidoc}/bin
添加到PATH
即可。
问题 8:缺少install
make: install: No such file or directory
显然install
是一个程序?这原来是在coreutils
中,因此我们将${pkgs.coreutils}/bin
添加到PATH
中。添加coreutils
还修复了我看到的关于缺少date
等命令的其他一些警告。
问题 9:无法创建 /usr/local/bin/paperjam
这花了我一点时间才弄清楚,因为我对 make 不是很熟悉。 Makefile 的PREFIX
为/usr/local
,但我们希望它成为/nix/store/
中程序的输出目录
我编辑了build-paperjam.sh
shell 脚本说:
make install PREFIX="$out"
一切正常!万岁!
我们的最终配置
这是最后的paperjam.nix
。它与我们开始的没什么不同——我们只是添加了 4 个环境变量。
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/ae8bdd2de4c23b239b5a771501641d2ef5e027d0.tar.gz") {}; in builtins.derivation { name = "paperjam-fake"; builder = ./build-paperjam.sh; system = builtins.currentSystem; SOURCE = pkgs.fetchurl { url = "https://mj.ucw.cz/download/linux/paperjam-1.2.tar.gz"; hash = "sha256-0AziT7ROICTEPKaA4Ub1B8NtIfLmxRXriW7coRxDpQ0"; }; CXX="clang++"; PATH = "${pkgs.gzip}/bin:${pkgs.gnutar}/bin:${pkgs.gnumake}/bin:${pkgs.clang}/bin:${pkgs.darwin.cctools}/bin:${pkgs.asciidoc}/bin:${pkgs.coreutils}/bin:${pkgs.bash}/bin"; NIX_LDFLAGS_aarch64_apple_darwin = "-L ${pkgs.qpdf}/lib -L ${pkgs.libpaper}/lib -L ${pkgs.libcxxabi}/lib -liconv -L ${pkgs.libiconv}/lib "; NIX_CFLAGS_COMPILE_aarch64_apple_darwin = "-isystem ${pkgs.qpdf}/include -isystem ${pkgs.libpaper}/include"; }
和最终的build-paperjam.sh
构建脚本。在这里,我们只需要编辑make install
行来设置PREFIX
。
#!/bin/bash tar -xf "$SOURCE" cd paperjam-1.2 make install PREFIX="$out"
来看看我们的编译推导吧!
现在我们对这个配置有了一些了解,让我们再谈谈nix-build
做了什么。
在幕后, nix-build paperjam.nix
实际上运行nix-instantiate
和nix-store --realize
:
$ nix-instantiate paperjam.nix /nix/store/xp8kibpll55s0bm40wlpip51y7wnpfs0-paperjam-fake.drv $ nix-store --realize /nix/store/xp8kibpll55s0bm40wlpip51y7wnpfs0-paperjam-fake.drv
我认为这意味着paperjam.nix
被编译为某种中间表示(也称为派生?),然后 Nix 运行时接管并负责实际运行构建脚本。
我们可以用nix show-derivation
查看这个.drv
中间表示
{ "/nix/store/xp8kibpll55s0bm40wlpip51y7wnpfs0-paperjam-fake.drv": { "outputs": { "out": { "path": "/nix/store/bcnyqizvcysqc1vy382wfx015mmwn3bd-paperjam-fake" } }, "inputSrcs": [ "/nix/store/pbjj91f0qr8g14k58m744wdl9yvr2f5k-build-paperjam.sh" ], "inputDrvs": { "/nix/store/38sikqcggyishxbgi2xnyrdsnq928gqx-asciidoc-10.2.0.drv": [ "out" ], "/nix/store/3llc749f9pn0amlb9vgwsi22hin7kmz4-libcxxabi-11.1.0.drv": [ "out" ], "/nix/store/a8ny8lrbpyn15wdxk3v89f4bdr08a38a-libpaper-1.1.28.drv": [ "out" ], "/nix/store/d888pj9lll12s5qx11v850g1vd4h3vxq-cctools-port-973.0.1.drv": [ "out" ], "/nix/store/gkpdv7xl39x9yxch0wjarq19mmv7j1pm-bash-5.2-p15.drv": [ "out" ], "/nix/store/hwx16m7hmkp2rcik8h67nnyjp52zj849-gnutar-1.34.drv": [ "out" ], "/nix/store/kqqwffajj24fmagxqps3bjcbrglbdryg-gzip-1.12.drv": [ "out" ], "/nix/store/lnrxa45bza18dk8qgqjayqb65ilfvq2n-qpdf-11.2.0.drv": [ "out" ], "/nix/store/rx7a5401h44dqsasl5g80fl25jqqih8r-gnumake-4.4.drv": [ "out" ], "/nix/store/sx8blaza5822y51abdp3353xkdcbkpkb-coreutils-9.1.drv": [ "out" ], "/nix/store/v3b7r7a8ipbyg9wifcqisf5vpy0c66cs-clang-wrapper-11.1.0.drv": [ "out" ], "/nix/store/wglagz34w1jnhr4xrfdk0g2jghbk104z-paperjam-1.2.tar.gz.drv": [ "out" ], "/nix/store/y9mb7lgqiy38fbi53m5564bx8pl1arkj-libiconv-50.drv": [ "out" ] }, "system": "aarch64-darwin", "builder": "/nix/store/pbjj91f0qr8g14k58m744wdl9yvr2f5k-build-paperjam.sh", "args": [], "env": { "CXX": "clang++", "NIX_CFLAGS_COMPILE_aarch64_apple_darwin": "-isystem /nix/store/h25d99pd3zln95viaybdfynfq82r2dqy-qpdf-11.2.0/include -isystem /nix/store/agxp1hx267qk1x79dl4jk1l5cg79izv1-libpaper-1.1.28/include", "NIX_LDFLAGS_aarch64_apple_darwin": "-L /nix/store/h25d99pd3zln95viaybdfynfq82r2dqy-qpdf-11.2.0/lib -L /nix/store/agxp1hx267qk1x79dl4jk1l5cg79izv1-libpaper-1.1.28/lib -L /nix/store/awkb9g93ci2qy8yg5jl0zxw46f3xnvgv-libcxxabi-11.1.0/lib -liconv -L /nix/store/nmphpbjn8hhq7brwi9bw41m7l05i636h-libiconv-50/lib ", "PATH": "/nix/store/90cqrp3nxbcihkx4vswj5wh85x5klaga-gzip-1.12/bin:/nix/store/siv9312sgiqwsjrdvj8lx0mr3dsj3nf5-gnutar-1.34/bin:/nix/store/yy3fdgrshcblwx0cfp76nmmi24szw89q-gnumake-4.4/bin:/nix/store/cqag9fv2gia03nzcsaygan8fw1ggdf4g-clang-wrapper-11.1.0/bin:/nix/store/f16id36r9xxi50mgra55p7cf7ra0x96k-cctools-port-973.0.1/bin:/nix/store/x873pgpwqxkmyn35jvvfj48ccqav7fip-asciidoc-10.2.0/bin:/nix/store/vhivi799z583h2kf1b8lrr72h4h3vfcx-coreutils-9.1/bin:/nix/store/0q1jfjlwr4vig9cz7lnb5il9rg0y1n84-bash-5.2-p15/bin", "SOURCE": "/nix/store/6d2fcw88d9by4fz5xa9gdpbln73dlhdk-paperjam-1.2.tar.gz", "builder": "/nix/store/pbjj91f0qr8g14k58m744wdl9yvr2f5k-build-paperjam.sh", "name": "paperjam-fake", "out": "/nix/store/bcnyqizvcysqc1vy382wfx015mmwn3bd-paperjam-fake", "system": "aarch64-darwin" } } }
这感觉非常容易理解——你可以看到有一堆环境变量、我们的 bash 脚本和我们输入的路径。
我们没有使用的编译助手: stdenv
通常当你用 Nix 构建一个包时,你不会自己做所有这些事情。相反,您使用一个名为stdenv
的助手,它似乎有两个部分:
- 一个名为
stdenv.mkDerivation
的函数,它接受一些参数并生成一堆环境变量(它似乎被记录在这里) - 使用这些环境变量的 1600 行 bash 构建脚本 ( setup.sh )。这就像我们的
build-paperjam.sh
,但更通用。
这两个工具一起使用:
- 为您依赖的每个 C 库自动添加
LDFLAGS
- 自动添加
CLAGS
以便您可以获得头文件 make
- 依赖于 clang 和 coreutils 以及 bash 和其他核心实用程序,这样你就不需要自己添加它们
- 将
system
设置为您当前的系统 - 让您轻松添加自定义 bash 代码以在构建的各个阶段运行
- 也许还以某种方式管理版本?不确定这个。
可能还有很多我还不知道的有用的东西
让我们看看jq
的推导
让我们再看看jq
的另一个编译推导。这很长,但这里有一些有趣的东西。我想看看这个,因为我想看看stdenv.mkDerivation
生成的更典型的推导是什么样的。
$ nix show-derivation /nix/store/q9cw5rp0ibpl6h4i2qaq0vdjn4pyms3p-jq-1.6.drv { "/nix/store/q9cw5rp0ibpl6h4i2qaq0vdjn4pyms3p-jq-1.6.drv": { "outputs": { "bin": { "path": "/nix/store/vabn35a2m2qmfi9cbym4z50bwq94fdzm-jq-1.6-bin" }, "dev": { "path": "/nix/store/akda158i8gr0v0w397lwanxns8yrqldy-jq-1.6-dev" }, "doc": { "path": "/nix/store/6qimafz8q88l90jwrzciwc27zhjwawcl-jq-1.6-doc" }, "lib": { "path": "/nix/store/3wzlsin34l1cs70ljdy69q9296jnvnas-jq-1.6-lib" }, "man": { "path": "/nix/store/dl1xf9w928jai5hvm5s9ds35l0m26m0k-jq-1.6-man" }, "out": { "path": "/nix/store/ivzm5rrr7riwvgy2xcjhss6lz55qylnb-jq-1.6" } }, "inputSrcs": [ "/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh", "/nix/store/jd98q1h1rxz5iqd5xs8k8gw9zw941lj6-fix-tests-when-building-without-regex-supports.patch" ], "inputDrvs": { "/nix/store/0lbzkxz56yhn4gv5z0sskzzdlwzkcff8-autoreconf-hook.drv": [ "out" ], "/nix/store/6wh5w7hkarfcx6fxsdclmlx097xsimmg-jq-1.6.tar.gz.drv": [ "out" ], "/nix/store/87a32xgqw85rxr1fx3c5j86y177hr9sr-oniguruma-6.9.8.drv": [ "dev" ], "/nix/store/gkpdv7xl39x9yxch0wjarq19mmv7j1pm-bash-5.2-p15.drv": [ "out" ], "/nix/store/xn1mjk78ly9wia23yvnsyw35q1mz4jqh-stdenv-darwin.drv": [ "out" ] }, "system": "aarch64-darwin", "builder": "/nix/store/0q1jfjlwr4vig9cz7lnb5il9rg0y1n84-bash-5.2-p15/bin/bash", "args": [ "-e", "/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh" ], "env": { "__darwinAllowLocalNetworking": "", "__impureHostDeps": "/bin/sh /usr/lib/libSystem.B.dylib /usr/lib/system/libunc.dylib /dev/zero /dev/random /dev/urandom /bin/sh", "__propagatedImpureHostDeps": "", "__propagatedSandboxProfile": "", "__sandboxProfile": "", "__structuredAttrs": "", "bin": "/nix/store/vabn35a2m2qmfi9cbym4z50bwq94fdzm-jq-1.6-bin", "buildInputs": "/nix/store/xfnl6xqbvnpacx8hw9d99ca4mly9kp0h-oniguruma-6.9.8-dev", "builder": "/nix/store/0q1jfjlwr4vig9cz7lnb5il9rg0y1n84-bash-5.2-p15/bin/bash", "cmakeFlags": "", "configureFlags": "--bindir=${bin}/bin --sbindir=${bin}/bin --datadir=${doc}/share --mandir=${man}/share/man", "depsBuildBuild": "", "depsBuildBuildPropagated": "", "depsBuildTarget": "", "depsBuildTargetPropagated": "", "depsHostHost": "", "depsHostHostPropagated": "", "depsTargetTarget": "", "depsTargetTargetPropagated": "", "dev": "/nix/store/akda158i8gr0v0w397lwanxns8yrqldy-jq-1.6-dev", "doCheck": "", "doInstallCheck": "1", "doc": "/nix/store/6qimafz8q88l90jwrzciwc27zhjwawcl-jq-1.6-doc", "installCheckTarget": "check", "lib": "/nix/store/3wzlsin34l1cs70ljdy69q9296jnvnas-jq-1.6-lib", "man": "/nix/store/dl1xf9w928jai5hvm5s9ds35l0m26m0k-jq-1.6-man", "mesonFlags": "", "name": "jq-1.6", "nativeBuildInputs": "/nix/store/ni9k35b9llfc3hys8nv5qsipw8pfy1ln-autoreconf-hook", "out": "/nix/store/ivzm5rrr7riwvgy2xcjhss6lz55qylnb-jq-1.6", "outputs": "bin doc man dev lib out", "patches": "/nix/store/jd98q1h1rxz5iqd5xs8k8gw9zw941lj6-fix-tests-when-building-without-regex-supports.patch", "pname": "jq", "postInstallCheck": "$bin/bin/jq --help >/dev/null\n$bin/bin/jq -r '.values[1]' <<< '{\"values\":[\"hello\",\"world\"]}' | grep '^world$' > /dev/null\n", "preBuild": "rm -r ./modules/oniguruma\n", "preConfigure": "echo \"#!/bin/sh\" > scripts/version\necho \"echo 1.6\" >> scripts/version\npatchShebangs scripts/version\n", "propagatedBuildInputs": "", "propagatedNativeBuildInputs": "", "src": "/nix/store/ggjlgjx2fw29lngbnvwaqr6hiz1qhy8g-jq-1.6.tar.gz", "stdenv": "/nix/store/qrz2mnb2gsnzmw2pqax693daxh5hsgap-stdenv-darwin", "strictDeps": "", "system": "aarch64-darwin", "version": "1.6" } } }
我觉得有趣的是这里的一些环境变量实际上是 bash 脚本本身——例如postInstallCheck
环境变量是一个 bash 脚本。这些 bash 脚本环境变量在主 bash 脚本中被eval
编辑(您可以在此处的 setup.sh 中看到发生的情况)
这个特定推导中的postInstallCheck
环境变量是这样开始的:
$bin/bin/jq --help >/dev/null $bin/bin/jq -r '.values[1]' <<< '{"values":["hello","world"]}' | grep '^world$' > /dev/null
我想这是一个确保jq
安装正确的测试。
最后:清理
我所有的编译器实验都使用了大约 3GB 的磁盘空间,但是nix-collect-garbage
清理了所有这些空间。
让我们回顾一下这个过程!
经历了这个之后,我觉得我对 Nix 的理解更好了一些。我仍然没有很大的动力去学习 Nix 语言,但现在我对 Nix 程序实际上在做什么有了一些了解!
我的理解是:
- 首先,
.nix
文件被编译成一个.drv
文件,它主要是一堆输入和输出以及环境变量。这就是 Nix 语言不再相关的地方。 - 然后所有环境变量都传递给构建脚本,该脚本负责进行实际构建
- 在 Nix 标准环境 (
stdenv
) 中,其中一些环境变量本身就是 bash 代码,由大型构建脚本setup.sh
进行eval
就这样!我可能在这里犯了一些错误,但这是一个有趣的兔子洞。
原文: https://jvns.ca/blog/2023/03/03/how-do-nix-builds-work-/