nyaw.xyz
← 返回
Archive
记录·2025.06.07

flake-parts 初见

本文假设读者对以下内容有基础认识:

讲述 flake-parts 所解决的问题和基础的使用方法。

Nix Flakes 的形制如下所示:

{
  inputs = ...;
  outputs = {...}: {
  # ...
  };
}

outputs 是一个函数,它的输出就是flake最后的output,我们可以通过在任意一个 flake 仓库使用 nix flake show 来观察:

> nix flake show
git+file:///home/my/nixos
├───allSystems: unknown
├───packages
│   ├───aarch64-linux
│   │   └───a omitted (use '--all-systems' to show)
│   └───x86_64-linux
│       └───a: derivation 'a package, declared in flake'
├───debug: unknown
#...

一部分 output attribute 如 checks, packages的形式1类似 checks.system.name, packages.system.name, 这部分的属性在flake声明的时候,通常采用如下方式:

# pure nix flake way
{
  outputs = {nixpkgs, ...}: {
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.callPackage ./package.nix {
      some-special-arg = ...;
    };
    packages.aarch64-linux.default = nixpkgs.legacyPackages.aarch64-linux.callPackage ./package.nix {
      some-special-arg = ...;
    };
  };
}

在 flake-parts 出现前大家通常使用 flake-utils 来简化对多架构声明的工作:

# flake-utils way
{
  outputs = {nixpkgs, flake-utils, ...}: flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux"] (system: {
    packages.default = nixpkgs.legacyPackages.${system}.callPackage = ./package.nix {
      some-special-arg = ...;
    };
  });
}

细心的小伙伴可能会发现,flake-utils 定义的 eachSystem 函数实际上就是一个简单的 lib.genAttrs,-utils 所带来的功能完全没有必要通过引入一个input来解决, 而且它实际上带来了一些问题,例如你写坏了但是它没有类型检查, 导致你的flake能 pass eval 但悄咪咪地坏了。此部分在ayats的博客中亦有记载,这章节的一部分示例也来自这篇文章2

当你开始使用 flake-parts,perSystem将代替这部分产生每个架构的配置的工作,并检查你写的东西是否合乎最基本的类型规定:

# -parts way
{
  outputs = inputs@{nixpkgs, flake-parts, ...}: flake-parts.lib.mkFlake {inherit inputs;} {
    systems = [
      "x86_64-linux"
      "aarch64-linux"
    ];

    perSystem = {pkgs, system, ...}: {
      packages.default = pkgs.callPackage ./package.nix {};

      # ↓ flake-parts will reject this, hooray!
      nixosConfigurations.nixos = ...;
    };
  };
}

你要是问 nixosConfigurations 或者 nixosModules 这类attribute怎么办?当然是直接放 flake 里面就好了:

# -parts way
{
  outputs = inputs@{nixpkgs, flake-parts, ...}: flake-parts.lib.mkFlake {inherit inputs;} {
    systems = [ ];
    perSystem = {pkgs, system, ...}: { };

    flake = { nixosConfigurations.foo = { }; };
  };
}

flake-parts.lib.mkFlake 这个函数最后产生的配置和以上提到的 pure nix flake way 和 flake-utils way 的配置是等价的。

perSystem 在功能上基本超越了 flake-utils 并额外提供了一定程度的类型安全。而这是 flake-parts 其中一个feature的一部分。

还有一些零碎但是比较能提升幸福感的小功能,比如 perSystem 的函数输入中默认提供了一个 self',你可以在 perSystem 中将 self.packages.x86_64-linux.hello 简写为 self'.packages.hello 等等。

nix 的模块系统3声名远扬,一言概之其作用就是:

  1. options 定义选项,包括类型、默认值等
  2. config 写实际配置,连同 options 丢进 module system 经过计算合并等操作得出结果

在 flake-parts 之前 nix flake 本身是没有 module 的,在flake toplevel定义的东西都只能遵循flake schema本身然后以flake的方式解析, 例如你不能在 flake toplevel import 任何东西、定义任何option,最直观的影响就是你的 nixosConfigurations, packages 等等只能写在 flake.nix 中, 然后写错了你自己也难以发现,因为没有 module system 和其能够附带的任何检查。

这部分玩法比较多样,但熟悉nix module 的用户能够很快上手。通过 flake-parts 你可以像使用 nixos module 一样使用flake,通过在flake.nix中修改nix风味的配置项来定制它。可以参考 flake-parts Reference Documentation 那一节4的应用,既然有了 module system 就可以进行更进一步的包装,关于它可以将一些功能的使用简化到什么程度,可参考nix-topology的文档5

easyOverlay

可以通过 overlayAttrs 来直接导出本地flake已经定义的packages来作为overlay outputs,节省了很多字数:

before

packages.x86_64-linux.foo =
  (import inputs.nixpkgs {system = "x86_64-linux";}).callPackage ./foo.nix {};
packages.x86_64-linux.bar =
  (import inputs.nixpkgs {system = "x86_64-linux";}).callPackage ./bar.nix {};

overlays.foo = prev: final: prev.callPackage ./foo.nix {};
overlays.bar = prev: final: prev.callPackage ./bar.nix {};

after

perSystem = {pkgs,...}: {
  packages.foo = pkgs.callPackage ./foo.nix {};
  packages.bar = pkgs.callPackage ./bar.nix {};
}
flake.overlayAttrs = config.packages;

不过这个特性可能将来会有变动。

partition

原文档

作用是拆分flake(A)的一部分input到另外一个独立的分区flake(B),以便独立eval, 在项目(A)被用户作为 flake input 导入使用时, 避免一并引入flake(B) input的内容, 从使用上表现为可以只eval packages 而避免eval check之类develop相关的input。

将 nixosModules 拆分

可以用更优雅的方式在flake定义的 nixosModules.* 中访问flake本身的attribute等。

before

outputs = {inputs, self, ...}: {

    nixosModules.default = import ./nixos-module.nix self;

    # packages.x86_64-linux.foo = #...
}
# ./nixos-module.nix
self: # passed the outer flake self in
{
  config,
  pkgs,
  # config and pkgs, pass from nixos module, contains system archetecture info
}: {
  options.services.foo.package = mkOption {
    type = lib.types.package;
    default = self.packages.${pkgs.system}.default;
    defaultText = lib.literalMD "`packages.default` from the foo flake";
  };
  config = { };
}

after

# Flake module
{ withSystem, ... }: {
  flake.nixosModules.default = { pkgs, ... }: {
    imports = [ ./nixos-module.nix ];
    services.foo.package = withSystem pkgs.stdenv.hostPlatform.system ({ config, ... }:
      config.packages.default
    );
  };
}
# nixos-module.nix
{ lib, config, ... }: {
  options = {
    services.foo = {
      package = mkOption {
        defaultText = lib.literalMD "`packages.default` from the foo flake";
      };
    };
  };
  config = { };
}
  1. 见nixpkgs的部分文档,以及未通过的rfc

  2. https://ayats.org/blog/no-flake-utils

  3. nix module system

  4. https://flake.parts/options/flake-parts.html

  5. nix-topology 配置中 flake 与 flake-parts 的对比