实用nixpkgs薄片的详细指南

881 阅读11分钟

Flakes是Nix生态系统中的一个新特性。Flakes取代了有状态的通道(这在新手中引起了很多混乱),并引入了一个更直观和一致的CLI,使其成为开始使用Nix的一个完美机会。

这篇文章是对Nix本身和flakes功能的一个快速介绍。它包含了在现实生活中使用flakes的例子和建议:用各种语言构建应用程序。

什么是Nix?

简而言之,Nix是一个软件包管理器和一个构建系统。它最重要的方面是允许为可重复的软件构建编写声明性的脚本。它还有助于在使用函数式编程范式时测试和部署软件系统。有一个庞大的Nix软件包库,叫做nixpkgs,还有一个GNU/Linux发行版,将Nix的思想扩展到操作系统层面,叫做NixOS

Nix的构建指令被称为 "派生",是用Nix这种编程语言编写的。派生指令可以为软件包或甚至整个系统编写。之后,它们可以通过软件包管理器Nix确定地 "实现"(构建)。衍生程序只能依赖于一组预先定义的输入,所以它们在某种程度上是可重复的。

你可以在我关于Nix的博文中阅读更多关于Nix的好处。

什么是Nix薄片?

片断是独立的单元,有输入(依赖关系)和输出(包、部署指令、用于其他片断的Nix函数)。你可以把它们想象成Rust crates或Go模块,但与语言无关。Flakes有很好的可重现性,因为它们只允许依赖它们的输入,而且它们在锁文件中钉住了上述输入的确切版本。

如果你已经熟悉了Nix,那么Flakes对于Nix表达式来说就像派生程序对于构建指令一样。

开始使用Nix

为了用flakes做任何事情,你首先必须在你的机器上建立并运行 "不稳定 "的Nix。不要介意它被称为 "不稳定":一般来说,在你的机器上运行并不危险,它只是比 "稳定 "的变化更频繁。最简单的方法是使用官方安装程序,它可以在任何Linux发行版、macOS或Windows Subsystem for Linux上运行:

curl -L https://nixos.org/nix/install | sh

按照说明进行操作,直到你的机器上有了Nix,然后用更新到不稳定版本:

nix-env -f '<nixpkgs>' -iA nixUnstable

并启用实验性功能:

mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf

如果你正在使用NixOS或在安装时遇到一些问题,请咨询NixOS的维基:https://nixos.wiki/wiki/flakes#Installing_flakes

对片状系统有一个感觉

现在你已经安装了一个 "片状 "的Nix,是时候使用它了!

nix shell

首先,让我们进入一个shell,其中有来自nixpkgs分支的GNU Hellonixpkgs-unstable:

nix shell github:nixos/nixpkgs/nixpkgs-unstable#hello

注意,这将启动与你正在运行的相同的shell,但在你的$PATH ,添加一个包含hello 可执行文件的目录。shell看起来应该和它在nix shell 外的情况没有什么不同,所以如果看起来什么都没有发生,不要惊慌!可执行文件本身并没有安装在任何地方,它被下载并解压在你可以认为是一个缓存目录中。

现在,在这个外壳中,尝试运行hello

让我们来看看这个命令是做什么的。nix shell 是一个nix子命令,用来运行一个shell和一些在$PATH 中可用的包。这些包可以作为参数以 "installable "的格式指定。每个可安装包都包含两个部分:URL(这里是github:nixos/nixpkgs/master )和一个 "属性路径"(这里是hello )。

有几个URL方案被支持:

  • github:owner/repo/[revision or branch] 和 (用于gitlab:owner/repo/[revision or branch] github.comgitlab.com 上的公共仓库;注意,分支名称不能包含斜线)。
  • https://example.com/path/to/tarball.tar.gz 用于tarballs。
  • git+https://example.com/path/to/repo.git 和 用于普通的git仓库(当然,你也可以对GitHub和GitLab使用这个)。你可以通过添加 来指定分支或修订版。git+ssh://example.com/path/to/repo.git ?ref=<branch name here>
  • file:///path/to/directory 或 或 用于本地目录。/path/to/directory ./path/to/relative/directory
  • flake-registry-value 为flake注册表的一个值(本文中我不会谈论flake注册表)。

所以,还有一些其他的方法可以得到同样的外壳。

nix shell https://github.com/nixos/nixpkgs/archive/nixpkgs-unstable.tar.gz#hello
nix shell 'git+https://github.com/nixos/nixpkgs?ref=nixpkgs-unstable#hello'
nix shell nixpkgs#hello # nixpkgs is specified in the default registry to be github:nixos/nixpkgs

