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.com和gitlab.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
--以及可选的description
和inputs
。
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 shell
、nix build
、nix profile
、nix 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老得多,所以不可能把它的任意属性格式装进整齐的packages
。legacyPackages
被设计出来以适应遗留的混乱。特别是,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
在这个例子中,也许我们想在hello
和cowsay
,以打印友好的问候语,然后让牛说出它。对于这样的开发外壳,有一个特殊的输出,叫做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
你可以使用direnv和nix-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-crate2nix
。flake.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};
};
});
}