尼克斯是什么?

2020-05-21 05:16:16

欢迎您于2020年5月25日美国东部时间下午1:00参加ShipIt!介绍Shopify如何使用Nix,讨论Shopify如何使用Nix通过Burke Libbey重新构建我们的开发工具。请登记。

在过去的一年多的时间里,Shopify一直在用Nix逐步重建我们的开发工具的一部分。我最初计划写的是我们现在是如何使用NIX的,以及我们将来要用它做什么(剧透:一切?)。然而,我意识到你们中的大多数人不会真正清楚地了解NIX是什么,而且我还没有找到很多入门材料来很快传达一个清晰的印象,所以这篇文章将是一个速成课程,介绍什么是NIX,如何看待它,以及为什么它是一项如此有价值和改变范式的技术。

在这篇文章中,有几个地方我将以微妙的方式向你撒谎,以掩盖所有规则的细微差别和例外。我不打算把这些都说出来。我只是想建立一个大致的理解。在这篇文章的最后,您应该有了思考NIX所需的基本概念脚手架。让我们一头扎进去吧!

您计算机上的所有东西都隐含地依赖于您计算机上的一大堆其他东西。

让我们先把这件事说清楚:尼克斯是一件很难解释的事情。

要真正理解它,您必须了解几个组件,它们的所有解释都多少是相互依赖的;而且,即使在解释完所有这些构建块之后,仍然需要仔细考虑它们如何组成的含义,才能真正发挥NIX的魔力。尽管如此,我们还是会试一试,一次试一个街区。

最容易开始的地方是Nix商店。一旦您安装了Nix,您将得到一个位于/nix/store的目录,其中包含一大堆如下所示的条目:

这个目录/nix/store是一种图形数据库。每个条目(直接位于/nix/store下的每个文件或目录)都是该图形数据库中的一个节点,它们之间的关系构成了边。

唯一允许将目录和文件写入/nix/store的是NIX本身,并且在NIX将节点写入此图形数据库之后,它将永远是完全不变的:NIX保证节点的内容在创建后不会更改。此外,由于我们将在后面讨论的魔术,给定节点的内容在功能上保证与其他图中同名的节点相同,无论它们是在哪里构建的。

那么,他们之间的关系是什么呢?换句话说,什么是边缘?那么,Store path(32个字符长的字母数字BLOB)的第一部分是一个加密散列(我们稍后将讨论这一点)。如果某个其他存储路径中的文件包含文字文本h9bvv0qpiygnqykn4bf7r3xrxmvqpsrd-nix-2.3.3";,,则该文本构成从包含该文本的节点指向该路径引用的节点的图形边缘。NIX存储中的节点在重新创建后是不可变的,它们产生的边在第一次创建时会被扫描并缓存到其他地方。

为了演示这种联系,如果在nix二进制文件上运行otool-L(或Linux上的ldd),您将看到许多引用的库,这些库如下所示:

这是由otool或ldd提取的,但最终来自嵌入到二进制文件中的文本,Nix在确定从该节点定向的边时也会看到这一点。

非常精明的读者可能会怀疑,在创建节点后扫描该节点中的文字路径引用是否是确定依赖项的可靠方法。就目前而言,令人惊讶的是,这在实践中几乎无懈可击。

为了将其付诸实践,我们可以演示这实际上使用了多少图形数据库,它使用的是nix-store--query。/nix/store是NIX中内置的一个工具,它直接与NIX Store交互,并且--query模式有许多标志,用于向作为Store的Graph数据库提出不同的问题。

类似地,我们可以使用--referers请求指向此节点的边,也可以使用--requisites请求从起始节点可到达的节点的完整传递闭包。

传递闭包是NIX中的一个重要概念,但您实际上不必理解图论:从一个节点定向的边在逻辑上是一个依赖关系:如果一个节点包含对另一个节点的引用,则依赖于该节点。因此,传递闭包(--requisites)还递归地包括那些依赖关系,依此类推,以包括给定节点所依赖的全部内容。