至于属性路径,现在只要知道它是一个由Nix "属性名称 "的句点分隔的列表,根据一些简单的逻辑来选择flake输出。

请注意,在这种情况下,Nix不需要构建任何东西,因为它可以直接从二进制缓存中获取GNU Hello及其依赖项。为了实现这一点,Nix从表达式中评估一个派生,对其内容进行散列,并查询它所知道的所有缓存,看看是否有人缓存了带有这个散列的派生。Nix使用所有的依赖关系和所有的指令作为这个哈希值的输入!如果某个二进制缓存有准备好的版本,它就可以被替换(下载)。否则,Nix将通过首先实现(替换或构建)所有的依赖关系,然后执行构建指令来构建衍生。

你可能想知道可执行文件到底安装在哪里。好吧,试试command -v hello ,看看它位于/nix/store 的一个子目录下。事实上,所有Nix派生都有 "存储路径"(位于/nix/store 的路径)作为输入和输出。

nix build

如果你只是想建立一些东西,而不是用它进入一个shell,可以试试nix build:

$ nix build nixpkgs#hello

这将构建Hello(或者从二进制缓存中获取它,如果有的话),然后将其符号链接到你当前目录中的result 。然后你就可以探索result ,例如:

$ ./result/bin/hello
Hello, world!

nix develop

尽管使用了二进制缓存,Nix是一个源代码优先的软件包管理器。这意味着它有能力为它的派生程序提供一个构建环境。所以,你可以用Nix来为你管理你的构建环境!要进入一个包含GNU Hello的所有运行时和构建时依赖项的shell,请使用:

$ nix develop nixpkgs#hello

在这个shell中,你可以调用unpackPhase ,将GNU Hello的源代码放在当前目录中,然后调用configurePhase ,以正确的参数运行configure 脚本,最后调用buildPhase ,进行构建。

nix profile

Nix实现了有状态的 "配置文件",允许用户 "永久 "地安装东西。

比如说:

nix profile install nixpkgs#hello
nix profile list
nix profile update hello
nix profile remove hello

如果你已经熟悉了Nix,这是对nix-env 的一种替代。

nix flake

nix flake 一组子命令是用来观察和操作片断本身,而不是它们的输出。

nix flake show

这个命令接收一个flake的URI,并将flake的所有输出打印成一个漂亮的树状结构,将属性路径映射到值的类型。

比如说:

$ nix flake show github:nixos/nixpkgs
github:nixos/nixpkgs/d1183f3dc44b9ee5134fcbcd45555c48aa678e93
├───checks
│   └───x86_64-linux
│       └───tarball: derivation 'nixpkgs-tarball-21.05pre20210407.d1183f3'
├───htmlDocs: unknown
├───legacyPackages
│   ├───aarch64-linux: omitted (use '--legacy' to show)
│   ├───armv6l-linux: omitted (use '--legacy' to show)
│   ├───armv7l-linux: omitted (use '--legacy' to show)
│   ├───i686-linux: omitted (use '--legacy' to show)
│   ├───x86_64-darwin: omitted (use '--legacy' to show)
│   └───x86_64-linux: omitted (use '--legacy' to show)
├───lib: unknown
└───nixosModules
    └───notDetected: NixOS module

nix flake clone

nix flake clone 将克隆flake的源文件到本地目录,类似于 。git clone

让我们克隆一些简单的flake并对其使用一些其他的nix flake 子命令。

nix flake clone git+https://github.com/balsoft/hello-flake/ -f hello-flake
cd hello-flake

nix flake lock (以前是 )nix flake update

每次你对本地目录中的一些flake调用Nix命令时,Nix都会确保flake.lock 中的内容满足flake.nix 中的inputs 。如果你想只做这些,而不实际构建(甚至评估)任何输出,请使用nix flake lock

还有一些用于flake输入操作的参数,可以传递给大多数Nix命令。

  • --override-input flake.nix 的 中指定的输入名称,以及作为该输入的 flake URI; - 将接受一个输入名称,并将该输入更新为最新版本的 flake URI,以满足 。inputs --update-input flake.nix

编写你自己的

现在你知道如何与flake交互了,是时候写一个了。

Nix语言复习

Nix中广泛使用的数据类型是属性集:一种用于存储键值对的数据类型。它类似于许多语言中的JSON对象或哈希玛。它的语法与类C语言中的语句列表很相似,令人困惑。

{
  hello = "world";
  foo = "bar";
}

上面的集合等同于这个JSON对象。

{
    "hello": "world",
    "foo": "bar"
}

hello 和 ,通常被称为 "属性 "或 "属性名称"; 和 是 "属性值"。foo "world" "bar"

