如何在 nix 中打包我的软件或为 nixpkgs 编写我自己的包派生

tob*_*ora 16 packaging nixos nix

如何编写一个简单的推导来打包 nix 程序以及如何创建 PR 将其包含在 nixpkgs 中?

(我写这篇文章是因为我找不到简单的解释)

tob*_*ora 40

注意:这个答案尚未完全完成,但它已经是一个很好的起点。我计划稍后添加更多特定于语言的内容(或者可能为每种语言创建一个问题,也保持这个答案\xe2\x80\xa6“”“合理地”“”简短)。

\n

以下是一些参考:

\n
    \n
  • 手册中的快速入门:我们将在此处详细介绍
  • \n
  • Nix 药丸(尤其是第 6 节):它们很棒,但采用自下而上的方法,首先解释我觉得令人困惑的 nix 的所有内部结构\xe2\x80\xa6 你可能不需要学习所有这些写出你的第一个推导。这里我们将采取自上而下的方法,即给出大多数用户想要直接使用的功能,并解释它之后的工作原理。
  • \n
  • stdenv的文档非常好,但包含很多信息和很少(不完整)的示例
  • \n
  • 我的另一个答案,但特定于构建预编译的二进制文件
  • \n
\n

你的第一个推导

\n

通俗地说,推导是构建程序的秘诀。当你做饭时,你需要一些原料(又名 nix 中的来源和依赖项)以及一些将原料组合成蛋糕的步骤(又名programs\xe2\x80\xa6)。

\n

一个简单的 C 程序

\n

让我们从一个简单的例子开始,这是我能想象到的最简单的 C 程序。您可以将其写入您喜欢的任何文件夹中的文件program.c(对于本例):

\n
#include <stdio.h>\n\nint main() {\n   printf("Hello, World!\\n");\n   return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

推导

\n

然后,我们需要告诉 nix 如何编译这个程序。所以创建一个文件derivation.nix

\n
{ stdenv }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  nativeBuildInputs = [ ];\n  buildInputs = [ ];\n\n  buildPhase = \'\'\n    gcc program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这描述了一个函数,其中输入是依赖项(蛋糕的“成分”;这里仅stdenv提供有用的实用程序),并输出一个推导,感谢stdenv.mkDerivation:非正式地,您可以想象这个过程将输出一个包含所有内容的文件夹编译好的文件。我提供了mkDerivation一些信息:

\n
    \n
  • 来源:这里的来源位于当前文件夹中,但您也可以从网络获取来源,我们稍后会看到
  • \n
  • nativeBuildInputs编译程序所需的依赖项(gcc默认情况下始终包含\xe2\x80\xa6,因此您无需在此处指定任何内容)
  • \n
  • buildInputs运行程序所需的依赖项(您通常会将库放在这里)
  • \n
  • buildPhase构建程序的说明(它是一个 bash 脚本)。在此阶段开始时,您将被放入包含源代码的文件夹中
  • \n
  • installPhase描述如何“安装”程序的说明(见下文)。
  • \n
\n

实际上还有更多阶段(解压缩源、修补、配置\xe2\x80\xa6),但本示例不需要它们。

\n

正在做什么installPhase

\n

安装阶段在这里说明最终的可执行文件/库/资产/\xe2\x80\xa6 应该位于哪里。在典型的 Linux 环境中,二进制文件通常复制在/bin/usr/bin或 中/usr/local/bin,库在/lib或中/lib64,资产在/share\xe2\x80\xa6 中,当所有程序将自己的东西放在同一个地方时,它很快就会变得一团糟。

\n

在 Nix 中,所有程序在类似路径中都有自己的文件夹(该路径的值在 中/nix/store/someUniqueHash-programName-version设置为),然后二进制文件转到,库转到,资产转到\xe2\x80\xa6 ,再现典型的 Linux 文件夹层次结构。因此,如果您不确定应该将文件放在哪里,您肯定需要检查将其放在普通 Linux 发行版中的位置,并在路径前面添加(很少有例外,就像我们使用而不是因为没有更多原因)有一个文件夹)。请注意,许多构建系统(cmake\xe2\x80\xa6)都有一个变量,例如表示程序应安装在哪里。:通常可能是or ,这会将二进制文件安装到等位置。在这种情况下,我们通常可以简单地设置并运行常用的编译命令。当您安装程序时,Nix 将正确创建指向已安装软件文件的链接,例如在 NixOs 中,全局安装的二进制文件链接在$outinstallPhase$out/bin$out/lib$out/share$out/$out/bin$out/usr/local/binlocalPREFIXPREFIX//usr/localPREFIX/binPREFIX=$out/run/current-system/sw/bin

