47
nix 实战经验:安装和打包

nix 的使用确实存在一定的复杂性,甚至需要学习其独特的编程语言,但我始终致力于以简洁的方式掌握它的使用。为了避开复杂的配置文件管理,我一直在寻找更简洁的方法。以下是我目前的学习成果,期待与您分享如何进行操作:

首先,使用 nix 来安装软件包非常简单。只需要在命令行输入 nix-env -i <package-name>,就可以轻松安装你需要的软件包。比如,如果你想安装 node.js,只需输入 nix-env -i nodejs

其次,如果你有一个 C++ 程序,名为 paperjam,并希望为其构建一个自定义的 nix 包,你需要创建一个名为 default.nix 的文件。在这个文件中,你可以描述你的程序及其依赖项。例如,如果你的程序依赖于 Boost C++ 库和 Python 解释器,你的 default.nix 文件可能如下:

{ pkgs ? import <nixpkgs> }: let myProgram = pkgs.stdenv.program { name = "paperjam"; buildInputs = [ pkgs.boost pkgs.python ]; }; in myProgram

最后,如果你想安装特定版本的 hugo,例如五年前的版本,你可以先在 nixpkgs 中找到相应的源,然后使用 nix-env -iA <package-set> 命令来安装。例如,如果你想安装 hugo 的 0.30.0 版本,你可以输入以下命令:nix-env -iA nixpkgs.hugo-0.30.0

虽然我对 nix 的理解仍停留在初级阶段,可能存在表述不准确的地方。甚至我自己也对于我是否真的喜欢上 nix 感到不确定 —— 它的使用确实具有一定的复杂性!但是,它成功帮助我编译了一些以前难以编译的软件,而且通常情况下,它的安装速度要比 homebrew 快。

尽管我对“声明式的包管理”不太感兴趣,但我仍然对nix的几个方面大为赞赏。首先,它提供了二进制包,这些包被托管在https://cache.nixos.org/上,使得下载和安装都变得相当迅速。对于那些没有提供二进制包的软件,nix能够让编译过程变得更为简单。我认为这主要归功于以下两个原因:

首先,nix允许我们在系统中安装同一库或程序的多个版本。例如,我当前的计算机上就同时安装了两个不同版本的node,一个位于/nix/store/4ykq0lpvmskdlhrvz1j3kwslgc6c7pnv-nodejs-16.17.1,另一个位于/nix/store/5y4bd2r99zhdbir95w5pf51bwfg37bwa-nodejs-18.9.1。这使得我们可以根据实际需要轻松切换和使用不同版本的软件。

其次,nix在构建包时使用了隔离的环境,只使用我们明确声明的依赖项的特定版本。这就意味着我们无需担心包可能会依赖于系统中其他我们并未了解的包。再也不用与LD_LIBRARY_PATH战斗了!这种机制使得我们能够更加精确地控制软件包的构建环境,避免了因依赖问题导致的各种错误和冲突。

在本文后面,我将通过两个具体的例子来展示nix如何帮助我在编译软件时遇到了更小困难。

下面是我开始使用 nix 的步骤:

首先,要进行安装,我尝试了两种方法。一种是官方提供的安装程序,另一种是非官方的安装程序,该程序来自 zero-to-nix.com。在 MacOS 上使用标准的多用户安装卸载 nix 的教程 有点复杂,因此我选择了一个卸载教程更为简单的安装方法。

接着,我把 ~/.nix-profile/bin 添加到了我的 PATH,这样我就可以随时在终端中运行 nix 了。

最后,我使用 nix-env -iA nixpkgs.NAME 命令来安装我需要的软件包。这个命令类似于 brew install 或者 apt-get install,但是使用的是 nix-env。

举个例子,如果我想安装 fish,只需要执行以下命令:

nix-env -iA nixpkgs.fish

这会从 https://cache.nixos.org 下载一些二进制文件,非常简单。

有些人使用 nix 来安装他们的 Node、Python 和 Ruby 包,但我并没有这样做,我还是像以前一样使用 npm install 和 pip install

我认为nix包主仓库中的包是由https://github.com/NixOS/nixpkgs/定义的。

你可以在https://search.nixos.org/packages查找包,似乎有两种官方推荐的查找包的方式:

  1. nix-env -qaP NAME,但这个命令运行非常缓慢,并且我并没有得到期望的结果。
  2. nix --extra-experimental-features 'nix-command flakes' search nixpkgs NAME,这个命令倒是管用,但显得有点冗长,并且输出的所有包都以legacyPackages开头。

我发现了一种我更喜欢的从命令行搜索nix包的方式:

  1. 运行nix-env -qa '*' > nix-packages.txt获取Nix仓库中所有包的列表。
  2. 编写一个简洁的nix-search脚本,仅在packages.txt中进行grep操作(cat ~/bin/nix-packages.txt | awk '{print 1}' | rg "1")。