要从一个属性集中获得一个属性值,请使用. 。比如说。

let
  my_attrset = { foo = "bar"; };
in my_attrset.foo

(let ... in 是一种创建绑定的方法;它里面的语法与属性集的语法相同)

你也可以通过用. 来设置特定的属性来缩写你的属性集,而不是定义整个属性集。

{
  foo.bar = "baz";
}

相当于

{
  foo = { bar = "baz"; };
}

其他类型包括字符串("foo")、数字(1, 3.1415)、异质列表([ 1 2 "foo" ])和--相当重要的--函数(x: x + 1)。

函数支持属性集的模式匹配。例如,这个函数。

{ a, b }: a + b

当与{ a = 10; b = 20; } 一起调用时将返回30。

函数的应用是以ML风格完成的。

let
  f = { a, b }: a + b;
in f { a = 10; b = 20; }

函数本身排在第一位。然后是一个用空格隔开的参数列表。

如果你想有一个多参数的函数,请使用currying。

let
  f = a: b: a + b;
in f 10 20

在这个例子中,f 10 评估为b: 10 + b ,然后f 10 20 评估为30

如果你想了解更多关于Nix的信息,请查看相应的手册部分Nix Pills

基本的片状结构

你在上面得到的语言描述远远不够完整或正式,但它应该能帮助你理解,更重要的是,写一些简单的Nix表达式,更重要的是,写一个flake。

一个Nix flake是一个目录,其中包含一个flake.nix 文件。该文件必须包含一个属性集,其中有一个必需的属性--outputs --以及可选的descriptioninputs

outputs 是一个函数,它接收一个属性集的输入(总是至少有一个输入-- --指的是Nix当前正在评估的flake;这是由于懒惰而可能的)。所以,最微不足道的片断可能是这样的。self

{
  outputs = { self }: { };
}

这是一个没有外部输入和输出的片断。不是很有用,是吧?

好吧,我们可以给它添加一个任意的输出,然后用nix eval 来评估它,看它是否工作。

{
  outputs = { self }: {
    foo = "bar";
  };
}
$ nix eval .#foo
"bar"

不过还是不怎么有用。

让我们做一个能做一些有用的东西的薄片吧!为此,我们很可能需要一些输入。

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = { self, nixpkgs }: { };
}

还是没有什么用;我们没有任何输出!但是现在有一个外部的。然而,现在有了一个外部的nixpkgs 输入。

虽然outputs 返回的属性集可能包含任意的属性,但一些标准的输出可以被各种nix 的工具所理解。例如,有一个包含包的packages 输出。它能很好地与Getting a feel for flakes中描述的命令一起工作。让我们来添加它吧

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = /* something here */;
  };
}

首先,让我们理解为什么我们在这里需要x86_64-linux 。薄片向我们承诺密封式评估,这意味着无论评估者的环境如何,薄片的输出都应该是一样的。在构建系统中,评估环境的一个特殊属性是平台(架构和操作系统的组合)。正因为如此,所有与包有关的flake输出都必须以某种方式明确指定平台。标准的方法是使输出成为一个属性集,名称是平台,值是输出在语义上代表的任何东西,但专门为该平台构建。在packages ,每个平台的值都是一个包的属性集。

你现在可能会想:那么,我们怎么能只写nix build nixpkgs#hello ,而不明确指定平台就能得到一个包呢?嗯,这是因为Nix实际上在幕后为你做了这些。对于nix shellnix buildnix profilenix develop (以及其他一些命令),Nix试图通过以特定的顺序尝试多个输出来弄清你想要的输出。比方说,你在运行Linux的x86_64机器上做nix build nixpkgs#hello 。然后Nix会尝试。

  • hello
  • packages.x86_64-linux.hello
  • legacyPackages.x86_64-linux.hello

我们已经熟悉了packages 输出;legacyPackages 是专门为nixpkgs设计的。nixpkgs仓库比flakes老得多,所以不可能把它的任意属性格式装进整齐的packageslegacyPackages 被设计出来以适应遗留的混乱。特别是,legacyPackages 允许每个平台的包集成为任意属性集,而不是结构化的包。

所以,让我们在我们自己的flake中从nixpkgs中重新导出hello

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
  };
}

现在我们可以构建重新导出的软件包。

$ nix build .#hello

或者运行它。

$ nix run .#hello
Hello, world!

(默认情况下,nix run 将执行与包的属性名称相同的二进制文件。)

好啊!现在我们有了一个输出包的flake,我们可以使用或构建。