\n
{ stdenv }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  nativeBuildInputs = [ ];\n  buildInputs = [ ];\n\n  buildPhase = \'\'\n    gcc program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

因此,在我们的示例中,在安装阶段我们只需要创建文件夹$out/bin并复制复制阶段获得的二进制文件\xe2\x80\xa6 这正是我们所做的!

\n

我怎样才能尝试呢?

\n

要尝试它,您仍然需要指定从哪里获取依赖项(同样,当您烹饪蛋糕时,您应该首先拜访您最喜欢的农民购买一些鸡蛋)。因此,创建另一个文件default.nix(名称在这里很重要,因为nix将首先查找该文件)包含

\n
{ pkgs ? import <nixpkgs> {} }:\npkgs.callPackage ./derivation.nix {}\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,您基本上告诉 nix 使用通道<nixpkgs>来获取依赖项,callPackage并将正确填充derivation.nix.

\n

然后,只需运行

\n
$ ls /run/current-system/sw/bin -al | grep firefox\nlrwxrwxrwx 1 root root   70 janv.  1  1970 firefox -> /nix/store/152drilm2qhjimzfx8mch0hmqvr27p29-firefox-99.0.1/bin/firefox\n
Run Code Online (Sandbox Code Playgroud)\n

最后,您应该有一个新文件夹result,并且该文件夹链接到$out您的派生文件夹:

\n
{ pkgs ? import <nixpkgs> {} }:\npkgs.callPackage ./derivation.nix {}\n
Run Code Online (Sandbox Code Playgroud)\n

然后您可以使用以下命令执行二进制文件:

\n
$ nix-build\n
Run Code Online (Sandbox Code Playgroud)\n

恭喜,您完成了第一次推导!

\n

下面我们将看到如何打包更复杂的应用程序。但在此之前,让我们看看如何安装该软件包并为 nixpkgs 做出贡献。

\n

我可以将其安装在我的系统上吗?

\n

您当然可以安装这个派生。复制您的文件(除非default.nix不需要)并将/etc/nixos已安装软件包的列表更改为:

\n
environment.systemPackages = with pkgs; [\n  (callPackage ./derivation.nix {})\n]\n
Run Code Online (Sandbox Code Playgroud)\n

干得好!

\n

您还可以使用命令式将其安装在任何系统上

\n
$ ls -al | grep result\nlrwxrwxrwx  1 leo  users  55 sept. 13 20:59 result -> /nix/store/xi0hx472hzykl6xjw0hnmh0zjyp6sc52-program-1.0\n
Run Code Online (Sandbox Code Playgroud)\n

我如何将我的推导提交给 nixpkgs?

\n

nixpkgs 项目中的所有包表达式都位于https://github.com/NixOS/nixpkgs中,您可以在那里添加自己的包!为此,首先分叉(以执行拉取请求)并克隆您的存储库。然后复制进去,其中derivation.nix根据你的程序的应用范围适当选择,就是你的程序的名称。pkgs/CATEGORY/PACKAGE/default.nixCATEGORYPACKAGE

\n

当然,nixpkgs 存储库不包含程序的源代码,因此您应该更改源属性以指向外部源(见下文)。

\n

然后,nixpkgs 中可用的所有程序的列表位于,pkgs/top-level/all-packages.nix因此您应该添加一行:

\n
  myprogram = callPackage ../CATEGORY/PACKAGE { };\n
Run Code Online (Sandbox Code Playgroud)\n

在此文件中(程序按字母顺序排序)。要测试它,请转到存储库的根目录并调用

\n
$ ./result/bin/myprogram\nHello, World!\n
Run Code Online (Sandbox Code Playgroud)\n

它应该像以前一样编译您的程序并创建一个result文件夹来测试它。

\n

完成后,提交您的工作并将其作为拉取请求提交!

\n

如果您不熟悉 git 或想要更多详细信息,您可能会喜欢这个线程https://discourse.nixos.org/t/how-to-find-needed-librarys-for-lined-source-bin-applications/39118 /43?u=托比亚斯博拉

\n

如果来源是在线的怎么办?