例如,Ruby应用程序可能依赖于将Gemfile中指定的所有rubygem捆绑在一起的结果。该包可能取决于安装Gem nokogiri的结果,而Gem nokogiri可能取决于libxml2(可能取决于libc或libSystem)。所有这些都存在于应用程序的传递闭包中(--requisites),但是只有gem包是直接引用(--reference)。

现在关键的一点是:依赖关系的传递闭包总是存在的,甚至在NIX之外也是如此:这些东西总是您的应用程序的依赖关系,但是通常情况下,您的计算机只是被信任在可接受的地方有可接受的库的可接受版本。NIX去掉了这些假设,并使整个图变得清晰。

要真正了解软件依赖的图形化,我们可以通过nix(nix-env-IA nixpkgs.ruby)安装Ruby,然后构建其所有依赖关系的图表:

第二个构建块是派生。在上面,我不经意地提到,只有Nix可以将东西写入Nix Store,但是它怎么知道要写什么呢?派生是关键。

派生是NIX存储中的一个特殊节点,它告诉NIX如何构建一个或多个其他节点。

如果您列出/nix/store,您很可能会看到一大堆项目,但其中一些项目将以.drv结尾:

这是一个派生。它是由Nix编写和读取的一种特殊格式,它为Nix商店中的任何东西提供了构建说明。Nix存储中的几乎所有内容(除了派生)都是通过构建派生放在那里的。那么派生是什么样子的呢?

构建此派生所需的所有内容都按路径明确列出在文件中(例如,您可以在这里看到";bash";)。

Nix Store中派生路径的散列组件实质上是文件内容的散列。

因为在内容中提到了每个直接依赖项,并且路径是内容的散列,这意味着如果依赖项和派生包含的任何其他信息没有改变,散列就不会改变,但是如果使用依赖项的不同版本,散列就会改变。

无论构建指令是什么,它都会运行,并在Nix Store中生成一个新路径(Graph Database中的一个新节点)。

仔细查看新创建的路径中的散列。您将在上面的派生内容中看到相同的散列。该输出路径是预定义的,但不是预生成的。输出路径也是稳定的散列。您基本上可以将其视为派生的散列和输出的名称(在本例中:";out";;默认输出)。

因此,如果派生的依赖项发生更改,则会更改派生的散列。它还会更改该派生的所有输出的散列。这意味着更改依赖项的依赖项会沿着树一路向下冒泡,直接或间接地更改每个派生的散列以及依赖于更改的事物的所有派生的输出。

这个派生有一个名为";out";(默认名称)的输出,如果我们构建它,会生成一些路径。

这是一个简单的玩具派生,没有inputDrvs。这实际上意味着除了构建器之外,没有其他依赖项。通常,您会看到更多类似的内容:

这对心理模型来说并不是很重要,但Nix也可以通过一些有限的方式将静态文件复制到Nix Store中,而这些并不是真正由派生构建的。此字段仅列出此派生所依赖的NIX存储中的任何静态文件。

NIX在多个平台和CPU架构上运行,通常编译器的输出只能在其中一个平台上运行,因此派生需要指明它针对的是哪种架构。

这里实际上有一个重要的点:NIX Store条目可以在机器之间复制,而不用担心,因为它们的所有依赖项都是显式的。在许多情况下,CPU详细信息是一种依赖关系。

此程序使用args和env执行,预计将生成输出。

您可以看到输出名称(";out";)在这里用作变量。基本上,我们正在运行bash-c&34;echo;hello world;>;$out&34;。这应该只是将文本hello world&34;写入派生输出。

在调用构建器之前,这些变量都被设置为一个环境变量,因此您可以看到我们是如何获得上面的$out变量的,并注意到它与上面的输出中给出的路径相同。

在经历了上一节的派生之后,您可能会开始了解显式声明的依赖项如何进入构建,以及Graph结构是如何组合在一起的-但是,是什么阻止构建引用未声明路径上的内容,或者根本不在Nix商店中的内容呢?

NIX做了大量工作,以确保构建只能看到其派生声明的Graph中的节点,并且不能访问存储之外的内容。