另一个我们可以添加的东西是一个 "开发 "外壳,其中包含一些在我们的flake上工作时可能有用的工具。$PATH 在这个例子中,也许我们想在hellocowsay ,以打印友好的问候语,然后让牛说出它。对于这样的开发外壳,有一个特殊的输出,叫做devShell 。在nixpkgs中也有一个函数用于构建这样的shells。为了防止多次写入不方便的nixpkgs.legacyPackages.x86_64-linux ,让我们通过一个let ... in 绑定来提取它。

{
  inputs = { nixpkgs.url = "github:nixos/nixpkgs"; };

  outputs = { self, nixpkgs }:
    let pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      packages.x86_64-linux.hello = pkgs.hello;

      devShell.x86_64-linux =
        pkgs.mkShell { buildInputs = [ self.packages.x86_64-linux.hello pkgs.cowsay ]; };
   };
}

现在我们可以用nix develop 进入开发环境。如果你想在该环境中运行Bash以外的shell,你可以使用nix shell -c $SHELL

$ nix develop -c $SHELL
$ hello | cowsay
 _______________
< Hello, world! >
 ---------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

让我们用nix flake show 来检查我们的flake。

$ nix flake show
path:/path/to/flake
├───devShell
│    └───x86_64-linux: development environment 'nix-shell'
└───packages
      └───x86_64-linux
            └───hello: package 'hello-2.10'

现在我们已经写好了稍微有用的 "hello "flake,是时候进入实际应用了

一些技巧和窍门

direnv

你可以使用direnvnix-direnv,在你改变目录进入项目时自动进入devShell ,该项目被打包在flake中。在这种情况下,.envrc 文件就非常简单了。

use flake

flake-utils

有一个库可以帮助你把无聊的每个平台的attrsets提取出来。 flake-utils.如果我们在我们的例子flake中使用flake-utils,我们可以让它支持所有的nixpkgs平台,几乎没有额外的代码。

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in {
        packages.hello = pkgs.hello;

        devShell = pkgs.mkShell { buildInputs = [ pkgs.hello pkgs.cowsay ]; };
      });
}

请注意,现在nix flake show 的输出中出现了更多的平台。

path:/path/to/flake
├───devShell
│     ├───aarch64-linux: development environment 'nix-shell'
│     ├───i686-linux: development environment 'nix-shell'
│     ├───x86_64-darwin: development environment 'nix-shell'
│     └───x86_64-linux: development environment 'nix-shell'
└───packages
      ├───aarch64-linux
      │     └───hello: package 'hello-2.10'
      ├───i686-linux
      │     └───hello: package 'hello-2.10'
      ├───x86_64-darwin
      │     └───hello: package 'hello-2.10'
      └───x86_64-linux
             └───hello: package 'hello-2.10'

所有的平台属性都是由flake-utils自动插入的。

包装现有的应用程序

flake最明显的用例是打包现有的应用程序,以获得Nix生态系统的好处。为了促进这一工作,我编写了一个模板库,你可以在nix flake init -t 。本节将主要介绍如何调整模板以正确构建你的应用程序。

Haskell (cabal)

Haskell在Nix生态系统中普遍存在。Nixpkgs包含了Hackage的完整镜像,并且有多种工具可以促进使用Nix构建Haskell应用程序。

用Nix构建Haskell应用程序的最流行方式是 cabal2nix.它从你的项目的cabal文件中提取依赖信息,并使用nixpkgshaskellPackages 集合来解决这些依赖。

要把它添加到你现有的Haskell应用程序中,请做。

nix flake init -t github:serokell/templates#haskell-cabal2nix

它将创建一个与此类似的flake.nix (注意throw <...> 替换名称;在那里写上你的项目名称)。

{
  description = "My haskell application";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};

        haskellPackages = pkgs.haskellPackages;

        jailbreakUnbreak = pkg:
          pkgs.haskell.lib.doJailbreak (pkg.overrideAttrs (_: { meta = { }; }));

        packageName = throw "put your package name here!";
      in {
        packages.${packageName} = # (ref:haskell-package-def)
          haskellPackages.callCabal2nix packageName self rec {
            # Dependency overrides go here
          };

        defaultPackage = self.packages.${system}.${packageName};

        devShell = pkgs.mkShell {
          buildInputs = with haskellPackages; [
            haskell-language-server
            ghcid
            cabal-install
          ];
          inputsFrom = builtins.attrValues self.packages.${system};
        };
      });
}

使用flake

你可以使用nix build 构建defaultPackage ,并做所有其他常规的事情。nix shell,nix develop, 等等。

潜在的修改和故障排除

除非你需要devShell 中提供的一些开发工具(如语言服务器或ghcid),否则就把它们删除。