\n

大多数时候,您会尝试下载在线托管的源。没问题,只需更改您的src属性,例如,如果您从 github 下载(请参阅此处的获取器列表):

\n
{ stdenv, lib, fetchFromGitHub }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  # For https://github.com/myuser/myexample\n  src = fetchFromGitHub {\n    owner = "myuser";\n    repo = "myexample";\n    rev = "v${version}"; # If there is a release like v1.0, otherwise put the commit directly \n    sha256 = ""; # <-- dummy hash: after the first compilation this line will give an error and the correct hash. Replace lib.fakeSha256 with "givenhash". Or use nix-prefetch-git. On older nix, this might fail, use sha256 = lib.fakeSha256; instead.\n  };\n\n  buildPhase = \'\'\n    gcc program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

确保sha256使用您自己的哈希更改该行(需要验证下载的文件是否正确)。lib.fakeSha256是一个虚拟哈希,因此第一次编译时会给出错误,指出哈希是错误的,而它是truehash。因此,用这个值替换哈希(还有类似的工具nix-prefetch-git,但我不得不承认我不使用它们)。警告:如果您使用缓存中已有的另一个程序的哈希值,它不会给出任何错误,而是会很好地峰值另一个包的源!

\n

另请注意,nix 会自动尝试对源代码执行正确的操作,特别是它会自动解压下载的压缩文件

\n
  src = fetchurl {\n    url = "http://example.org/libfoo-source-${version}.tar.bz2";\n    sha256 = "0x2g1jqygyr5wiwg4ma1nd7w4ydpy82z9gkcv8vh2v8dn3y58v5m";\n  };\n
Run Code Online (Sandbox Code Playgroud)\n

如何使用库

\n

现在,让我们使用一个库(本示例中的 ncurses)使程序稍微复杂一些。我们将使用ncurseshello-world 程序:

\n
environment.systemPackages = with pkgs; [\n  (callPackage ./derivation.nix {})\n]\n
Run Code Online (Sandbox Code Playgroud)\n

如果你像上面那样直接编译这个程序,你会得到一个错误

\n
program.c:1:10: fatal error: ncurses.h: No such file or directory\n
Run Code Online (Sandbox Code Playgroud)\n

这是预期的,因为我们没有添加 ncurses 作为依赖项。为此,请在(空格分隔)列表中添加buildInputsncurses(您还必须将其添加到输入依赖项的第一行中):这样做将确保buildInputs编译器搜索的程序的二进制文件可用子目录\xe2\x80\xa6中的头文件include同时更新编译命令-lncurses

\n
{ stdenv, ncurses }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  buildInputs = [\n    ncurses\n  ];\n\n  buildPhase = \'\'\n    gcc -lncurses program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

像以前一样编译并运行程序,就是这样!

\n

调试程序,第 1 部分:nix-shell

\n

nix-build有时使用as来调试程序可能会很烦人nix-build,因为它不会缓存编译:每次失败时,它都会在下一次从头开始编译(这是确保可重复性所必需的)。然而在实践中这可能有点烦人\xe2\x80\xa6 nix-shell的创建(也)是为了解决这个问题。如果您运行gcc命令来编译上述文件,它将直接失败,因为 gcc 和 ncurses 库未全局安装(并且它是一个功能,例如它允许多个项目使用同一库的不同版本)。要创建安装此程序的 shell,只需运行nix-shell,它会自动检查程序的依赖项:

\n
$ nix-env -i -f default.nix\n
Run Code Online (Sandbox Code Playgroud)\n

稍后我们将看到 的更高级用法nix-shell

\n

使用默认阶段和挂钩节省时间

\n

编译程序的方法通常是相同的,因为许多程序都是简单地使用以下方式编译的:

\n
  myprogram = callPackage ../CATEGORY/PACKAGE { };\n
Run Code Online (Sandbox Code Playgroud)\n

因此,nix 默认情况下会尝试上述命令(当它尝试修补时,还会尝试更多命令,test\xe2\x80\xa6),这就是为什么 nixpkgs 中的许多程序实际上并不需要编写任何阶段。

\n

