Chapter 03

Nix 语言入门

学习专为包管理设计的纯函数式语言——Nix 语言的语法小巧但足够强大

Nix 语言特性概览

Nix 语言是为描述包构建而设计的领域特定语言(DSL),具有以下特点:

纯函数式
没有可变状态,没有副作用,函数是一等公民。相同的表达式永远求值为相同的结果。
惰性求值
表达式只在需要时才被计算。引用了 Nixpkgs 中 80000+ 个包的表达式,你只请求其中一个时,其他包的 derivation 不会被计算。
动态类型
变量类型在运行时确定,无需类型声明。但类型错误会在求值时报错,而非编译期。
不是通用语言
Nix 语言没有 I/O 操作(无法读写文件、无法执行命令),专为描述构建计划设计。实际的构建步骤在 bash 脚本中执行。

基本类型

# 使用 nix repl 交互式学习:运行 nix repl 进入

# 字符串
"hello world"               # 普通字符串
''
  多行字符串
  开头的缩进会被自动去除
''
let name = "Nix"; in "Hello, ${name}!"   # 字符串插值:Hello, Nix!
"path: ${/etc/hosts}"        # 路径插值

# 数字
42                           # 整数
3.14                         # 浮点数
1 + 2                        # 运算:3
10 / 2                       # 注意:整数除法,结果为 5

# 布尔值
true
false
true && false               # false
true || false               # true
!true                        # false

# null
null

# 路径(不加引号的路径,Nix 会自动解析)
/etc/nixos                   # 绝对路径
./relative/path              # 相对路径(相对于当前 .nix 文件)
<nixpkgs>                   # 尖括号路径,从 NIX_PATH 中查找

数据结构

Attribute Set(属性集)

Attrset 是 Nix 最核心的数据结构,类似其他语言的 Map/Dictionary:

# 基本 attrset
{ name = "Alice"; age = 30; }

# 访问属性
let person = { name = "Alice"; age = 30; }; in
person.name                   # "Alice"

# 带默认值的属性访问(or 关键字)
person.email or "unknown"   # "unknown"(email 属性不存在时)

# 嵌套 attrset
{
  services.nginx.enable = true;   # 等价于下面的写法
}
# 等价于:
{
  services = {
    nginx = {
      enable = true;
    };
  };
}

# 递归 attrset(rec):属性可以引用同一集合中的其他属性
rec {
  x = 10;
  y = x * 2;   # y = 20,可以引用 x
}

# 合并两个 attrset(// 运算符,右侧优先)
{ a = 1; b = 2; } // { b = 99; c = 3; }
# 结果:{ a = 1; b = 99; c = 3; }

List(列表)

# 列表(用空格分隔,不是逗号!)
[ 1 2 3 ]
[ "a" "b" "c" ]
[ true false null ]

# 列表连接
[ 1 2 ] ++ [ 3 4 ]   # [ 1 2 3 4 ]

# 列表索引(从 0 开始)
builtins.elemAt [ "a" "b" "c" ] 1   # "b"

# 常用 builtins 列表操作
builtins.length [ 1 2 3 ]         # 3
builtins.head [ 1 2 3 ]            # 1
builtins.tail [ 1 2 3 ]            # [ 2 3 ]
builtins.map (x: x * 2) [ 1 2 3 ]  # [ 2 4 6 ]
builtins.filter (x: x > 2) [ 1 2 3 4 ]  # [ 3 4 ]

函数

Nix 的函数语法简洁,所有函数都是单参数的(多参数通过柯里化实现):

# 最简单的函数:参数: 函数体
x: x + 1         # 一个将输入加 1 的匿名函数

# 调用函数(函数 空格 参数,不是圆括号!)
(x: x + 1) 5      # 6

# 用 let 绑定函数名
let
  double = x: x * 2;
  add = x: y: x + y;   # 多参数:柯里化(返回另一个函数)
in
double 21           # 42
add 3 4             # 7,等价于 (add 3) 4

# 解构 attrset 参数(Nix 最常用的函数形式)
{ a, b }: a + b    # 接受包含 a 和 b 的 attrset
{ a, b ? 0 }: a + b  # b 有默认值 0
{ a, ... }: a       # ... 允许额外属性存在(常见于模块系统)

# 实际调用解构函数
(let f = { name, age ? 0 }: "${name} is ${toString age}"; in
  f { name = "Alice"; age = 30; })
# "Alice is 30"

let…in、with 与 import

# let...in:局部变量绑定
let
  x = 10;
  y = 20;
  sum = a: b: a + b;
in
  sum x y       # 30

# with:将 attrset 的所有属性引入作用域
with { a = 1; b = 2; }; a + b   # 3

# 实际用途:with pkgs; 避免重复写 pkgs.
with pkgs; [ git vim curl ]
# 等价于:
[ pkgs.git pkgs.vim pkgs.curl ]

# import:导入另一个 .nix 文件(返回该文件的值)
import ./other.nix             # 导入同目录下的 other.nix
import <nixpkgs> {}            # 导入 nixpkgs 并传入空配置

# if…then…else(表达式,不是语句)
if true then "yes" else "no"   # "yes"
let x = 5; in if x > 3 then "big" else "small"  # "big"

# assert:断言(失败时终止求值并报错)
assert 1 + 1 == 2; "ok"         # "ok"

builtins:内置函数

# 类型转换
builtins.toString 42            # "42"
builtins.toJSON { a = 1; }      # '{"a":1}'
builtins.fromJSON '{"a":1}'     # { a = 1; }
builtins.typeOf 42              # "int"
builtins.typeOf "hello"         # "string"
builtins.typeOf []               # "list"
builtins.typeOf {}               # "set"

# 字符串操作
builtins.stringLength "hello"   # 5
builtins.substring 0 3 "hello"  # "hel"
builtins.replaceStrings ["a"] ["b"] "cat"  # "cbt"
builtins.match "(.*)\\.nix" "hello.nix"   # [ "hello" ]

# attrset 操作
builtins.attrNames { b = 2; a = 1; }  # [ "a" "b" ](排序)
builtins.hasAttr "x" { x = 1; }        # true
builtins.getAttr "x" { x = 42; }       # 42
builtins.removeAttrs { a=1; b=2; } ["b"]  # { a = 1; }

# 文件获取(构建时用)
builtins.fetchurl {
  url = "https://example.com/file.tar.gz";
  sha256 = "abc123...";   # 必须提供哈希,保证可复现
}

# 从 GitHub 获取源码
builtins.fetchGit {
  url = "https://github.com/user/repo";
  rev = "abc123def456";   # 锁定具体 commit
}

写第一个 Derivation

Derivation 是 Nix 构建包的核心原语,用 derivation{}(低级)或 stdenv.mkDerivation{}(高级)创建:

# 使用高级 API:stdenv.mkDerivation
# 文件:hello-custom.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation {
  pname = "hello-custom";   # 包名
  version = "1.0.0";

  # 源码来源
  src = pkgs.fetchurl {
    url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
    sha256 = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yQ=";
  };

  # 构建依赖(build 阶段用)
  nativeBuildInputs = [ pkgs.autoconf pkgs.automake ];

  # 运行时依赖(产物包含的库)
  buildInputs = [];

  # 自定义构建步骤(默认:configure + make + make install)
  buildPhase = ''
    make
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp hello $out/bin/
  '';

  # 元数据(可选)
  meta = with pkgs.lib; {
    description = "A program that produces a familiar, friendly greeting";
    license = licenses.gpl3Plus;
    platforms = platforms.all;
  };
}
# 构建这个 derivation
nix-build hello-custom.nix
# 输出:/nix/store/abc123xyz-hello-custom-1.0.0

# 运行构建结果
./result/bin/hello-custom
# 输出:Hello, world!

# 查看构建产物在 store 中的路径
ls -la result       # result 是指向 /nix/store/... 的符号链接

nix repl 调试技巧

# 进入交互式 Nix REPL
nix repl

# 在 REPL 中:
nix-repl> 1 + 2                  # 3
nix-repl> let x = 5; in x * 2   # 10
nix-repl> :l <nixpkgs>          # 加载 nixpkgs(需要几秒)
nix-repl> pkgs.hello             # 查看 hello 包的 derivation
nix-repl> pkgs.hello.version     # "2.12.1"
nix-repl> :t pkgs.hello          # 显示类型
nix-repl> :q                     # 退出
本章小结

Nix 语言的语法比较小巧:掌握 attrset({})、list([])、函数(x: ...)、解构函数({ a, b }: ...)、let...inwithimport 这几个构造,就足以读懂 99% 的 Nix 配置。下一章开始将这些知识用于实际的开发环境配置。