在nix中,所有的东西都是通过符号链接来安装的。nix的一个主要设计是,没有一个单一的bin文件夹来存放所有的包,而是使用了符号链接。有许多层的符号链接。例如,以下是一些符号链接的例子:

在我机器上的~/.nix-profile最终是一个到/nix/var/nix/profiles/per-user/bork/profile-111-link的链接。
~/.nix-profile/bin/fish 是到/nix/store/afkwn6k8p8g97jiqgx9nd26503s35mgi-fish-3.5.1/bin/fish的链接。
当我安装某样东西的时候,它会创建一个新的profile-112-link目录并建立新的链接,并且更新我的~/.nix-profile使其指向那个目录。

这意味着,如果我安装了新版本的fish但我并不满意,我可以很容易地退回先前的版本,只需运行nix-env --rollback,这样就可以让我回到之前的配置文件目录了。

卸载包并不意味着删除它们

如果我像这样卸载 nix 包,实际上并不会释放任何硬盘空间,而仅仅是移除了符号链接:

$ nix-env --uninstall oil

我尚不清楚如何彻底删除包 - 我试着运行了如下的垃圾收集命令,这似乎删除了一些项目:

$ nix-collect-garbage...85 store paths deleted, 74.90 MiB freed

然而,我系统上仍然存在 oil 包,在 /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0

nix-collect-garbage 有一个更具攻击性的版本,它也会删除你配置文件的旧版本(这样你就不能回滚了)。

$ nix-collect-garbage -d --delete-old

尽管如此,上述命令仍无法删除 /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0,我不明白原因。


升级过程

你可以通过以下的方式升级 nix 包:

nix-channel --updatenix-env --upgrade

(这与 apt-get update && apt-get upgrade 类似。)

我还没真正尝试升级任何东西。我推测,如果升级过程中出现任何问题,我可以通过以下方式轻松地回滚(因为在 nix 中,所有事物都是不可变的!):

nix-env --rollback

有人向我推荐了 Ian Henry 的 这篇文章,该文章讨论了 nix-env --upgrade 的一些令人困惑的问题 - 也许它并不总是如我们所料?因此,我会对升级保持警惕。

在历经数月使用现存的nix包之后,我萌生了制作自定义包的想法。这个包是为一个名为paperjam的程序量身定做的,而这个程序目前尚未被打包封装。

实际上,由于我系统上的libiconv版本不正确,我在没有nix的情况下遇到了编译paperjam的难题。我坚信,即使我对如何制作nix包一无所知,使用nix来编译它可能会更加简单。事实证明,我的想法是正确的!

然而,梳理并实现这个目标的过程相当复杂,因此我在此详述了我实现它的方法和步骤。

构建示例包的步骤

在我着手制作 paperjam 自定义包之前,我想先试手构建一个已存在的示例包,以便确保我已经理解了构建包的整个流程。这个任务曾令我头痛不已,但在我在 Discord 提问之后,有人向我阐述了如何从 https://github.com/NixOS/nixpkgs/ 获取一个可执行的包并进行构建。以下是操作步骤:

步骤 1: 从 GitHub 的 nixpkgs 下载任意一个包,以 dash 包为例:

wget https://raw.githubusercontent.com/NixOS/nixpkgs/47993510dcb7713a29591517cb6ce682cc40f0ca/pkgs/shells/dash/default.nix -O dash.nix

步骤 2: 用 with import <nixpkgs> {}; 替换开头的声明({ lib , stdenv , buildPackages , autoreconfHook , pkg-config , fetchurl , fetchpatch , libedit , runCommand , dash }:)。我不清楚为何需要这样做,但事实证明这么做是有效的。

步骤 3: 运行 nix-build dash.nix

这将开始编译该包。

步骤 4: 运行 nix-env -i -f dash.nix

这会将该包安装到我的 ~/.nix-profile 目录下。

就这么简单!一旦我完成了这些步骤,我便感觉自己能够逐步修改 dash 包,进一步创建属于我自己的包了。

制作自定义包的过程

因为 paperjam 依赖于 libpaper,而 libpaper 还没有打包,所以我首先需要构建 libpaper 包。

以下是 libpaper.nix,我基本上是从 nixpkgs 仓库中其他包的源码中复制粘贴得到的。我猜测这里的原理是,nix 对如何编译 C 包有一些默认规则,例如 “运行 make install”,所以 make install 实际上是默认执行的,并且我并不需要明确地去配置它。

with import <nixpkgs> {};stdenv.mkDerivation rec {  pname = "libpaper";  version = "0.1";  src = fetchFromGitHub {    owner = "naota";    repo = "libpaper";    rev = "51ca11ec543f2828672d15e4e77b92619b497ccd";    hash = "sha256-S1pzVQ/ceNsx0vGmzdDWw2TjPVLiRgzR4edFblWsekY=";  };  buildInputs = [ ];  meta = with lib; {    homepage = "https://github.com/naota/libpaper";    description = "libpaper";    platforms = platforms.unix;    license = with licenses; [ bsd3 gpl2 ];  };}

