如果您还没有听说过 Nix,那就太好了!如果你已经看过它并且有点困惑......我明白了。 Nix 一开始可能看起来很不寻常,但它对您的软件项目也有巨大的价值。我希望我们可以重新开始这篇文章。 🙂

管理依赖关系和创建可重现的环境是一个非常困难TM 的问题

管理软件开发中的依赖关系可能是一个很大的痛苦,我们为此创建了大量的解决方案:我们有 apt、npm、brew、pip、gem、rpm 等等......我们还使用像 Docker 这样的容器来管理软件包并创建相对可重现的环境。

所有这些解决方案都有其局限性。它们要么特定于某种语言(Python 为pip,Ruby 为gem,Haskell 为stack......),某些操作系统(Debian/Ubuntu 为apt,Fedora & co 为rpm......),我们的某些部分堆栈(后端/前端),或者它们具有额外的复杂性(容器)。它们可能或多或少可靠(npm),它们的结果或多或少是可重复的。很少有解决方案能够明智地管理同一个包的多个版本。

所有依赖管理难题的解决方案

解决方案是明显:我们需要另一个包管理器来管理它们!

Nix非常接近于成为理想的跨操作系统、跨语言、跨堆栈的包管理器,同时非常可靠和可重复。当然,它并不完美,它有自己的权衡和限制。关于 Nix 有很多要解释的内容,如何有一种称为Nix的函数式编程语言、一个称为Nixpkgs的包存储库、一个称为NixOS的整个操作系统......等等。但是对于这篇文章,我想向您展示一个简单的、最小的用例,它可能已经对您非常有价值!

简单示例:使用 Nix 管理开发依赖项

