Skip to content

使用 Flakes 来管理你的 NixOS

启用 NixOS 的 Flakes 支持

与 NixOS 当前默认的配置方式相比,Flakes 提供了更好的可复现性,同时它清晰的包结构定义原生支 持了以其他 Git 仓库为依赖,便于代码分享,因此本书更建议使用 Flakes 来管理系统配置。

目前 Flakes 作为一个实验特性,仍未被默认启用,我们需要手动修改 /etc/nixos/configuration.nix 文件,启用 Flakes 特性以及配套的船新 nix 命令行工具:

nix
{ config, pkgs, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  # ......

  # 启用 Flakes 特性以及配套的船新 nix 命令行工具
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  environment.systemPackages = with pkgs; [
    # Flakes 通过 git 命令拉取其依赖项,所以必须先安装好 git
    git
    vim
    wget
    curl
  ];
  # 将默认编辑器设置为 vim
  environment.variables.EDITOR = "vim";

  # ......
}

然后运行 sudo nixos-rebuild switch 应用修改后,即可使用 Flakes 特性来管理系统配置。

nix 的新命令行工具还提供了一些方便的功能,比如说你现在可以使用 nix repl 命令打开一个 nix 交互环境,有兴趣的话,可以使用它复习测试一遍前面学过的所有 Nix 语法。

将系统配置切换到 flake.nix

在启用了 Flakes 特性后,sudo nixos-rebuild switch 命令会优先读取 /etc/nixos/flake.nix 文件,如果找不到再尝试使用 /etc/nixos/configuration.nix

可以首先使用官方提供的模板来学习 flake 的编写,先查下有哪些模板:

bash
nix flake show templates

其中有个 templates#full 模板展示了所有可能的用法,可以看看它的内容:

bash
nix flake init -t templates#full
cat flake.nix

我们参照该模板创建文件 /etc/nixos/flake.nix 并编写好配置内容,后续系统的所有修改都将全部 由 Nix Flakes 接管,示例内容如下:

nix
{
  description = "A simple NixOS flake";

  inputs = {
    # NixOS 官方软件源,这里使用 nixos-23.11 分支
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    # 因此请将下面的 my-nixos 替换成你的主机名称
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        # 这里导入之前我们使用的 configuration.nix,
        # 这样旧的配置文件仍然能生效
        ./configuration.nix
      ];
    };
  };
}

这里我们定义了一个名为 my-nixos 的系统,它的配置文件为 /etc/nixos/ 文件夹下的 ./configuration.nix,也就是说我们仍然沿用了旧的配置。

现在执行 sudo nixos-rebuild switch 应用配置,系统应该没有任何变化,因为我们仅仅是切换到 了 Nix Flakes,配置内容与之前还是一致的。

切换完毕后,我们就可以通过 Flakes 特性来管理系统了。

目前我们的 flake 包含这几个文件:

  • /etc/nixos/flake.nix: flake 的入口文件,执行 sudo nixos-rebuild switch 时会识别并部 署它。
  • /etc/nixos/flake.lock: 自动生成的版本锁文件,它记录了整个 flake 所有输入的数据源、hash 值、版本号,确保系统可复现。
  • /etc/nixos/configuration.nix: 这是我们之前的配置文件,在 flake.nix 中被作为模块导 入,目前所有系统配置都写在此文件中。
  • /etc/nixos/hardware-configuration.nix: 这是系统硬件配置文件,由 NixOS 生成,描述了系统 的硬件信息

到这里为止, /etc/nixos/flake.nix 仅仅是 /etc/nixos/configuration.nix 的一个 thin wrapper,它自身并没有提供任何新的功能,也没有引入任何破坏性的变更。在本书后面的内容中,我 们会逐渐看到这样一个 wrapper 带来了哪些好处。

注意:本书描述的配置管理方式并非「Everything in a single file」,更推荐将配置内容分门 别类地存放到不同的 nix 文件中,然后在 flake.nixmodules 参数列表中引入这些配置 文件,并通过 Git 管理它们。这样做的好处是,可以更好地组织配置文件,提高配置的可维护性。 后面的 模块化 NixOS 配置 一节将会详细介绍如何模块化 你的 NixOS 配置,其他实用技巧 - 使用 Git 管理 NixOS 配置 将会 介绍几种使用 Git 管理 NixOS 配置的最佳实践。

flake.nix 配置详解