如果你需要不同版本的GHC,在haskellPackages 定义中选择它,例如,要使用GHC 9.0.1,就做。

haskellPackages = pkgs.haskell.packages.ghc901;

如果某些依赖关系无法构建,你需要在包定义中覆盖它(准确地说,是callCabal2nix 的最后一个参数)。例如,如果你的项目依赖于gi-gtk-declarative ,而它在当前版本的nixpkgs中被破坏了,因为一些版本约束不匹配,那么就做(用失败的包名代替gi-gtk-declarative )。

# <...>
packages.${packageName} =
  haskellPackages.callCabal2nix packageName self rec {
    gi-gtk-declarative =
      jailbreakUnbreak haskellPackages.gi-gtk-declarative;
  };
# <...>

注意:jailbreakUnbreak 只删除了cabal版本约束。如果在某些依赖关系中存在实际的破坏性变化,构建仍然会失败。在这种情况下,你能做的不多,只能尝试修复软件包的实际问题,这需要对overrideAttrs 的工作方式有一定的了解,而这已经超出了本教程的范围。不要害怕阅读nixpkgs手册或在任何你能找到的Nix社区中寻求帮助!

如果某些依赖关系因为测试失败而无法构建(当测试需要网络访问或Nix构建的本地资源不可用时可能会出现这种情况),你可以像这样禁用它们。

# <...>
packages.${packageName} =
  haskellPackages.callCabal2nix packageName self rec {
    gi-gtk-declarative =
      pkgs.haskell.lib.dontCheck haskellPackages.gi-gtk-declarative;
  };
# <...>

其他构建选项

用Nix构建Haskell应用程序和库的一些其他选项。

Rust (Cargo)

Rust在Nix社区也相当流行,尽管由于相对年轻,目前的集成度还没有那么好。有多种相互竞争的方式来构建crate。我更喜欢crate2nix的工作方式:它在操作上与cabal2nix非常相似,但由于Cargo的一些特殊性,它要复杂一点。

要得到一个通过crate2nix构建单个Cargo箱的模板,请运行nix flake init -t github:serokell/templates#rust-crate2nixflake.nix ,就会和这个文件类似。

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    crate2nix = {
      url = "github:kolloch/crate2nix";
      flake = false;
    };
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, crate2nix, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        crateName = throw "Put your crate name here";

        inherit (import "${crate2nix}/tools.nix" { inherit pkgs; })
          generatedCargoNix;

        project = pkgs.callPackage (generatedCargoNix {
          name = crateName;
          src = ./.;
        }) {
          defaultCrateOverrides = pkgs.defaultCrateOverrides // {
            # Crate dependency overrides go here
          };
        };

      in {
        packages.${crateName} = project.rootCrate.build;

        defaultPackage = self.packages.${system}.${crateName};

        devShell = pkgs.mkShell {
          inputsFrom = builtins.attrValues self.packages.${system};
          buildInputs = [ pkgs.cargo pkgs.rust-analyzer pkgs.clippy ];
        };
      });
}

故障排除和改进

假设你的软件包需要一些 "本地"(非Rust)库,要么是自己需要,要么是通过依赖关系过境。在这种情况下,你必须手动指定它们到defaultCrateOverrides ,除非它们已经在 nixpkgs 的defaultCrateOverrides 中指定。例如,如果你的应用程序叫my-music-player ,需要libpulseaudio

# <...>
defaultCrateOverrides = pkgs.defaultCrateOverrides // {
  my-music-player = _: {
    buildInputs = [ pkgs.libpulseaudio ];
  };
}
#<...>

替代方法

  • 手动使用nixpkgs中的buildRustCrate
  • naersk

Python

在nixpkgs中通过python3Packages ,Python生态系统得到了一定的支持,但其支持并不像对Haskell那样完整。然而,如果你的项目是用poetry打包的,你很幸运,因为有poetry2nix ,它可以让你轻松地构建应用程序。

要初始化模板,nix flake init -t github:serokell/templates#python-poetry2nix 。它看起来像这样。

{
  description = "My Python application";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};

        customOverrides = self: super: {
          # Overrides go here
        };

        app = pkgs.poetry2nix.mkPoetryApplication {
          projectDir = ./.;
          overrides =
            [ pkgs.poetry2nix.defaultPoetryOverrides customOverrides ];
        };

        packageName = throw "put your package name here";
      in {
        packages.${packageName} = app;

        defaultPackage = self.packages.${system}.${packageName};

        devShell = pkgs.mkShell {
          buildInputs = with pkgs; [ poetry ];
          inputsFrom = builtins.attrValues self.packages.${system};
        };
      });
}