派生版本根本不能访问派生未声明的任何内容。这是通过以下几种方式强制执行的:

在大多数情况下,NIX使用补丁版本的编译器和链接器,它们不会尝试查找默认位置(/usr/lib等)。

NIX通常在拒绝访问构建不应该访问的所有内容的实际沙箱中构建派生。

沙箱是为派生版本创建的,该派生版本向文件系统授予对派生中明确提到的路径的读取访问权限(并且仅授予该访问权限)。

这意味着Nix商店里的艺术品基本上不能依赖Nix商店以外的任何东西。

最后,将这一切结合在一起的障碍是:Nix语言。NIX有一种用于构造派生的自定义语言。这里有很多我们可以谈论的话题,但是语言的设计有两个主要方面需要引起注意。NIX语言是:

这是尼克斯密码。您可能会明白这是怎么回事:我们正在创建一个类似哈希表的东西,其中包含键";a";和";b";,而";b";是调用昂贵函数的结果。

在NIX中,此代码几乎不需要任何时间就可以运行,因为直到需要它时,它的值才会实际求值。我们甚至可以:

在这里,我们重新创建表(技术上称为NIX中的属性集),并从中提取";a";。

上面的代码示例中明显缺少要完成的任何类型的实际工作,而不仅仅是在NIX语言中推送数据。原因是NIX语言实际上不能做很多事情。

NIX语言缺少许多您在普通编程语言中期望的功能。它有。

就与世界…的互动而言,它实际上根本没有做任何事情。嗯,除了你调用派生函数的时候。

NIX语言正好有一个有副作用的功能。当您使用正确的参数集调用派生时,作为调用该函数的副作用,Nix会将一个新的<;hash>;-<;name>;.drv文件写出到Nix Store中。

派生{名称=";demo";$builder=";${bash}/bin/bash";;*args=[";-c";";echo';hello world&39;>;$out";];系统=";x86_64-达尔文";;}。

返回的对象只是您传入的对象(带有name、builder、args和系统键),但是带有一些额外的字段(包括drvPath,它是在调用Derivation之后打印的),但重要的是,Nix存储中的路径实际上是创建的。

值得再次强调的是:这基本上是NIX语言唯一实际能做的事情。在Nix代码中有大量的推送数据和函数,但归根结底都是对派生的调用。

请注意,我们在该派生中引用了${bash}。这实际上是本文前面的派生,而变量替换实际上是派生彼此依赖的方式。变量bash指的是对派生的另一个调用,它在求值时生成构建bash的指令。

NIX语言实际上从来没有构建过任何东西。它创建派生,然后,其他NIX工具读取这些派生并构建输出。NIX语言只是一种用于创建派生的特定于域的语言。

Nixpkgs是NIX的全局默认软件包存储库,但它与您听到软件包存储库时可能会想到的完全不同。

Nixpkgs是一个单独的NIX程序。它利用了NIX语言经过延迟计算这一事实,并且包括很多对派生的调用。Nixpkgs的(简化但)基本结构类似于:

{*ruby=派生{.};*python=派生{.};*NodeJS=派生{.};*…。}。

为了构建“ruby”,各种工具只是强制Nix对该属性集的“ruby”属性求值,这将调用派生,将Ruby的派生生成到Nix Store中,并返回构建的路径。然后,该工具在该路径上运行类似nix-build的命令来生成输出。

嗯,要让你真切地感受到NIX带来的范式转变,需要的文字比我在这里写的要多得多--可能还需要一些动手实验,但希望我已经让你尝到了甜头。

如果你正在寻找更多的Nix内容,我正在向公众重新发布我在Shopify为开发者录制的一系列截屏视频。在YouTube上查看Nixology。

您也可以和我一起讨论Shopify如何使用NIX重新构建我们的开发工具。我将再次介绍其中的一些内容,并展示我们日常实际使用的一些工具。

如果你想在Nix上工作,来加入我的团队吧!我们一直在招聘,所以请查看我们的工程招聘页面,了解我们的空缺职位。了解我们在冠状病毒期间继续招聘时采取的行动