上面我们创建了一个 flake.nix 文件并通过它来管理系统配置,但你对它的结构还是一头雾水,下 面我们来详细解释一下这个文件的内容。

1. flake inputs

首先看看其中的 inputs 属性,它是一个 attribute set,其中定义了这个 flake 的所有依赖项, 这些依赖项会在被拉取后,作为参数传递给 outputs 函数:

nix
{
  inputs = {
    # NixOS 官方软件源,这里使用 nixos-23.11 分支
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    # 省略掉前面的配置......
  };
}

inputs 中的每一项依赖有许多类型与定义方式,可以是另一个 flake,也可以是一个普通的 Git 仓 库,又或者一个本地路径。 Flakes 的其他玩法 - Flake 的 inputs 中详细介绍了常见 的依赖项类型与定义方式。

这里我们只定义了 nixpkgs 这一个依赖项,使用的是 flake 中最常见的引用方式,即 github:owner/name/reference,这里的 reference 可以是分支名、commit-id 或 tag。

nixpkgsinputs 中被定义后,就可以在后面的 outputs 函数的参数中使用此依赖项中的内 容了,我们的示例中正是这么干的。

2. flake outputs

再来看看 outputs,它是一个以 inputs 中的依赖项为参数的函数,函数的返回值是一个 attribute set,这个返回的 attribute set 即为该 flake 的构建结果:

nix
{
  description = "A simple NixOS flake";

  inputs = {
    # NixOS 官方软件源,这里使用 nixos-23.11 分支
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  # 这里的 `self` 是个特殊参数,它指向 `outputs` 函数返回的 attribute set 自身,即自引用
  outputs = { self, nixpkgs, ... }@inputs: {
    # hostname 为 my-nixos 的主机会使用这个配置
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
      ];
    };
  };
}

flake 有很多的用途,也可以有很多不同类型的 outputs,Flake 的 outputs 一节有更详细的介绍。这里 我们只用到了 nixosConfigurations 这一类型的 outputs,它用于配置 NixOS 系统。

在我们运行 sudo nixos-rebuild switch 命令时,它会从 /etc/nixos/flake.nixoutputs 函数返回值中查找 nixosConfigurations.my-nixos (其中的 my-nixos 将会是你当前系统的 hostname)这一属性,并使用其中的定义来配置你的 NixOS 系统。

实际我们也可以自定义 flake 的位置与 NixOS 配置的名称,而不是使用默认值。只需要在 nixos-rebuild 命令后面添加 --flake 参数即可,一个例子:

nix
sudo nixos-rebuild switch --flake /path/to/your/flake#your-hostname

上述命令中的 --flake /path/to/your/flake#your-hostname 参数简要说明如下:

  1. /path/to/your/flake 为目标 flake 的位置,默认会使用 /etc/nixos/ 这个路径。
  2. # 是一个分隔符,其后的 your-hostname 则是 NixOS 配置的名称。nixos-rebuild 默认会 以你当前系统的 hostname 为配置名称进行查找。

你甚至能直接引用一个远程的 GitHub 仓库作为你的 flake 来源,示例如下:

nix
sudo nixos-rebuild switch --flake github:owner/repo#your-hostname

3. nixpkgs.lib.nixosSystem 函数的简单介绍

一个 Flake 可以依赖其他 Flakes,从而使用它们提供的功能

默认情况下,一个 flake 会在其每个依赖项(即 inputs 中的每一项)的根目录下寻找 flake.nix 文件并懒惰求值(lazy evaluation)它们的 outputs 函数,接着将这些函数返回 的 attribute sets 作为参数传递给它自身的 outputs 函数,这样我们就能在当前 flake 中使用它 所依赖的其他 flakes 提供的功能了。

更精确地说,对每个依赖项的 outputs 函数的求值都是懒惰(lazy)的,也就是说,一个 flake 的 outputs 函数只有在被真正使用到的时候才会被求值,这样就能避免不必要的计算,从而提高效率。

上面的描述可能有点绕,我们还是结合本节中使用的 flake.nix 示例来看看这个过程。我们的 flake.nix 声明了 inputs.nixpkgs 这个依赖项,因此 nixpkgs/flake.nix 会在我们执行 sudo nixos-rebuild switch 这个命令时被求值。从 Nixpkgs 仓库的源码中能看到它的 flake outputs 定义中有返回 lib 这个属性,我们的例子中就使用了 lib 属性中的 nixosSystem 这 个函数来配置我们的 NixOS 系统:

