29 分钟
Nix 详解(三) nix 领域特定语言
version: nix-2.14.1 | 示例代码库
概述
为了更好的描述一个包,从源码到制品的过程,nix 设计了一套领域特定语言(DSL),来声明一个包。这个语言就叫做 nix 语言。
nix 是一种特定领域的、纯函数式的、惰性求值的、动态类型的编程语言。
该语言主要的应用场景为:
- 定义一个 nix channel,之前文章多次提到的 nixpkgs 收录的超过 8 万个包,就是通过 nix 语言声明的。
- 在
shell.nix中使用,正如之前文章所讲,其可以为一个项目定义一个可重现的隔离的开发环境。 - 在 NixOS 中,来定义操作系统环境,本系列不多赘述。
Hello World
nix-lang-demo/01-hello.nix
let
msg = "hello world";
in msg运行代码,nix-instantiate --eval nix-lang-demo/01-hello.nix,输出如下:
"hello world"除了直接运行一个 .nix 代码文件外。通过实验性的 nix repl 命令,可以打开一个 nix 交互式 shell,来交互式的执行 nix 表达式。
关于 let in 参见下文:局部变量。
程序结构
和常规的命令式通用编程语言不同,nix 是一种声明式的表达式语言。
常规的 Go、Java、C 等编程语言,一个程序的入口是一个 main 函数。
在 nix 中,没有一个 main 函数。一个 nix 的程序就是 nix 提供的几种基本结构组合而成的表达式。
在执行一个正确的 nix 程序时,解释器最终会推导出一个且必须推导出一个值出来。这个值,必须是 nix 支持的几种数据类型之一,参见下文。
数据类型
nix 的数据类型类似于 JSON,可以分为基本数据类型、列表和属性集。
基本数据类型
字符串,支持多种表达方式。
"string"双引号包裹的字符串,对于特殊字符需使用\转移,如:\"、\$、\n、\r、\t。该类字符串支持使用${}进行插值。和其他语言的""相比,在 nix 中,该类型字符串支持多行的写法。''string''两个单引号包裹的字符串,支持多行,该类字符串会自动删除每一行相同数目(这个数目为所有行中前导空格数最小的数目)的前导空格。比如:'' This is the first line. This is the second line. This is the third line. ''等价于
"This is the first line.\nThis is the second line.\n This is the third line.\n"该类型字符串也支持
${}进行字符串插值。对于特殊字符需使用''转移,如:'''等价于"''"''$等价于"$"\n等价于"\\n",''\n等价于"\n"\r等价于"\\r",''\r等价于"\r"\t等价于"\\t",''\t等价于"\t"
双单引号字符串和双引号字符串相比,有更少的引用,且,在书写多行字符串时,代码格式化的缩进会自动去除,且,有更少的转移字符。因此,在写多行字符串时,建议使用双单引号格式。
最后,符合 RFC 2396 的 URL 可以不适用引号包裹,可以直接使用。
数字,支持且不区分整型和浮点型,格式如
123、123.43、.27e13路径,如
/bin/sh、./abc、abc/123,包含一个斜杠的会被识别为路径类型。nix 会把这些路径都转换为绝对路径,注意 nix 中的相对路径都是相对于.nix源代码文件的。nix 也支持
~/abc这种写法。nix 还支持一种特殊写法,如
<nixpkgs>,nix 在NIX_PATH环境变量中查找指定名字的路径,当NIX_PATH不存在时,会在~/.nix-defexpr/channels中查找。路径可以作为字符串插值的符号,如
"${./foo.txt}",针对这种情况,nix 会将路径对应文件或目录复制到"/nix/store/<hash>-foo.txt"中。(Nix 语言假定在计算 Nix 表达式时所有输入文件都将保持不变。例如,假设您在 nix repl 会话期间使用了内插字符串中的文件路径。稍后在同一会话中,更改文件内容后,再次使用文件路径评估内插字符串可能不会返回新的存储路径,因为 Nix 可能不会重新读取文件内容)除了
<>语法外,路径也支持插值,注意,至少要有一个/出现在插值之前,才会被识别为路径。例如:a.${foo}/b.${bar}会被识别为除法运算而不是路径,因此需要改为./a.${foo}/b.${bar}。注意,通过
nix-instantiate --eval执行文件时,如果使用--strict启用严格模式,则需要保证所有的 PATH 都必须存在,且 nix 会将这些文件或目录复制到/nix/store中,路径变量的值将变为/nix/store/$hash-$name。bool,可选值为 true 或 false。
null,空值,表示 null。
完整示例 (nix-lang-demo/02-primitives-data-type.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/02-primitives-data-type.nix --strict --json | jq
let
a = "1";
in {
demo_01_str_double_quotes = "foo bar \r \t \n \\ \${";
demo_02_str_with_string_interpolation = "a: ${a}";
demo_03_str_two_single_quotes = ''
line1
line2
\r \n \t \
''\r ''\t ''\n ''' ''${
a: ${a}
'';
demo_04_str_url = https://rectcircle.cn;
demo_05_num_int = 1;
demo_06_num_float = 1.1;
demo_07_num_e = .27e13;
demo_08_path_abs_path = /bin/sh;
demo_09_path_rel_path1 = ./demopath/a;
demo_10_path_rel_path2 = demopath/a;
demo_11_path_home_path = ~/.bashrc;
demo_12_bool_true = true;
demo_13_bool_false = false;
demo_14_null = null;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/02-primitives-data-type.nix --strict --json | jq,输出如下:
{
"demo_01_str_double_quotes": "foo bar \r \t \n \\ ${",
"demo_02_str_with_string_interpolation": "a: 1",
"demo_03_str_two_single_quotes": "line1\nline2\n\\r \\n \\t \\\n\r \t \n'' ${\na: 1\n",
"demo_04_str_url": "https://rectcircle.cn",
"demo_05_num_int": 1,
"demo_06_num_float": 1.1,
"demo_07_num_e": 2700000000000,
"demo_08_path_abs_path": "/nix/store/9wk86jmq024g8yb40wh4y5znkh1dix8y-sh",
"demo_09_path_rel_path1": "/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a",
"demo_10_path_rel_path2": "/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a",
"demo_11_path_home_path": "/nix/store/x1znix2cdfg9fnmgvkdda19n28jphdm7-.bashrc",
"demo_12_bool_true": true,
"demo_13_bool_false": false,
"demo_14_null": null
}函数类型
nix 语言是函数式的,其函数也是一种数据类型,也就是说 nix 的函数可以作为返回值,也可以作为函数参数、可以赋值给变量。
因为函数可以在列表、属性集中使用,因此先介绍函数。
nix 函数的定义语法为: 函数参数: 函数体,语义为:接收一个值作为一个参数,并返回值。函数调用方式为 函数名 函数参数值。例如:
let
addOne = x: x+1;
in addOne 1 # 返回 2可以说,上面这句话,这就是 nix 函数的全部。但是基于此 nix 提供了一些和 Python 差不多强大的函数能力。
- 多参数函数。如:函数 f
x: y: x + y,其实等价于x: (y: x + y),可以理解为,参数为x的函数返回了一个参数为y的函数,这个参数为y的函数返回x + y的值。调用方式为f 1 2,其实等价于(f 1) 2。 - 命名参数函数。示例如下:
- 简单场景,函数 f
{a, b}: a + b,本质上是一种语法糖,节本等价于x: x.a + x.b。调用方式为f {a = 1; b = 2; },但是需要注意的是这种方式 nix 会对参数进行属性是否存在校验。也就是说调用时缺少(f {a = 1;})或者多余(f {a = 1; b = 2; c= 3;})属性均会报错。 - 属性默认值,函数 f
{a, b ? 0}: a + b,b ? 0表示 b 的默认值为 0,调用时可以不传 b,如f {a = 1;}将返回 1。 - 其他属性和命名属性,函数 f
args@{ a, b, ... }: a + b + args.b + args.c或{ a, b, ... }@args: a + b + args.b + args.c。...该函数调用时,允许传递了除了 a, b 之外的属性。@args表示将整个属性集赋值给变量args,在函数体中可以使用 args 访问整个属性集。...和@一般同时出现,但这不是强制的。如下方式调用:f {a = 1; b = 2;}报错。f {a = 1; b = 2; c = 3;}返回 8。f {a = 1; b = 2; c = 3; d = 4;}返回 8。
- 简单场景,函数 f
完整示例 (nix-lang-demo/03-func-data-type.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq
let
addOne = x: x+1;
add = x: y: x + y;
addTwo = add 2;
addAttrs = {x, y}: x + y;
addAttrsYDefault2 = {x, y?2}: x + y;
addAttrsAtAndRemaining = attrs@{x, y, ...}: x + attrs.y + attrs.z;
in {
demo_01_add_one_2 = addOne 2;
demo_02_add_1_2 = add 1 2;
demo_03_add_two_1 = addTwo 1;
demo_04_add_attrs_x1_y2 = addAttrs { x = 1; y = 2; };
demo_05_add_attrs_y_default2_x1 = addAttrsYDefault2 { x = 1; };
demo_06_add_attrs_at_and_remaining_x_1_y_1_z_1_q_3 = addAttrsAtAndRemaining { x = 1; y = 1; z = 1; q = 3; };
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq,输出如下:
{
"demo_01_add_one_2": 3,
"demo_02_add_1_2": 3,
"demo_03_add_two_1": 3,
"demo_04_add_attrs_x1_y2": 3,
"demo_05_add_attrs_y_default2_x1": 3,
"demo_06_add_attrs_at_and_remaining_x_1_y_1_z_1_q_3": 3
}列表
nix 通过方括号 [] 定义一个列表。和其他语言不同,列表中的元素通过空格而不是分割。
如: [ 123 ./foo.nix "abc" (f { x = y; }) ],这个列表包含 4 个元素。第一个元素为数字、第二个元素为路径、第三个元素为字符串、第四个元素为调用函数 f 并获取结果(使用了小括号包裹)。
而对于 [ 123 ./foo.nix "abc" f { x = y; } ] 列表,包含 5 个元素。第四个元素为一个函数、第五个元素为属性集。
注意:数组的求值是惰性的,且是严格长度的。
完整示例 (nix-lang-demo/04-list-data-type.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/04-list-data-type.nix --strict --json | jq
let
addAttrs = { x, y }: x + y;
demo_01_list_1 = [ 123 demopath/a "abc" (addAttrs { x = 1; y = 2; }) ];
demo_01_list_2 = [ 123 demopath/a "abc" addAttrs { x = 1; y = 2; } ];
in
{
demo_01_list_1 = demo_01_list_1;
demo_01_list_2_len = builtins.length demo_01_list_2;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/03-func-data-type.nix --strict --json | jq,输出如下:
{
"demo_01_list_1": [
123,
"/nix/store/w996igw5fhzp5pmk8g9bfv99is99b0ap-a",
"abc",
3
],
"demo_01_list_2_len": 5
}属性集
nix 通过花括号 {} 定义一个属性集。属性集的每个元素(属性)为一个键值对,key 和 value 使用 = 分割,以 ; 结尾。
如:
{
x = 123;
text = "Hello";
y = f { bla = 456; };
}该示例包含 3 个属性,分别是:值为数字的 x、值为字符串的 text、值为 f 函数返回值的 y。
属性集的属性通过点号 . 方式访问,如:{ a = "Foo"; b = "Bar"; }.a。
如果访问属性不存在时,取默认值,可以通过 or 实现,如:{ a = "Foo"; b = "Bar"; }.c or "Xyzzy"。
属性集的 Name 可以是任意字符串,如果是包含特殊字符可以使用 ."xxx" 的方式访问,如:{ "$!@#?" = 123; }."$!@#?"。
属性的访问也支持插值,如:let bar = "foo"; in { foo = 123; }.${bar},等价于 { foo = 123; }.foo。
属性定义时其名字也支持插值,如:let bar = "foo"; in { ${bar} = 123; }.foo,等价于 { foo = 123; }.foo。
属性定义是如果其名字插值的是一个 null,则不会将该属性添加到该属性即中(因为 null 无法转换为一个字符串),如:{ ${null} = true; } 等价于 {}。
属性集可以通过 __functor 属性名,将该属性集定义成一个函数,如:
let add = { __functor = self: x: x + self.x; };
inc = add // { x = 1; };
in add 1- 第一行,定义了一个 add 属性集,其
__functor是属性是一个函数,该函数参数为 self 和 x,函数体为self.x + x - 第二行,使用
{ x = 1; }更新(//语法) add 属性集,其返回,赋值给变量 inc(注意这里的更新并不会影响 add 值自身,因为 nix 的值都是不可变的)。 - 第三行,将 inc 作为函数调用,参数为 1。此时,实际上调用了
__functor函数。 - 利用该特性可以实现类似面向对象的效果。
默认情况下,定义一个属性集,属性之间是不能相互引用,如下将报错:
{
y = 123;
x = y;
}通过,在花括号前添加 rec,表示声明一个递归属性集。此时,同一属性集内部的属性可以相互引用,如下不会报错:
rec {
y = 123;
x = y;
}此外,递归属性集,属性的引用和顺序无关,如下不会报错:
rec {
x = y;
y = 123;
}此外,在递归属性集中,如果引用的名字,在作用域内有同名的变量,且属性集内也有同名的属性,此时取属性集属性的值。如下:
let y = 456;
in rec {
x = y;
y = 123;
}将返回: { x = 123; y = 123; }。
完整示例 (nix-lang-demo/05-attrs-data-type.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/05-attrs-data-type.nix --strict --json | jq
let
bKey = "b";
dKey = "d";
demo_01_define = {
a = 1;
b = "b";
"$!@#?" = 123;
${dKey} = 4;
${null} = true;
};
demo_02_access = {
a = demo_01_define.a;
b = demo_01_define.${bKey};
c = demo_01_define.c or "c not exist";
"$!@#?" = demo_01_define."$!@#?";
d = demo_01_define.d;
};
callable_attr_define = { __functor = self: x: x + self.x; };
demo_03_callable_attr_object = callable_attr_define // { x = 1; };
demo_04_rec_attr1 = rec {
y = 123;
x = y;
};
y = 456;
demo_05_rec_attr2 = rec {
x = y;
y = 123;
};
in
{
demo_01_define = demo_01_define;
demo_02_access = demo_02_access;
demo_03_call_attr = demo_03_callable_attr_object 2;
demo_04_rec_attr1 = demo_04_rec_attr1;
demo_05_rec_attr2 = demo_05_rec_attr2;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/05-attrs-data-type.nix --strict --json | jq,输出如下:
{
"demo_01_define": {
"$!@#?": 123,
"a": 1,
"b": "b",
"d": 4
},
"demo_02_access": {
"$!@#?": 123,
"a": 1,
"b": "b",
"c": "c not exist",
"d": 4
},
"demo_03_call_attr": 3,
"demo_04_rec_attr1": {
"x": 123,
"y": 123
},
"demo_05_rec_attr2": {
"x": 123,
"y": 123
}
}变量
局部变量
nix 通过 let in 来创建一个作用域,并定义一批变量,如:
let
a = 1;
b = 2;
in
a + b如上写法等价于: 1 + 2。
属性继承
当我们想构造一个属性集,并想将作用域中的某些属性作为该属性集的属性时,一般的写法如下:
let
a = 1;
b = 2;
in {
a = a;
b = b;
c = 3;
}nix 提供继承语法糖,可以将上述简化为:
let
a = 1;
b = 2;
in {
inherit a b;
c = 3;
}inherit 还可以从一个属性集中继承其中的几个属性,示例如下:
let
a = 1;
x = {
b = 2;
c = 3;
};
in {
inherit a;
inherit (x) b c;
}等价于:
let
a = 1;
x = {
b = 2;
c = 3;
};
in {
a = 1;
b = x.b;
c = x.c;
}with 表达式
类似于 python 的 with。通过 with 可以创建一个作用域,并将一个属性集中属性作为作用域中的变量。
示例如下:
with {
a = 1;
b = 2;
}; a + b等价于:
let
a = 1;
b = 2;
in a + b等价于:
let
x = {
a = 1;
b = 2;
};
in with x;
a + b示例
nix-lang-demo/06-var.nix
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/06-var.nix --strict --json | jq
let
a = 1;
b = 2;
attrs1 = {
x = 3;
y = 4;
};
attrs2 = {
m = 5;
n = 6;
};
in with attrs2;
{
inherit a b;
inherit (attrs1) x y;
m = m;
inherit n;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/06-var.nix --strict --json | jq,输出如下:
{
"a": 1,
"b": 2,
"m": 5,
"n": 6,
"x": 3,
"y": 4
}流程控制
条件表达式
语法如下:
if e1 then e2 else e3例如:
let x = 1;
in if x > 0 then "x > 0" else "x <= 0"返回, “x > 0”。
循环
nix 是个无副作用的函数式的表达式语言。因此,nix 没有命令式编程语言的 while 或者 for 循环。
一般情况,需要循环场景,就是对列表或者属性集进行转换。nix 可以通过内置高阶函数,如 builtins.filter、 builtins.map,来达到类似的效果。
示例
nix-lang-demo/07-flow-control.nix
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/07-flow-control.nix --strict --json | jq
let
x = 1;
l = [1 2 3 4 5 6];
filter = builtins.filter;
map = builtins.map;
in {
demo_01_x_great_than_0 = if x > 0 then "x > 0" else "x <= 0";
demo_02_l_filter_map = map (e: e * 2) (filter (e: e<=3) l);
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/07-flow-control.nix --strict --json | jq,输出如下:
{
"demo_01_x_great_than_0": "x > 0",
"demo_02_l_filter_map": [
2,
4,
6
]
}错误处理
断言
通过 assert 可以检查某些条件是否成立,语法如下:
assert e1; e2其中 e1 是一个可以计算为 bool 类型值的表达式。如果 e1 为 true,则返回 e2 的值,如果 e1 为 false,则停止计算,并打印调用栈信息。如:
assert true; 1将返回 1。assert false; 1将报错。
抛出错误
nix 的错误抛出,由内置函数提供,语法如下:
builtins.throw s抛出错误,如果上层没有处理,解释器会打印消息 s,并停止运行(评估)。
错误终止
nix 的错误终止,由内置函数提供,语法如下:
builtins.abort s上层无法捕捉该异常,解释器会打印消息 s,并停止运行(评估)。
错误捕捉
nix 的错误捕捉,由内置函数提供,语法如下:
builtins.tryEval e- 只能捕捉
assert和builtins.throw产生的错误。 - 返回一个属性集,包含两个手机用:
successbool 类型,是否成功,如果捕捉到错误,则该属性为false。value任意类型。- 如果
success = false,则该参数为false,注意,不是错误消息(参见:issue)。 - 否者该参数 e 的值。
- 如果
示例
nix-lang-demo/08-err-handle.nix
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/08-err-handle.nix --strict --json | jq
let
divide = a: b: assert b !=0 ; a / b;
throw_err = msg: builtins.throw msg;
abort_err = msg: builtins.abort msg;
in {
demo_01_4_div_2 = divide 4 2;
demo_02_try_4_div_0 = builtins.tryEval (divide 4 0);
demo_03_try_4_div_2 = builtins.tryEval (divide 4 2);
demo_04_try_throw_err = builtins.tryEval (throw_err "err");
# demo_05_try_abort_err = builtins.tryEval (abort_err "err"); # abort 无法捕捉
# demo_06_try_builtins_4_div_0 = builtins.tryEval (4 / 0); # 除 0 无法捕捉
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/08-err-handle.nix --strict --json | jq,输出如下:
{
"demo_01_4_div_2": 2,
"demo_02_try_4_div_0": {
"success": false,
"value": false
},
"demo_03_try_4_div_2": {
"success": true,
"value": 2
},
"demo_04_try_throw_err": {
"success": false,
"value": false
}
}操作
Nix 操作符和 C 语言的类似,区别是:
- nix 不支持
:?,类似效果的是if then else。 - nix 不支持
++,--、+=、-=等类似的涉及修改变量值的操作符。 - nix 支持的一些 C 语言没有的操作符:
attrset ? attrpath,返回 bool 值, 判断属性集中是否存在某个属性。attrpath 支持a.b.c格式。list ++ list,返回一个 list,两个 list 连接产生一个新的 list。string + string,返回一个 string,字符串拼接。path + path,返回一个 path,路径拼接(注意最终都会转换为绝对路径进行拼接,而不是路径 join)。path + string,返回一个 path,路径拼接(两者先转换为字符串,然后直接拼接到一起,然后转换为一个路径)。string + path,返回一个 string,路径拼接(path 路径必须存在,nix 会将该路径复制到 /nix/store 中,并将 string 和/nix/store/$hash-文件名拼接,并转换为字符串),比如"/abc" + ./README.md,返回"/abc/nix/store/qmj08qmd1bb89g6wami4v2fq5ma4f42c-README.md"。attrset // attrset使用后一个属性集更新到前一个属性集中(存在则覆盖),返回这个更新后的属性集。bool -> bool一种特殊的逻辑运算符,等价于!b1 || b2,参见:wiki。
完整示例 (nix-lang-demo/09-operators.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/09-operators.nix --strict --json | jq
let
attrs1 = {
x = 1;
};
list1 = [1 2];
list2 = [3 4];
in
{
demo_01_attrs1_has_x = attrs1 ? x;
demo_02_attrs1_has_y = attrs1 ? y;
demo_03_attrs1_has_a_dot_b = attrs1 ? a.b;
demo_04_list1_concat_list2 = list1 ++ list2;
demo_05_str1_concat_str2 = "abc" + "123";
# demo_06_path1_concat_path2 = demopath/a + demopath/b; # 严格模式将报错,因为返回的路径不存在。
demo_07_path1_concat_str2 = "demopath/a" + demopath/b;
# demo_08_str1_concat_path2 = demopath/a + "demopath/b"; # 严格模式将报错,因为返回的路径不存在。
demo_08_attrs = attrs1;
demo_09_attrs1_merge_attrs2 = attrs1 // {y = 2;};
demo_10_implication = false -> true;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/09-operators.nix --strict --json | jq,输出如下:
{
"demo_01_attrs1_has_x": true,
"demo_02_attrs1_has_y": false,
"demo_03_attrs1_has_a_dot_b": false,
"demo_04_list1_concat_list2": [
1,
2,
3,
4
],
"demo_05_str1_concat_str2": "abc123",
"demo_07_path1_concat_str2": "demopath/a/nix/store/nqxj2if4v96ksj1mgsblgc375wcslf83-b",
"demo_08_attrs": {
"x": 1
},
"demo_09_attrs1_merge_attrs2": {
"x": 1,
"y": 2
},
"demo_10_implication": true
}内置常量和内置函数
- 内置常量:
builtins,包含内置函数的属性集。builtins.currentSystem,如"i686-linux"or"x86_64-darwin"。
- 已经添加到顶层作用域,无需通过
builtins引用的内置函数:abort,参见上文错误处理。baseNameOf s类似于 gnu 的 basename,去除路径的路径,返回文件名。breakIn debug mode (enabled using –debugger), pause Nix expression evaluation and enter the REPL. Otherwise, return the argument v.derivationnix 编译系统核心函数,参见下文:推导。derivationStrict没找到相关手册,只有一个相关 issue。dirOf s类似于 gnu 的 dirname,返回路径所在目录。fetchGit、fetchMercurial、fetchTarball、fetchTree,参见下文:fetch 相关函数。fromTOML未找到相关文档。import参见下文:模块系统。isNull e判断是否是 null(此功能已弃用;使用e == null代替)。map f list转换一个列表,函数式编程的 map 原语。placeholder不太理解,参见:原文。removeAttrs set list从 set 中删除指定的属性。scopedImport未找到相关文档。throw参见上文错误处理。toString将值转换为字符串,一个属性集可以通过特殊属性__toString = self: ...;自定义 toString 格式。
- 其他内置函数,参见:Nix 手册 - 内置函数。
fetch 相关函数
nix 提供了一些从网络上下载文件的内置函数,执行这些函数,nix 会将这些文件下载下来,并存储到 /nix/store 中,并返回存储的路径。
builtins.fetchurl下载 url。let fetchurl = builtins.fetchurl; in fetchurl { url = "http://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz"; sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465"; }builtins.fetchGit args从 git 中下载文件。args 是一个属性集。
url仓库地址。name存储到/nix/store的名称,默认为 URL 的 basename。rev要获取的 git 修订版。默认为 ref 指向的。ref分支名或者标签名,如master、"refs/heads/0.5-release",默认为HEAD。submodules是否 checkout 子模块,默认为 false。shallow是否浅克隆,默认为 false。allRefs是否获取仓库的所有引用,默认为 false,即只获取ref参数配置的。
示例:通过 ssh 从私有仓库获取。
builtins.fetchGit { url = "[email protected]:my-secret/repository.git"; ref = "master"; rev = "adab8b916a45068c044658c4158d81878f9ed1c3"; }示例:配置引用。
builtins.fetchGit { url = "https://github.com/NixOS/nix.git"; ref = "refs/heads/0.5-release"; }示例:下载指定分支的指定 commit(推荐配置 rev 来指定 commit,这样是可重现的,否则随着分支的提交,未来某个时刻获取到的和当前可能不一致)。
builtins.fetchGit { url = "https://github.com/nixos/nix.git"; rev = "841fcbd04755c7a2865c51c1e2d3b045976b7452"; ref = "1.11-maintenance"; }示例:如果要查找的 commit 位于 git 存储库的默认分支中,您可以省略 ref 属性。
builtins.fetchGit { url = "https://github.com/nixos/nix.git"; rev = "841fcbd04755c7a2865c51c1e2d3b045976b7452"; }示例:指定某个具体 tag。
builtins.fetchGit { url = "https://github.com/nixos/nix.git"; ref = "refs/tags/1.9"; }示例:获取最新版本。
builtins.fetchGit { url = "ssh://[email protected]/nixos/nix.git"; ref = "master"; }
builtins.fetchTarball args从 url 中下载一个 tar 包(压缩格式必须是 gzip, bzip2 or xz 之一的)(缓存在~/.cache/nix/tarballs/路径),并解包到一个目录中。注意,tar 的顶层目录会被删除。然后将目录存储到/nix/store,并返回该路径,该函数一般和 import 函数(参见下文)一起使用。with import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/nixos-14.12.tar.gz"; sha256 = "1jppksrfvbk5ypiqdz4cddxdl8z6zyzdb2srq8fcffr327ld5jj2"; }) {};
完整示例 (nix-lang-demo/10-builtins-fetch.nix)。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/10-builtins-fetch.nix --strict --json | jq
let
fetchurl = builtins.fetchurl;
fetchGit = builtins.fetchGit;
fetchTarball = builtins.fetchTarball;
in
{
demo_01_fetchurl = fetchurl {
url = "http://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz";
sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465";
};
demo_02_fetchGit = fetchGit {
name = "learn-nix-demo-source";
url = "https://github.com/rectcircle/learn-nix-demo.git";
rev = "7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326";
ref = "master";
shallow = true;
};
demo_03_fetchTarball = fetchTarball {
url = "https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre462063.2ce9b9842b5/nixexprs.tar.xz";
};
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/10-builtins-fetch.nix --strict --json | jq,输出如下:
{
"demo_01_fetchurl": "/nix/store/9bw6xyn3dnrlxp5vvis6qpmdyj4dq4xy-hello-2.1.1.tar.gz",
"demo_02_fetchGit": "/nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source",
"demo_03_fetchTarball": "/nix/store/10njnx13qh4x3z7j7q0jh7m64s0s95w1-source"
}模块系统
nix 通过 import path, 执行其他文件的代码,并返回执行的结果。在 nix 中 import 是一个内置函数。这里的 path 可以是一个 .nix 文件,也可以是一个目录,如果是一个目录或压缩包的话,将执行该目录中的 default.nix 文件。示例如下:
通过 import 函数可以将 nix 代码拆分到文件和目录,以实现模块划分和代码复用。
前文介绍的 nixpkgs channel 本质上就是这样一个模块。下文有一些导入 nixpkgs 的一些惯用用法。
示例 1:通过 github 提供的 archive 链接,导入一个历史上某个版本的 nixpkgs。
let pkgs = import (builtins.fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/d1c3fea7ecbed758168787fe4e4a3157e52bc808.tar.gz"; }) {}; in示例 2:通过 git 命令,导入一个历史上某个版本的 nixpkgs。
let pkgs = import (builtins.fetchGit { # Descriptive name to make the store path easier to identify name = "my-old-revision"; url = "https://github.com/NixOS/nixpkgs/"; ref = "refs/heads/nixpkgs-unstable"; rev = "d1c3fea7ecbed758168787fe4e4a3157e52bc808"; }) {}; in
完整示例
nix-lang-demo/demopath/default.nix
{
c = "demopath/default.nix var c";
}nix-lang-demo/11-import.nix。
# nix-env -iA nixpkgs.jq # 为了更好的展示结果,使用 jq 进行结果格式化展示。
# nix-instantiate --eval nix-lang-demo/11-import.nix --strict --json | jq
let
import_file = import ./01-hello.nix;
import_dir = import ./demopath;
in
{
demo_01_import_file = import_file;
demo_02_import_dir = import_dir;
}执行代码 nix-env -iA nixpkgs.jq && nix-instantiate --eval nix-lang-demo/11-import.nix --strict --json | jq,输出如下:
{
"demo_01_import_file": "hello world",
"demo_02_import_dir": {
"c": "demopath/default.nix var c"
}
}推导 (derivation)
参考: nix-pills/our-first-derivation | nix-pills/working-derivation.html
概述
前文,我们一直将 nix 定位为一个包管理工具。但从本质上来说,nix 的核心是一个包构建系统。
因此,nix 语言需要提供一套机制,可以让用户定义,软件包从源码到二进制产物的过程。
而推导(derivation)就是这样一个最重要的的一个内置函数。是 nix 作为一个构建系统的核心。
参数说明
在 nix 中, derivation 内置函数,定义一个软件包重源码到二进制产物的过程,该函数传递一个属性集作为参数,包含如下属性:
system必填,字符串,定义该构建过程要求的 CPU 架构(x86_64、arm)和操作系统名(linux、darwin)。可通过nix -vv --version命令获取(或者通过builtins.currentSystem变量获取,如果是支持所有平台,则可以直接使用这个参数),如果系统不匹配将失败(通过配置,nix 支持远端构建,参见: forward builds for other platforms)。该字段会作为环境变量传递给builder进程。name必填,字符串。被 nix-env 用作包的符号名称,并影响其最终存储路径/nix/store/$hash-$name,如果同时支持多版本的场景吗,建议该字段为包名-版本号。。该字段会作为环境变量传递给builder进程。builder必填,字符串或路径,描述一个构建脚本,可以来是另一个 derivation、源码,如./builder.sh。推荐使用 bash"${pkgs.bash}/bin/bash"。该字段指向的路径会拷贝到/nix/store中,并作为环境变量传递给builder进程。args选填,字符串列表,传递给builder的命令行参数。推荐写法为["-c" '' 编译脚本 '']。outputs选填,字符串列表,默认为["out"]。一般情况下,不需要更改(除非想精细化的管理依赖,如配置为[ "lib" "headers" "doc" ]时,其他的推导只需要依赖lib目录,这种写法可以加速缓存下载)。nix 会在/nix/store中创建这个列表中声明的所有路径。然后,将该列表中的元素作为 key,对应的路径作为 value,作为环境变量传递给builder进程。其他属性选填,支持字符串、数字、路径、列表、bool、null。这些字段会作为环境变量传递到builder进程中。需要说明的是:- 路径类型,会拷贝到
/nix/store中,然后将绝对路径传递builder给进程。 - bool 类型 true,会转换为 1。bool 类型 false、null 会转换为空串。
- 列表类型,元素会转换为字符串,然后用空格分隔拼接成一个字符串。
- 路径类型,会拷贝到
示例
源码
假设我们有一个 Go 项目,该项目是一个命令行工具,希望通过 nix 编译和发行该包。本部分实现一下该项目:
nix-package-demo/go.mod
module github.com/rectcircle/learn-nix-demo/nix-package-demo
go 1.19nix-package-demo/main.go
package main
import "fmt"
func main() {
fmt.Println("hello world!")
}定义 derivation
nix-lang-demo/12-derivation.nix
# drv_path=$(nix-instantiate nix-lang-demo/12-derivation.nix) && echo "drv_path: $drv_path" && echo "drv: $(nix --extra-experimental-features nix-command show-derivation $drv_path)" && nix-store -r $drv_path && nix-store --read-log $drv_path
# nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
# 整体来开,该文件定义了一个函数,该函数,参数为 pkgs 默认会拿系统中的 nixpkgs,返回一个 derivation 的返回值。
{pkgs ? import <nixpkgs> { } }:
let
derivation = builtins.derivation;
# pkgs = import <nixpkgs> {};
# 从 github 中获取示例项目的源码,会存储到 /nix/store 中的一个子目录中。source 的值是一个指向这个子目录的路径。
source = fetchGit {
name = "learn-nix-demo-source";
url = "https://github.com/rectcircle/learn-nix-demo.git";
rev = "7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326";
ref = "master";
shallow = true;
};
in
derivation {
# 由于 go 项目是跨平台的,所以,这里直接使用 builtins.currentSystem,表示支持任意平台。
system = builtins.currentSystem;
name = "my-nix-package-demo-0.0.1";
# 会启动 nixpkgs 的 bash 来构建项目。
builder = "${pkgs.bash}/bin/bash";
# 额外的环境变量,会传递到 builder 进程。
A = "1";
# bash 命令的参数。即 bash -c 脚本 。
args = [ "-c"
# 在这个脚本,观察下,nix 如何设置这个脚本的环境变量,以及文件系统,参见输出。
''
set -e
${pkgs.coreutils}/bin/echo ">>> export -p" && export -p && echo
echo ">>> export PATH=${pkgs.go_1_19}/bin:${pkgs.bash}/bin:${pkgs.coreutils}/bin" && export PATH="${pkgs.go_1_19}/bin:${pkgs.bash}/bin:${pkgs.coreutils}/bin" && echo
echo ">>> pwd" && pwd && echo
echo ">>> id" && id && echo
echo ">>> ls -al /" && ls -al / && echo
echo ">>> ls -al /bin" && ls -al /bin && echo
echo ">>> ls -al /build" && ls -al /build && echo
echo ">>> ls -al /nix/store" && ls -al /nix/store && echo
echo ">>> mkdir -p $out/bin" && mkdir -p $out/bin && echo
echo ">>> cd ${source}/nix-package-demo && CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./" && cd ${source}/nix-package-demo && CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./ && echo
echo ">>> ls -al $out/bin" && ls -al $out/bin && echo
''];
}测试和输出分析
执行 drv_path=$(nix-instantiate nix-lang-demo/12-derivation.nix) && echo "drv_path: $drv_path" && echo "drv: $()" && nix-store -r $drv_path && nix-store --read-log $drv_path 命令,输出可以分为三部分。
第一部分,nix-instantiate nix-lang-demo/12-derivation.nix 的执行,通过 echo "drv_path: $drv_path" 可以看出去,其将打印一个路径。
drv_path: /nix/store/svf3hf64w6sadkc0gdpbss7ql0cr6s3d-my-nix-package-demo-0.0.1.drv这个路径命名为 /nix/store/$hash-$name.drv。可以看出,nix-instantiate 会执行 nix-lang-demo/12-derivation.nix 表达式(如果该文件返回的是一个函数类型,则会使用 {} 再调用该函数)。并将结果到该路径。
.drv 文件是 nix 构建工具的输入,nix 会根据该文件的配置来执行构建(如有缓存,将直接拉取而跳过构建)。
第二部分, nix --extra-experimental-features nix-command show-derivation $drv_path 将使用 json 格式展示上一步产生的 .drv 文件。
{
"/nix/store/svf3hf64w6sadkc0gdpbss7ql0cr6s3d-my-nix-package-demo-0.0.1.drv": {
"args": [
"-c",
"set -e\n/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin/echo \">>> export -p\" && export -p && echo\n\necho \">>> export PATH=/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin\" && export PATH=\"/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin\" && echo\n\necho \">>> pwd\" && pwd && echo\necho \">>> id\" && id && echo\necho \">>> ls -al /\" && ls -al / && echo\necho \">>> ls -al /bin\" && ls -al /bin && echo\necho \">>> ls -al /build\" && ls -al /build && echo\necho \">>> ls -al /nix/store\" && ls -al /nix/store && echo\n\necho \">>> mkdir -p $out/bin\" && mkdir -p $out/bin && echo\necho \">>> cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo && CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./\" && cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo && CGO_ENABLED=0 go build -o $out/bin/my-nix-package-demo ./ && echo\n\necho \">>> ls -al $out/bin\" && ls -al $out/bin && echo\n"
],
"builder": "/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash",
"env": {
"A": "1",
"builder": "/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash",
"name": "my-nix-package-demo-0.0.1",
"out": "/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1",
"system": "x86_64-linux"
},
"inputDrvs": {
"/nix/store/c65s9ncxdkfcijaxn6c9gglcw1zyaapx-go-1.19.6.drv": [
"out"
],
"/nix/store/czc8ym3wasmrsnwvlxzavxlfpfi2zg65-bash-5.2-p15.drv": [
"out"
],
"/nix/store/psc5y2s3prwxf1ph760nd7n1978s4411-coreutils-9.1.drv": [
"out"
]
},
"inputSrcs": [
"/nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source"
],
"outputs": {
"out": {
"path": "/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1"
}
},
"system": "x86_64-linux"
}
}重点关注,如下字段:
inputDrvsnix 会分析,我们的 nix 代码,分析我们是否引用了其他推导。本例中,我们在 builder 中使用了${pkgs.bash}、在 args 中使用了${pkgs.go_1_19}、${pkgs.bash}、${pkgs.coreutils}。因此, nix 识别出这些依赖,添加到了该字段。在nix-instantiate执行过程中,这些依赖就会被构建完成。inputSrcsnix 会分析,我们的 nix 代码,分析我们是否引用了其他路径。本例中,我们在 args 中引用 fetchGit 获取到的source路径。因此 nix 识别出了这些依赖,添加到了该字段。在nix-instantiate执行过程中,这些依赖就会被获取完成。env字段中包含了A,说明声明中的A属性被加到了环境变量中。此外outputs.out也被加到了环境变量中。outputs可以看出 outputs 目录,已经被创建出来。
此外,需要强调的是:
- 所有的路径都在
/nix/store目录中。nix 不会依赖除了 /nix/store 之外的其他目录,这保证了 nix 函数式不可变的特性。 - 可以看出
outputs.out目录的 hash 值在编译执行之前就确定,从该特性可以看出,nix 的 hash 是由 nix 代码的执行情况决定的,而不是文件内容的 hash。这保证了,同样的 nix 代码生成的各种目录都是一致的。基于这一点 nix 才能实现二进制缓存。
第三部分:nix-store -r $drv_path && nix-store --read-log $drv_path 根据 .drv 进行编译(对应目录不存在的话),然后打印出输出。
/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1
>>> export -p
declare -x A="1"
declare -x HOME="/homeless-shelter"
declare -x NIX_BUILD_CORES="4"
declare -x NIX_BUILD_TOP="/build"
declare -x NIX_LOG_FD="2"
declare -x NIX_STORE="/nix/store"
declare -x OLDPWD
declare -x PATH="/path-not-set"
declare -x PWD="/build"
declare -x SHLVL="1"
declare -x TEMP="/build"
declare -x TEMPDIR="/build"
declare -x TERM="xterm-256color"
declare -x TMP="/build"
declare -x TMPDIR="/build"
declare -x builder="/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash"
declare -x name="my-nix-package-demo-0.0.1"
declare -x out="/nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1"
declare -x system="x86_64-linux"
>>> export PATH=/nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6/bin:/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin:/nix/store/bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1/bin
>>> pwd
/build
>>> id
uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld),65534(nogroup)
>>> ls -al /
total 32
drwxr-x--- 9 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-x--- 9 nixbld nixbld 4096 Mar 12 07:27 ..
drwxr-xr-x 2 nixbld nixbld 4096 Mar 12 07:27 bin
drwx------ 2 nixbld nixbld 4096 Mar 12 07:27 build
drwxr-xr-x 4 nixbld nixbld 4096 Mar 12 07:27 dev
dr-xr-xr-x 2 nixbld nixbld 4096 Mar 12 07:27 etc
drwxr-xr-x 3 nixbld nixbld 4096 Mar 12 07:27 nix
dr-xr-xr-x 194 nobody nogroup 0 Mar 12 07:27 proc
drwxrwxrwt 2 nixbld nixbld 4096 Mar 12 07:27 tmp
>>> ls -al /bin
total 224
drwxr-xr-x 2 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-x--- 9 nixbld nixbld 4096 Mar 12 07:27 ..
-r-xr-xr-x 1 nixbld nixbld 217776 Jan 1 1970 sh
>>> ls -al /build
total 8
drwx------ 2 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-x--- 9 nixbld nixbld 4096 Mar 12 07:27 ..
>>> ls -al /nix/store
total 68
drwxrwxr-t 17 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-xr-x 3 nixbld nixbld 4096 Mar 12 07:27 ..
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 2w4k8nvdyiggz717ygbbxchpnxrqc6y9-gcc-12.2.0-lib
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15
dr-xr-xr-x 3 nixbld nixbld 4096 Jan 1 1970 633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6
dr-xr-xr-x 6 nixbld nixbld 4096 Jan 1 1970 76l4v99sk83ylfwkz8wmwrm4s8h73rhd-glibc-2.35-224
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 9zbi407givkvv1m0bd0icwcic3b3q24y-mailcap-2.1.53
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 bg8f47vihykgqcgblxkfk9sbvc4dnksa-coreutils-9.1
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 bw9s084fzmb5h40x98mfry25blj4cr9r-acl-2.3.1
dr-xr-xr-x 3 nixbld nixbld 4096 Jan 1 1970 bx5ikpp0p8nx88xdldkx16w3k3jzd2qc-busybox-static-x86_64-unknown-linux-musl-1.36.0
dr-xr-xr-x 3 nixbld nixbld 4096 Jan 1 1970 dg8213bqr29hg180gf4ypcj2vvzw4fl3-tzdata-2022g
dr-xr-xr-x 5 nixbld nixbld 4096 Jan 1 1970 jn9kg98dsaajx4mh95rb9r5rf2idglqh-attr-2.5.1
dr-xr-xr-x 3 nixbld nixbld 4096 Jan 1 1970 jvl8dr21nrwhqywwxcl8di4j55765gvy-gmp-with-cxx-stage4-6.2.1
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 lg2skbyyn1d7nkczqjz8mms38z4nhj2b-iana-etc-20221107
dr-xr-xr-x 3 nixbld nixbld 4096 Jan 1 1970 qmnr18aqd08zdkhka695ici96k6nzirv-libunistring-1.0
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 vv6rlzln7vhxk519rdsrzmhhlpyb5q2m-libidn2-2.3.2
dr-xr-xr-x 4 nixbld nixbld 4096 Jan 1 1970 zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source
>>> mkdir -p /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin
>>> cd /nix/store/zjii7ls858zb1qw0mi2v3rd7xg780fav-learn-nix-demo-source/nix-package-demo && CGO_ENABLED=0 go build -o /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin/my-nix-p>
>>> ls -al /nix/store/rqj3xxlzw7i9iwqqw2xafj9ykv4gy1zh-my-nix-package-demo-0.0.1/bin
total 1796
drwxr-xr-x 2 nixbld nixbld 4096 Mar 12 07:27 .
drwxr-xr-x 3 nixbld nixbld 4096 Mar 12 07:27 ..
-rwxr-xr-x 1 nixbld nixbld 1827660 Mar 12 07:27 my-nix-package-demoexport -p可以看出,nix builder 中的执行环境是一个和操作系统完全隔离的干净的环境。其中:HOME="/homeless-shelter"、PATH="/path-not-set"只是一个占位符。- 上文
.drv中的环境变量都正确的注入了。 pwd、TMP、TEMPDIR都在/build目录。
id可以看出,nix 创建了一个构建用的用户1000(nixbld)。ls -al /可以看出,nix 应该利用了 Linux 的 Mount 和 User namespace 实现的构建隔离。
恢复现场
nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -dderivation 和 stdenv.mkDerivation
这里可以看出,写一个 derivation 启动一个 bash 还是比较麻烦的,最麻烦的是,需要手动设置 PATH 环境变量。
为此,nixpkgs 封装了一个便捷的函数 stdenv.mkDerivation,该函数就是对 derivation 的封装,提供了更友好的编程接口。
因此,在实践中,一般使用 stdenv.mkDerivation 来定义一个推导。
关于 stdenv.mkDerivation 参见下文 nixpkgs 分析。
常见 shell.nix 分析
上一篇文章,我们使用 shell.nix 定义了一个项目的开发依赖。代码 nix-package-demo/shell.nix 如下所示:
# { pkgs ? import <nixpkgs> { } }:
let
pkgs = import ( builtins.fetchTarball {
url = "https://mirrors.tuna.tsinghua.edu.cn/nix-channels/releases/nixpkgs-unstable%40nixpkgs-23.05pre460011.f5ffd578778/nixexprs.tar.xz";
}) {};
in
pkgs.mkShell {
buildInputs =
[
pkgs.curl
pkgs.jq
pkgs.go
pkgs.which
];
shellHook = ''
export TEST_ENV_VAR=ABC
'';
}执行命令 drv_path=$(nix-instantiate nix-package-demo/shell.nix) && echo "drv_path: $drv_path" && echo "drv: $(nix --extra-experimental-features nix-command show-derivation $drv_path)" 可以看到输出如下:
drv_path: /nix/store/wwmkmm2wvfjh5jh5mhb0anxqpz4s26cx-nix-shell.drv
drv: {
"/nix/store/wwmkmm2wvfjh5jh5mhb0anxqpz4s26cx-nix-shell.drv": {
"args": [
"-e",
"/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh"
],
"builder": "/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash",
"env": {
"__structuredAttrs": "",
"buildInputs": "/nix/store/gd51gknpxqaxyd0gycmszm8ckrvwvs0l-curl-7.88.0-dev /nix/store/7paksrb0nbm7q9x7rzzabqlgjci9rx8k-jq-1.6-dev /nix/store/633qlvqjryvq0h43nwvzkd5vqxh2rh3c-go-1.19.6 /nix/store/v0g0r8khhdxn8gwcx3yg57wmndzfdgz5-which-2.21",
"buildPhase": "{ echo \"------------------------------------------------------------\";\n echo \" WARNING: the existence of this path is not guaranteed.\";\n echo \" It is an internal implementation detail for pkgs.mkShell.\";\n echo \"------------------------------------------------------------\";\n echo;\n # Record all build inputs as runtime dependencies\n export;\n} >> \"$out\"\n",
"builder": "/nix/store/5ynbf6wszmggr0abwifdagrixgnya5vy-bash-5.2-p15/bin/bash",
"cmakeFlags": "",
"configureFlags": "",
"depsBuildBuild": "",
"depsBuildBuildPropagated": "",
"depsBuildTarget": "",
"depsBuildTargetPropagated": "",
"depsHostHost": "",
"depsHostHostPropagated": "",
"depsTargetTarget": "",
"depsTargetTargetPropagated": "",
"doCheck": "",
"doInstallCheck": "",
"mesonFlags": "",
"name": "nix-shell",
"nativeBuildInputs": "",
"out": "/nix/store/2zx26yarglz5wqbkl6mqbaxqfyinrixn-nix-shell",
"outputs": "out",
"patches": "",
"phases": "buildPhase",
"preferLocalBuild": "1",
"propagatedBuildInputs": "",
"propagatedNativeBuildInputs": "",
"shellHook": "export TEST_ENV_VAR=ABC\n",
"stdenv": "/nix/store/c3f4jdwzn8fm9lp72m91ffw524bakp6v-stdenv-linux",
"strictDeps": "",
"system": "x86_64-linux"
},
"inputDrvs": {
"/nix/store/65wj1fwk5f3wncd1j3dmk29k3nzghl8d-which-2.21.drv": [
"out"
],
"/nix/store/c65s9ncxdkfcijaxn6c9gglcw1zyaapx-go-1.19.6.drv": [
"out"
],
"/nix/store/czc8ym3wasmrsnwvlxzavxlfpfi2zg65-bash-5.2-p15.drv": [
"out"
],
"/nix/store/r7wldahsa6maa0m7nnjf82azcy4g8hdh-jq-1.6.drv": [
"dev"
],
"/nix/store/saw3hgzcr6lsy051kclm3y7kif8b4i6h-curl-7.88.0.drv": [
"dev"
],
"/nix/store/xjk0c9yw2i25xr08ngk60bc47q9fw2jd-stdenv-linux.drv": [
"out"
]
},
"inputSrcs": [
"/nix/store/6xg259477c90a229xwmb53pdfkn6ig3g-default-builder.sh"
],
"outputs": {
"out": {
"path": "/nix/store/2zx26yarglz5wqbkl6mqbaxqfyinrixn-nix-shell"
}
},
"system": "x86_64-linux"
}
}因此 mkShell 本质上就是创建了一个包含了声明依赖的 derivation。
而 nix-shell 的流程就是,先调用 nix-instantiate nix-package-demo/shell.nix,生成一个 .drv 文件,然后根据该文件配置,启动一个 Shell。
nixpkgs 分析
参考: nixpkgs github | nixpkgs 手册 |
基于上面的背景知识,nixpkgs 和 nix channel 的原理可以很容易的立即。
nixpkgs 本质上就是一个 nix 代码库,该库主要包含如下两类内容:
- 一些对 nix 原生能力进行易用化封装的函数,如
mkShell、stdenv.mkDerivation。 - 包含了开源世界 80000+ 个软件包的
derivation声明。
可以通过 nixpkgs.hello 的源码(pkgs/applications/misc/hello/default.nix),以及生成的 .drv 来了解,如何通过 stdenv.mkDerivation 来定一个软件包的 derivation。
nix-env -iA nixpkgs.hello
ls -al /nix/store/*-hello-*.drv
nix --extra-experimental-features nix-command show-derivation /nix/store/7ky0zmis8b384k5sx852i0fq7x9ir2jl-hello-2.12.1.drv这里重点介绍一下,nixpkgs 的 stdenv.mkDerivation 的属性集参数的一些重要属性(详见:Chapter 6. The Standard Environment)。
pname包名。version包版本。最终对应 derivation name 为"${pname}-${version}"。src源代码路径一般等于 fetch 相关函数调用。在脚本可以通过src环境变量获取到。nativeBuildInputs声明在编译时依赖的其他包(derivation),如 go 编译器,git 等。buildInputs声明在运行时依赖的其他包(derivation),如 glibc 等,为了支持交叉编译,还有大量depsXxx相关属性,不太理解。passthru该属性目前主要用户测试,该字段的变更不会影响.drv文件的生成,不会影响 hash 的生成。xxxPhase该函数会执行位于pkgs/stdenv/generic/setup.sh中的genericBuild函数,该函数将构建过程分成了很多各阶段。如果项目使用 autotools 来管理编译过程,则一般不用修改该类字段。如果项目中没有提供 Makefile 则需要手动提供buildPhase、installPhase脚本。支持的所有阶段如下($开头的表示默认没有实现):$prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases
针对各种不同的编程语言和框架, nixpkgs 也提供了对应的便捷函数,如 buildGoModule,本文不多赘述,详见:Chapter 17. Languages and frameworks。
注意:不管是 nativeBuildInputs 还是 buildInputs,即使其产物在 Cache 中存在,也都会自动下载下来。似乎并不存在 build-only 方式的声明,参见: issue。
自定义 channel
根据 nixpkgs 分析章节,做一个自定义 channel 会非常的简单。
上文,推导(derivation)章节的示例已经定义了一个包了,下面我们使用同样的示例代码,定义两个包。
第一个包,使用 stdenv.mkDerivation 函数定义,nix-lang-demo/13-mkderivation.nix。
{pkgs ? import <nixpkgs> { } }:
let
stdenv = pkgs.stdenv;
in
stdenv.mkDerivation {
pname = "my-nix-package-demo-build-by-my-mk-derivation";
version = "0.0.1";
src = fetchGit {
name = "learn-nix-demo-source";
url = "https://github.com/rectcircle/learn-nix-demo.git";
rev = "7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326";
ref = "master";
shallow = true;
};
nativeBuildInputs = [ pkgs.go_1_19 pkgs.git ];
buildPhase = ''
cd nix-package-demo && CGO_ENABLED=0 go build -o $pname ./
'';
installPhase = ''
mkdir -p $out/bin
cp $pname $out/bin
'';
}第二个包,使用 buildGoModule 函数定义,nix-lang-demo/14-build-go-module.nix。
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/go/module.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.buildGoModule {
pname = "my-nix-package-demo-by-build-go-module";
version = "0.0.1";
src = fetchGit {
name = "learn-nix-demo-source";
url = "https://github.com/rectcircle/learn-nix-demo.git";
rev = "7f4952a6ecf7dcd90c8bb0c8d14795ae1add5326";
ref = "master";
shallow = true;
};
vendorHash = null; # 自动生成。
modRoot = "./nix-package-demo";
CGO_ENABLED = false;
postInstall = ''
mv $out/bin/nix-package-demo $out/bin/$pname
'';
}现在定义这个 channel 的 ./default.nix。
# nix-env -qaP -f ./
# nix-env -iA my-nix-package-demo_0_0_1 -f ./
# nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
# nix-env -iA my-nix-package-demo-build-by-my-mk-derivation_0_0_1 -f ./
# nix-env -e my-nix-package-demo-build-by-my-mk-derivation-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
# nix-env -iA my-nix-package-demo-by-build-go-module_0_0_1 -f ./
# nix-env -e my-nix-package-demo-by-build-go-module-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
{ pkgs ? import <nixpkgs> { } }:
{
my-nix-package-demo_0_0_1 = import ./nix-lang-demo/12-derivation.nix { inherit pkgs; };
my-nix-package-demo-build-by-my-mk-derivation_0_0_1 = import ./nix-lang-demo/13-mkderivation.nix { inherit pkgs; };
my-nix-package-demo-by-build-go-module_0_0_1 = import ./nix-lang-demo/14-build-go-module.nix { inherit pkgs; };
}此时,通过 nix-env -qaP -f ./ 即可像 nixpkgs 一样列出这个 channel 的三个包。
my-nix-package-demo_0_0_1 my-nix-package-demo-0.0.1
my-nix-package-demo-build-by-my-mk-derivation_0_0_1 my-nix-package-demo-build-by-my-mk-derivation-0.0.1
my-nix-package-demo-by-build-go-module_0_0_1 my-nix-package-demo-by-build-go-module-0.0.1可以使用如下命令安装卸载。
nix-env -iA my-nix-package-demo_0_0_1 -f ./
nix-env -e my-nix-package-demo-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
nix-env -iA my-nix-package-demo-build-by-my-mk-derivation_0_0_1 -f ./
nix-env -e my-nix-package-demo-build-by-my-mk-derivation-0.0.1 ; nix-collect-garbage -d # 彻底卸载。
nix-env -iA my-nix-package-demo-by-build-go-module_0_0_1 -f ./
nix-env -e my-nix-package-demo-by-build-go-module-0.0.1 ; nix-collect-garbage -d # 彻底卸载。其他说明
纯函数性
最后,讨论一下 nix 语言如何保证 nix 工具是一个纯函数包管理工具。
首先,纯函数指的是没有副作用的函数,也就是说,对于同一个参数的多次调用,一个纯函数可以保证,其返回值永远不变,且不会对外部世界产生任何影响。
从语法上看,nix 所有的语法、操作符都是纯函数性的。但是由于 nix 语言定义的是编译的过程,必然要涉及文件系统和网络相关的操作,如从 github 下载代码、读取文件、将编译产物写入文件。显然包含这些操作就的函数就不是纯函数了。
针对这种情况,nix 的解决办法是,所有对于路径的操作,nix 会根据固定的规则生成一个位于 /nix/store 的路径。如果是输入类路径,会将文件拷贝到这个位置。
关键在于这个路径个规则。由于 nix 除了路径和网络下载之外的所有操作都是纯函数的,因此 nix 代码不管运行多少次,到了需要处理目录的地方,其运行状态一定是完全一致,因此 nix 就可以根据运行状态生成一个 hash,并结合路径名生成该路径。这样,在狭义上 nix 并非纯函数,但是在逻辑上,却达到了纯函数的效果。
由于 nix 有了纯函数的保证,那么这些路径的操作就是可以被缓存的。这样,在配合二进制缓存,nix 的安装速度可以做到非常快。
这种机制,对纯函数性的保证实际上比较脆弱,如下的场景可能破坏 nix 的纯函数性,带来不可重现的问题。
- 对于 fetchGit 可以利用 git 的
rev机制,可以保证纯函数性。对于fetchTarball可以使用sha256保证纯函数性。但是对于fetchurl则无法保证纯函数型(因此在严格评估模式 restricted evaluation mode下,该函数是不可用的)。 - 在使用 derivation 中,总是会调用 shell 来执行命令,而 shell 是无法保证纯函数性的,例如用户在 shell 脚本中使用 curl 来下载内容,且没有校验和处理异常,则会破坏 nix 的纯函数性。
因此,在开发一个 nix 包时,如果要保证纯函数性,则要求:
- 不要使用
fetchurl。 - 在编写 shell 脚本时,不要使用 curl 下载内容,时刻注意该 shell 脚本是否是可重现的。
各种 Name
至此,当我们要安装一个包时,我们会遇到好几种 Name,在这里总结下这些 Name 之间的关系。
derivation_name即这个包的名字:- 定义位置:在调用
derivation函数时,传递的name属性。 - 使用位置:
- 如果
derivation没有配置 outputs 时(采用默认值["out"]),则该 out 为/nix/sotre/$hash_$derivation_name。 - 使用
nix-env -e $derivation_name删除包时。 - 查询包
nix-env -qaP输出的第二列。
- 如果
- 定义位置:在调用
pname在 nixpkgs 的包名:- 定义位置:在调用
stdenv.mkDerivation函数是,传递的pname属性。 - 使用位置:
stdenv.mkDerivation函数传递给 derivation 函数的 name 时,传递的是$pname-$version。也就是说:$derivation_name=$pname-$version。
- 定义位置:在调用
attr_name执行一个default.nix后产生属性集中的属性名。- 定义位置:
default.nix中最终返回的属性机中。 - 使用位置:
- 查询包
nix-env -qaP输出的第一列,格式为$channel_name.$attr_name。 - 查询包
nix-env -qaP -f path/to/channel输出的第一列,格式为$attr_name。 - 安装包
nix-env -iA $channel_name.$attr_name。 - 安装包
nix-env -iA $attr_name -f path/to/channel。
- 查询包
- 定义位置:
包存储结构
传统的 Unix 包存储结构规范是 FHS 。这个规范有如下特点:
- 这个规范并不是强制的,可以遵循可以不遵循。
- 没有版本的概念,同一个包的不同版本会相互覆盖。
- 一个包的各个组成部分,在不同个目录。比如 so 文件在 /usr/lib,可执行文件在 /usr/bin。
而 Nix 的包并不遵循 FHS 规范,Nix 的包有如下特点:
- Nix 包存储结构是强制,是有 Nix 工具生成和维护的。
- 有版本的概念,同一个包的不同版本存储在不同的目录。
- 一个包的所有文件都存储在和其他包隔离的自己的目录中。
下面是来自 Nix 论文 的包存储结构和依赖关系示意图。

- Nix 的所有包都存储在
/nix/store下的目录中,这个目录的格式为$hash_$derivation_name$hash:nix 包都是通过 nix 语言定义的,由于 nix 语言的纯函数性,因此对于每个 nix 包的制品的存储目录(在编译过程中成为 out 目录),生成的唯一的 hash 值。这个 hash 值存在的目的是,当该包的依赖变了的情况下,这个包虽然包名和版本号没变,但是其内容已经变了,这个包已经不是之前的包了,为了不可变性,这个 hash 也会变化。$derivation_name: 由包名和版本号构成。
- 上图还展示了 Nix 包的依赖关系,这个关系在编译时,根据 Nix 语言包声明的依赖关系就决定了。
- 在传统的 Linux 发行版中(符合 FHS 规范),像 libc 这种最常见的动态链接库都是存储在固定的路径中的如
/lib/x86_64-linux-gnu。如果一个包是通过源码编译,自然没有问题,在编译时 libc 也自动的被配置到对应的/nix/store/xxx-glibc-xxx/lib目录中。但是,某些专有软件并没有提供源码,此时这类软件的编译过程变为:下载常规 Linux 版本可执行文件,然后通过 patchelf 工具修改 ld-linux.so 到/nix/store/xxx-glibc-xxx/lib路径即可,详见:wiki。