这个脚本基本上告诉 nix 如何从 GitHub 下载源代码。

我通过运行 nix-build libpaper.nix 来构建它。

接下来,我需要编译 paperjam。我制作的 nix 包 的链接在这里。除了告诉它从哪里下载源码外,我需要做的主要事情有:

  • 添加一些额外的构建依赖项(像 asciidoc
  • 在安装过程中设置一些环境变量(installFlags = [ "PREFIX=$(out)" ];),这样它就会被安装在正确的目录,而不是 /usr/local/bin

我首先从散列值为空开始,然后运行 nix-build 以获取一个关于散列值不匹配的错误信息。然后我从错误信息中复制出正确的散列值。

我只是在 nixpkgs 仓库中运行 rg PREFIX 来找出如何设置 installFlags 的 —— 我认为设置 PREFIX 应该是很常见的操作,可能之前已经有人做过了,事实证明我的想法是对的。所以我只是从其他包中复制粘贴了那部分代码。

然后我执行了:

nix-build paperjam.nixnix-env -i -f paperjam.nix

然后所有的东西都开始工作了,我成功地安装了 paperjam

下一个目标:安装一个五年前的 Hugo 版本

当前,我使用的是 2018 年的 Hugo 0.40 版本来构建我的博客。由于我并不需要任何的新功能,因此我并没有感到有升级的必要。对于在 Linux 上操作,这个过程非常简单:Hugo 的发行版本是静态二进制文件,这意味着我可以直接从 发布页面 下载五年前的二进制文件并运行。真的很方便!

但在我的 Mac 电脑上,我遇到了一些复杂的情况。过去五年中,Mac 的硬件已经发生了一些变化,因此我下载的 Mac 版 Hugo 二进制文件并不能运行。同时,我尝试使用 go build 从源代码编译,但由于在过去的五年内 Go 的构建规则也有所改变,因此没有成功。

我曾试图通过在 Linux docker 容器中运行 Hugo 来解决这个问题,但我并不太喜欢这个方法:尽管可以工作,但它运行得有些慢,而且我个人感觉这样做有些多余。毕竟,编译一个 Go 程序不应该那么麻烦!

幸好,Nix 来救援!接下来,我将介绍我是如何使用 nix 来安装旧版本的 Hugo。

使用 nix 安装 Hugo 0.40 版本

我的目标是安装 Hugo 0.40,并将其添加到我的 PATH 中,以 hugo-0.40 作为命名。以下是我实现此目标的步骤。尽管我采取了一种相对特殊的方式进行操作,但是效果不错(可以参考 搜索和安装旧版本的 Nix 包 来找到可能更常规的方法)。

步骤 1: 在 nixpkgs 仓库中搜索找到 Hugo 0.40。

我在此链接中找到了相应的 .nix 文件 https://github.com/NixOS/nixpkgs/blob/17b2ef2/pkgs/applications/misc/hugo/default.nix

步骤 2: 下载该文件并进行构建。

我下载了带有 .nix 扩展名的文件(以及同一目录下的另一个名为 deps.nix 的文件),将文件的首行替换为 with import <nixpkgs> {};,然后使用 nix-build hugo.nix 进行构建。

虽然这个过程几乎无需进行修改就能成功运行,但我仍然做了两处小调整:

  • 把 with stdenv.lib 替换为 with lib
  • 为避免与我已安装的其他版本的 hugo 冲突,我把包名改为了 hugo040

步骤 3: 将 hugo 重命名为 hugo-0.40

我编写了一个简短的后安装脚本,用以重命名 Hugo 二进制文件。

postInstall = ''    mv $out/bin/hugo $out/bin/hugo-0.40  '';

我是通过在 nixpkgs 仓库中运行 rg 'mv ' 命令,然后复制和修改一条看似相关的代码片段来找到如何实施此步骤。

步骤 4: 安装。

我通过运行 nix-env -i -f hugo.nix 命令,将 Hugo 安装到了 ~/.nix-profile/bin 目录中。

所有的步骤都顺利运行了!我把最终的 .nix 文件存放到了我自己的 nixpkgs 仓库 中,这样我以后如果需要,就能再次使用它了。

可重复的构建过程并非神秘,其实它们极其复杂

我觉得值得一提的是,这个 hugo.nix 文件并不是什么魔法——我之所以能在今天轻易地编译 Hugo 0.40,完全归功于许多人长期以来的付出,他们让 Hugo 的这个版本得以以可重复的方式打包。



这条帮助是否解决了您的问题? 已解决 未解决

提交成功!非常感谢您的反馈,我们会继续努力做到更好! 很抱歉未能解决您的疑问。我们已收到您的反馈意见,同时会及时作出反馈处理!