nix
{
  inputs = {
    # NixOS 官方软件源,这里使用 nixos-23.11 分支
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
      ];
    };
  };
}

nixpkgs.lib.nixosSystem 后面跟的 attribute set 就是该函数的参数,我们这里只设置了两个参 数:

  1. system: 这个很好懂,就是系统架构参数。
  2. modules: 此函数是一个 modules 的列表,NixOS 的实际系统配置都定义在这些 modules 中。

/etc/nixos/configuration.nix 这个配置文件本身就是一个 Nixpkgs Module,因此可以直接将其添 加到 modules 列表中使用。

新手阶段了解这些就足够了,探究 nixpkgs.lib.nixosSystem 函数的具体实现需要对 Nixpkgs 的模 块系统有一定的了解。读者可以在学习了 模块化 NixOS 配置 一节后,再回过头来从 nixpkgs/flake.nix 中找到 nixpkgs.lib.nixosSystem 的定义,跟踪它的源码,研究其实现方 式。

Nixpkgs Module 结构的简单介绍

在后面的 模块化 NixOS 配置 一节中会详细介绍这套模块 系统的工作方式,这里只介绍些基础知识。

为什么 /etc/nixos/configuration.nix 这个配置文件会符合 Nixpkgs Module 定义,从而能直接在 flake.nix 中引用它呢?可能会有读者觉得这有点出乎意料。

这实际是因为 Nixpkgs 中包含了大量 NixOS 的实现源码,这些源码大都使用 Nix 语言编写。为了编 写维护如此多的 Nix 代码,并且使用户能灵活地自定义其 NixOS 系统的各项功能,就必须要有一套 Nix 代码的模块化系统。

这套 Nix 代码的模块系统的实现也同样在 Nixpkgs 仓库中,它主要被用于 NixOS 系统配置的模块 化,但也有其他的应用,比如 nix-darwin 跟 home-manager 都大量使用了这套模块系统。

既然 NixOS 是基于这套模块系统构建的,那它的配置文件(包括 /etc/nixos/configuration.nix) 是一个Nixpkgs Module,也就显得非常自然了。

在学习后面的内容之前,我们需要先简单了解下这套模块系统的工作方式。

一个简化的 Nixpkgs Module 结构如下:

nix
{lib, config, options, pkgs, ...}:
{
  # 导入其他 Modules
  imports = [
    # ......
    # ./xxx.nix
  ];

  for.bar.enable = true;
  # other options declarations
  # ...
}

可以看到它的定义实际是一个 Nix 函数,该函数有 5 个由模块系统自动生成、自动注入、无需额外 声明的参数

  1. lib: nixpkgs 自带的函数库,提供了许多操作 Nix 表达式的实用函数
  2. config: 包含了当前环境中所有 option 的值,在后面学习模块系统时会大量使用它
  3. options: 当前环境中所有 Modules 中定义的所有 options 的集合
  4. pkgs: 一个包含所有 nixpkgs 包的集合,它也提供了许多相关的工具函数
    • 入门阶段可以认为它的默认值为 nixpkgs.legacyPackages."${system}",可通过 nixpkgs.pkgs 这个 option 来自定义 pkgs 的值
  5. modulesPath: 一个只在 NixOS 中可用的参数,是一个 Path,指向 nixpkgs/nixos/modules

传递非默认参数到模块系统中

而如果你需要将其他非默认参数传递到子模块,就需要使用一些特殊手段手动指定这些非默认参数。

Nixpkgs 的模块系统提供了两种方式来传递非默认参数:

  1. nixpkgs.lib.nixosSystem 函数的 specialArgs 参数
  2. 在任一 Module 中使用 _module.args 这个 option 来传递参数

这两个参数的官方文档藏得很深,而且语焉不详、晦涩难懂。读者感兴趣的话我把链接放在这里:

  1. specialArgs: NixOS Manual 跟 Nixpkgs Manual 中分别有与它有关的只言片语
  2. _module.args:

总之,specialArgs_module.args 需要的值都是一个 attribute set,它们的功能也相同,都 是将其 attribute set 中的所有参数传递到所有子模块中。这两者的区别在于:

  1. 在任何 Module 中都能使用 _module.args 这个 option,通过它互相传递参数,这要比只能在 nixpkgs.lib.nixosSystem 函数中使用的 specialArgs 更灵活。
  2. _module.args 是在 Module 中声明使用的,因此必须在所有 Modules 都已经被求值后,才能使 用它。这导致如果你在 imports = [ ... ]; 中使用 _module.args 传递的参数,会报错 infinite recursion,这种场景下你必须改用 specialArgs 才行