大多数阶段都是真正可配置的:例如,您可以启用/禁用阶段的某些部分,提供一些参数,例如makeFlags = [ "PREFIX=$(out)" ];向 makefile\xe2\x80\xa6 添加标志。这些阶段的完整文档在手册中提供,更多具体在本小节中。如果您确实想检查正在运行的内容,可以检查genericBuild文件pkgs/stdenv/generic/setup.sh中的函数,该函数调用文件中上面写入的默认阶段,除非它们被派生覆盖。您还可以直接从 中读取使用的代码,nix-shell我们稍后会看到。

\n

请注意,这些默认阶段也可以被依赖项覆盖。例如,如果您的程序使用cmake,添加nativeBuildInputs = [ cmake ];将自动调整配置阶段以使用 cmake (也可以按照此处记录的方式进行配置)。scons、ninja、meson\xe2\x80\xa6 也会发生类似的行为。更一般地,nix 定义了许多“钩子”,这些“钩子”将在给定阶段之前或之后运行,以便修改构建过程。只要将它们包含在内nativeBuildInputs就足以触发它们。大多数钩子都记录在此处,其中包括:

\n
    \n
  • autoPatchelfHook自动修补(通常是专有的)二进制文件,使它们可以在 nix 中使用(另请参阅我的其他答案
  • \n
\n

例如,我们可以在 (ncurse) 程序中使用 CMake,如下所示:创建一个CMakeLists.txt包含常用 cmake 规则的文件来编译程序:

\n
cmake_minimum_required(VERSION 3.10)\n\n# set the project name\nproject(myprogram)\n\n# Configure curses as a dependency\nfind_package(Curses REQUIRED)\ninclude_directories(${CURSES_INCLUDE_DIR})\n\n# add the executable\nadd_executable(myprogram program.c)\n\n# Link the curses library\ntarget_link_libraries(myprogram ${CURSES_LIBRARIES})\n\n# Explains how to install the program\ninstall(TARGETS myprogram DESTINATION bin)\n
Run Code Online (Sandbox Code Playgroud)\n

现在可以大大简化我们的derivation.nix

\n
{ stdenv, ncurses, cmake }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  buildInputs = [\n    ncurses\n    cmake\n  ];\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

使用 nix-shell 和 nix 内部调试程序:第 2 部分

\n

注意:本节不是理解其余部分所必需的,您可以安全地跳过它。

\n

我们在上面看到了如何nix-shell将我们放入具有所有必需依赖项的 shell 中,以通过利用缓存来节省编译时间。在这个 shell 中,当然可以像以前一样运行常用命令来编译程序,但有时最好运行与 nix 构建器运行的命令完全相同的命令。

\n

这也是了解更多有关 nix 内部结构的机会(我们还参考Nix 药丸以获取更多详细信息和wiki)。当您编写派生时,nix 将从中派生出一个.drv文件,该文件以简单的 json 格式解释如何构建包。

\n

要查看该文件,您可以运行:

\n
$ nix-build -A myprogram\n
Run Code Online (Sandbox Code Playgroud)\n

确切的输出并不重要,但请注意有几个重要部分:首先,派生指定输出文件夹、源和依赖项、构建期间可用的一些环境变量以及 nix-shell 自动填充的我们:看到了吗"out": \xe2\x80\xa6?由于以下原因,您已经正确配置了它nix-shell

\n
{ stdenv, lib, fetchFromGitHub }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  # For https://github.com/myuser/myexample\n  src = fetchFromGitHub {\n    owner = "myuser";\n    repo = "myexample";\n    rev = "v${version}"; # If there is a release like v1.0, otherwise put the commit directly \n    sha256 = ""; # <-- dummy hash: after the first compilation this line will give an error and the correct hash. Replace lib.fakeSha256 with "givenhash". Or use nix-prefetch-git. On older nix, this might fail, use sha256 = lib.fakeSha256; instead.\n  };\n\n  buildPhase = \'\'\n    gcc program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

更重要的是这些行:

\n
  src = fetchurl {\n    url = "http://example.org/libfoo-source-${version}.tar.bz2";\n    sha256 = "0x2g1jqygyr5wiwg4ma1nd7w4ydpy82z9gkcv8vh2v8dn3y58v5m";\n  };\n
Run Code Online (Sandbox Code Playgroud)\n

这意味着要生成输出,nix 将简单地使用/nix/store/\xe2\x80\xa6/bin/bash参数运行构建器(这里只是 bash 解释器)-e /nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh

\n

这个文件非常简单:

\n
#include <ncurses.h>\n\nint main(int argc, char ** argv)\n{\n    initscr(); // init screen and sets up screen\n    printw("Hello World"); // print to screen\n    refresh(); // refreshes the screen\n    getch(); // pause the screen output\n    endwin(); // deallocates memory and ends ncurses\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果你输入

\n
program.c:1:10: fatal error: ncurses.h: No such file or directory\n
Run Code Online (Sandbox Code Playgroud)\n

你会发现它完全等于您将意识到它与配置默认阶段的pkgs/stdenv/generic/setup.sh

\n

因此,在 中nix-shell,您可以使用类似的东西一次运行所有阶段(创建不同的$out文件夹允许您不以只读方式写入/nix/store):

\n
{ stdenv, ncurses }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  buildInputs = [\n    ncurses\n  ];\n\n  buildPhase = \'\'\n    gcc -lncurses program.c -o myprogram\n  \'\';\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myprogram $out/bin\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您还可以通过将最后一行替换为以下内容来指定要运行的几个阶段:

\n
$ nix-shell\n$ gcc -lncurses program.c -o myprogram\n$ ./myprogram\n
Run Code Online (Sandbox Code Playgroud)\n

要获取阶段列表,您可以执行以下操作:

\n
$ ./configure --prefix=$out\n$ make\n$ make install\n
Run Code Online (Sandbox Code Playgroud)\n

如果它为空,则默认值例如通过

\n
$ typeset -f genericBuild | grep \'phases=\'\nphases="${prePhases:-} unpackPhase patchPhase ${preConfigurePhases:-}             configurePhase ${preBuildPhases:-} buildPhase checkPhase             ${preInstallPhases:-} installPhase ${preFixupPhases:-} fixupPhase installCheckPhase             ${preDistPhases:-} distPhase ${postPhases:-}"\n
Run Code Online (Sandbox Code Playgroud)\n

封装其他语言

\n

上面提供的说明当然适用于许多语言和情况,但某些语言提供了一些其他工具来处理环境变量和依赖项方面的自身要求(例如,我们不能真正使用pip来安装 python 依赖项)。很难在此页面上列出所有现有语言,因此这里有一些需要遵循的通用建议:

\n
    \n
  • nixpkgs手册包含每种语言的一个部分,因此它通常是一个很好的起点
  • \n
  • 尼克斯维基有时包含给定语言的附加信息。检查是否有帮助
  • \n
  • nixpkgs存储库包含数千个程序\xe2\x80\xa6,肯定有人在像您要打包的程序这样的程序之前打包过。获取灵感,使用在线搜索(有时似乎会错过一些条目)或rg(更好的 grep)在本地副本中搜索,以使用您想要使用的工具查找派生。
  • \n
\n

不过,为了简单起见,我将在下面列出一些您可能经常遇到的情况。

\n

如何打包(通常是专有的)二进制文件

\n

我已经在这里做了相当广泛的回答。您当然对解决方案 4 (autoPatchElf) 或 5-6 (buildFHSUserEnv)\xe2\x80\xa6 感兴趣 基本上将您的二进制文件复制到$out/bin,如果您幸运的话添加autoPatchelfHook应该nativeBuildInputs足够了(如果程序有资产,您也可以复制它并$out/opt放入$out/bin一些调用 ) 中的程序的链接或脚本$out/opt

\n

如何打包shell脚本

\n

让我们考虑一下该文件myshellscript.sh

\n
#!/usr/bin/bash\n\necho "Hello, world"\n
Run Code Online (Sandbox Code Playgroud)\n

只需使用

\n
{ stdenv }:\nstdenv.mkDerivation rec {\n  name = "program-${version}";\n  version = "1.0";\n\n  src = ./.;\n\n  installPhase = \'\'\n    mkdir -p $out/bin\n    cp myshellscript.sh $out/bin\n    chmod +x $out/bin/myshellscript.sh # not needed if the file is already executable\n  \'\';\n}\n
Run Code Online (Sandbox Code Playgroud)\n

并且 bash 脚本将通过修复阶段默认存在的patchShebangsAuto钩子自动进行修补。

\n

进一步阅读以了解如何使用平凡的构建器使这个推导变得更小!

\n

包装器,或如何添加可执行文件

\n

假设我们的包需要一些可执行文件才能工作,例如cowsay. 因为 nix 试图保持包之间的“隐秘性”(又名纯度),以限制冲突,正如这里所解释的那样(也许不同的程序需要不同版本的cowsay),所以您不能假设