假设要破解我们的一个项目,我们需要curljqentr和[# 7rpmpg_tmp又取决于Postgres二进制文件。这是使用 Nix 获得所有这些的方法:

# Install Nix
curl https://nixos.org/nix/install | sh

# Create a `shell.nix` file
cat > shell.nix << EOF
let
  pkgs = import <nixpkgs> {};
in
pkgs.stdenv.mkDerivation {
  name = "my-env";

  buildInputs =
    [
      pkgs.curl
      pkgs.jq
      pkgs.entr
      pkgs.ephemeralpg
    ];
}
EOF

# Run `nix-shell`
nix-shell

# Have all dependencies that we need ready to go on your $PATH! 🎉

而已!

如上所述安装 Nix 是对系统的相对侵入性更改,例如,您会在根文件系统中找到一个新的“/nix/”目录。我建议先在虚拟机或 Docker 容器中尝试一下。

我想我有必要多解释一下这里发生的事情以及如何使用它。

我们在本例中使用的 Nix 生态系统的一部分

我们正在使用 Nix 生态系统的三个部分。

1/3:Nix 编程语言

这是一种非常小的语言,语法可以在一页上描述。如果您熟悉函数式编程语言(尤其是来自 ML 家族、Haskell、Elm),那么您就对了。如果你不是,它可能看起来很陌生和限制性(没有循环?!)。

Nix 语言中的所有内容都是表达式,也就是说,您编写的所有内容都会计算为一个值。该语言大多是纯粹的:大多数函数调用没有副作用,并且对于相同的输入总是返回相同的结果。剩余的杂质主要由加密哈希处理。基于此,任何 Nix 表达式的结果都是高度可重现的。

2/3:Nix 软件包存储库

Nixpkgs是一个巨大但组织良好的 Nix 表达式,它定义了如何构建超过 40,000 个软件包,包括_所有_它们的依赖项。目前总共超过 17 mb。

Nix 编程语言通过懒惰并仅评估您当前需要的部分来继续高效地处理这个巨大的表达式。

Nixpkgs 是您在使用 Nix 时可能遇到的许多复杂性的来源。mkDerivationcallPackage等抽象非常适合消除重复并保持 Nixpkgs 可维护,但它们的抽象可能很难理解。想太多其中一些会让我头疼。

3/3:nix-shell实用程序

nix-shell二进制文件从文件加载 Nix 表达式(默认情况下从文件shell.nix或,如果该文件不存在,则从default.nix),评估该表达式,然后将我们放入一个 shell 中,其中所有依赖项都在表达式中定义(神奇地)可用。

更多解释和细节

话虽如此,让我们更详细地看一下上面的例子。

shell.nix文件解释

让我们再看一下我们之前创建的shell.nix文件:

let
  pkgs = import <nixpkgs> {};
in
pkgs.stdenv.mkDerivation {
  name = "my-env";

  buildInputs =
    [
      pkgs.curl
      pkgs.jq
      pkgs.entr
      pkgs.ephemeralpg
    ];
}

let [DEFINITIONS] in [EXPRESSION]表达式允许我们在let之后的第一部分中定义局部范围内的变量,然后使用该范围计算in之后的第二部分。因此,在第一部分中定义的变量pkgs可以在第二部分中使用。

第二行的import <nixpkgs> {}是我们如何导入默认情况下在我们的 Nix 安装中可用的 Nixpkgs 表达式。我将在另一篇文章中解释如何将 Nixpkgs“固定”到一个特定的、可重现的版本。

pkgs.stdenv.mkDerivation是一个复杂函数,它在整个 Nixpkgs 中用于定义如何构建不同的包。我们现在需要知道的只是它接受一个集合({...})作为参数并返回一个推导(用 Nix 的话来说就是“可以构建的东西”)。如果我们在nix-shell中运行这个表达式,构建输入将能够在我们的路径上。

对于name = "my-env";,我们将用作参数的集合的name属性设置为mkDerivation。我们放什么并不重要。

buildInputs是最重要的部分:我们将其设置为我们希望在nix-shell中可用的派生列表。列表中的项目由空格分隔。我们只使用pkgs值中的属性。请记住,pkgs是由导入巨大的 Nixpkgs Nix 表达式产生的,因此它的属性中包含所有超过 40,000 个包。我们只挑选合适的。

在 Nixpkgs 上找到正确的属性并不总是那么简单,但是在谷歌上搜索一下就会有很长的路要走。例如,Google 告诉我pg_tmp是 Nix 中ephemeralpg包的一部分。

nix-shell实用程序解释

当我们运行nix-shell时,会发生很多奇妙的魔法。让我们来看看它执行的最重要的步骤:

1.nix-shell会在当前目录中查找shell.nix的文件,在我们的例子中立即找到。

  1. 它评估文件中包含的 Nix 表达式。在大多数情况下,表达式非常庞大,因为正在导入包含超过 40,000 个包的整个 Nixpkgs 表达式。请记住,Nix 仅通过评估我们实际需要的部分来保持这种合理的效率。

  2. 现在 Nix 表达式已经被求值,nix-shell确切地知道需要哪些依赖项。如果已经构建了各个依赖项,它会检查位于/nix/store目录中的本地缓存。如果没有,它将尝试从位于cache.nixos.org的二进制缓存中获取它们。只有作为最后的手段,它才会实际构建所需的包本身。

4、当所有的依赖都成功缓存到/nix/store中后,nix-shell将一个$PATH的环境变量放在一起,将所有请求的依赖绑定在一起。然后它使用该路径集启动一个新的 shell。

如果您只想运行单个命令而不是将其放入 shell,则可以使用--run标志。例如,nix-shell --run "curl http://google.com/"将使用 Nix 安装的 curl 版本查询 Google。

一旦您再次使用Ctrl-D离开 Nix shell,nix-shell对我们的环境所做的任何更改都将消失。看起来我们定义为依赖项的entr包从未安装,除了/nix/store中的缓存二进制文件。后者可以用nix-collect-garbage清理或保留以便下次更快启动nix-shell

更多

恭喜你在 Nix 的第一次试鞋中幸存下来!通过上面的示例,您已经有了一个可用的第一部分,可以很容易地用额外的包进行扩展。在即将发布的文章中,我想向您展示如何固定您的 Nixpkgs 版本,以使您的依赖项可跨时间和不同系统重现!

Logo

CI/CD社区为您提供最前沿的新闻资讯和知识内容

更多推荐