NixOS 社区比较推荐优先使用 _module.args 这个 options,仅在无法使用 _module.args 时才改 用 specialArgs

假设你想将某个依赖项传递到子模块中使用,可以使用 specialArgs 参数将 inputs 传递到所有 子模块中:

nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    another-input.url = "github:username/repo-name/branch-name";
  };

  outputs = inputs@{ self, nixpkgs, another-input, ... }: {
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";

      # 将所有 inputs 参数设为所有子模块的特殊参数,
      # 这样就能直接在子模块中使用 inputs 中的所有依赖项了
      specialArgs = { inherit inputs;};
      modules = [
        ./configuration.nix
      ];
    };
  };
}

或者使用 _module.args 这个 option 也能达成同样的效果:

nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    another-input.url = "github:username/repo-name/branch-name";
  };

  outputs = inputs@{ self, nixpkgs, another-input, ... }: {
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        {
          # 将所有 inputs 参数设为所有子模块的特殊参数,
          # 这样就能直接在子模块中使用 inputs 中的所有依赖项了
          _module.args = { inherit inputs; };
        }
      ];
    };
  };
}

选择上述两种方式之一修改你的配置,然后在 /etc/nixos/configuration.nix 中就可以使用 inputs 这个参数了,模块系统会自动匹配到 specialArgs 中定义的 inputs,并将其注入到所 有需要该参数的子模块中:

nix
# Nix 会通过名称匹配,
# 自动将 specialArgs/_module.args 中的 inputs 注入到此函数的第三个参数
{ config, pkgs, inputs, ... }:

# 然后我们就能在这下面使用 inputs 这个参数了
{
  # ......
}

下一节将演示如何使用 specialArgs/_module.args 来从其他 flake 来源安装系统软件。

从其他 flake 来源安装系统软件

管系统最常见的需求就是装软件,我们在上一节已经见识过如何通过 environment.systemPackages 来安装 pkgs 中的包,这些包都来自官方的 nixpkgs 仓库。

现在我们学习下如何安装其他 flake 来源的软件包,这比直接从 nixpkgs 安装要灵活很多,最主要的 用途是安装 Nixpkgs 中还未添加或未更新的某软件的最新版本。

helix 编辑器为例,这里演示下如何直接编译安装 helix 的 master 分支。

首先在 flake.nix 中添加 helix 这个 inputs 数据源:

nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";

    # helix editor, use the master branch
    helix.url = "github:helix-editor/helix/master";
  };

  outputs = inputs@{ self, nixpkgs, ... }: {
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit inputs;};
      modules = [
        ./configuration.nix

        # 如下 Module 与前面的 `specialArgs` 参数功能完全一致
        # 选择其中一种即可
        # { _module.args = { inherit inputs; };}
      ];

    };
  };
}

接下来在 configuration.nix 中就能引用这个 flake input 数据源了:

nix
{ config, pkgs, inputs, ... }:
{
  # 省略无关配置......
  environment.systemPackages = with pkgs; [
    git
    vim
    wget
    curl
    # 这里从 helix 这个 inputs 数据源安装了 helix 程序
    inputs.helix.packages."${pkgs.system}".helix
  ];
  # 省略其他配置......
}

改好后再 sudo nixos-rebuild switch 部署,就能安装好 Helix 程序了。这次部署用时会比以往长 挺多,因为 Nix 会从源码编译整个 Helix 程序。

部署完毕后,可直接在终端使用 hx 命令测试验证。

另外,如果你只是想尝试一下 Helix 的最新版本,再决定要不要真正地将它安装到系统里,有更简单 的办法,一行命令就行(但如前所述,源码编译会很费时间):

bash
nix run github:helix-editor/helix/master

我们会在后面的 新一代 Nix 命令行工具的使用 中详 细介绍 nix run 的用法。

使用其他 Flakes 包提供的功能

其实这才是 Flakes 最主要的功能,一个 Flake 可以依赖其他 Flakes,从而使用它们提供的功能——就 如同我们在写 TypeScript/Go/Rust 等程序时使用其他 Library 提供的功能一样。

上面使用 Helix 的官方 Flake 中提供的最新版本就是一个例子,其他更多的用例会在后面提到,这里 引用几个后面会讲的例子: