Skip to content

pkgs.callPackage

pkgs.callPackage 被用于参数化构建 Nix 包,为了理解它的用处,我们首先考虑下不使用 pkgs.callPackage 的情况下,我们要如何定义一个 Nix 包(也就是 Derivation)。

1. 不使用 pkgs.callPackage 的情况

我们可以使用如下代码来定义一个 Nix 包:

nix
pkgs.writeShellScriptBin "hello" ''echo "hello, ryan!"''

使用 nix repl 来验证一下,能看到它的执行结果确实是一个 Derivation:

shell
 nix repl -f '<nixpkgs>'
Welcome to Nix 2.13.5. Type :? for help.

Loading installable ''...
Added 19203 variables.

nix-repl> pkgs.writeShellScriptBin "hello" '' echo "hello, xxx!" ''
«derivation /nix/store/zhgar12vfhbajbchj36vbbl3mg6762s8-hello.drv»

上面这个 Derivation 的定义很短,就一行,但 nixpkgs 中大部分的 Derivation 的定义都要比这复杂很多。 前面我们介绍并大量使用了 import xxx.nix 来从其他 Nix 文件中导入 Nix 表达式,我们可以在这里也使用这种方法来提升代码的可维护性:

  1. 将上面这一行 Derivation 的定义存放到单独的文件 hello.nix 中。
    1. hello.nix 自身的上下文中不包含 pkgs 这个变量,所以需要修改下其内容,将 pkgs 作为参数传递给 hello.nix
  2. 在需要使用这个 Derivation 的地方,使用 import ./hello.nix pkgs 来导入它并使用 pkgs 作为参数来执行其中定义的函数。

仍然使用 nix repl 来验证一下,能看到它的执行结果仍然是一个 Derivation:

shell
 cat hello.nix
pkgs:
  pkgs.writeShellScriptBin "hello" '' echo "hello, xxx!" ''

 nix repl -f '<nixpkgs>'
Welcome to Nix 2.13.5. Type :? for help.

warning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring
Loading installable ''...
Added 19203 variables.

nix-repl> import ./hello.nix pkgs
«derivation /nix/store/zhgar12vfhbajbchj36vbbl3mg6762s8-hello.drv»

2. 使用 pkgs.callPackage 的情况

在前面不使用 pkgs.callPackage 的例子中,我们直接将 pkgs 作为参数传到了 hello.nix 中,这样做的缺点有:

  1. hello 这个 derivation 的所有其他依赖项都只能从 pkgs 中获取,耦合度太高。
    1. 比如说我们如果需要其他自定义依赖项,就必须修改 pkgs 或者修改 hello.nix 的内容,而这两个都很麻烦。
  2. hello.nix 变复杂的情况下,很难判断 hello.nix 到底依赖了 pkgs 中的哪些 Derivation,很难分析 Derivation 之间的依赖关系。

pkgs.callPackage 作为一个参数化构建 Derivation 的工具函数,可解决上述两个问题。 首先看看源码中此函数的定义与注释 nixpkgs/lib/customisation.nix#L101-L121

nix
  /* Call the package function in the file `fn` with the required
    arguments automatically.  The function is called with the
    arguments `args`, but any missing arguments are obtained from
    `autoArgs`.  This function is intended to be partially
    parameterised, e.g.,

      callPackage = callPackageWith pkgs;
      pkgs = {
        libfoo = callPackage ./foo.nix { };
        libbar = callPackage ./bar.nix { };
      };

    If the `libbar` function expects an argument named `libfoo`, it is
    automatically passed as an argument.  Overrides or missing
    arguments can be supplied in `args`, e.g.

      libbar = callPackage ./bar.nix {
        libfoo = null;
        enableX11 = true;
      };
  */
  callPackageWith = autoArgs: fn: args:
    let
      f = if lib.isFunction fn then fn else import fn;
      fargs = lib.functionArgs f;

      # All arguments that will be passed to the function
      # This includes automatic ones and ones passed explicitly
      allArgs = builtins.intersectAttrs fargs autoArgs // args;

    # ...... 省略后面的内容 ......

简单的说,它的使用格式是 pkgs.callPackage fn args,其中 fn 是一个 nix 文件或者函数,args 是一个 attribute set,它的工作流程是:

  1. pkgs.callPackage fn args 会先判断 fn 是一个函数还是一个文件,如果是文件就先通过 import xxx.nix 导入其中定义的函数。
    1. 第一步执行完毕得到的是一个函数,其参数通常会有 lib, stdenv, fetchurl 等参数,可能还会带有一些自定义参数。
  2. 之后,pkgs.callPackage fn args 会将 argspkgs 这个 attribute set 合并。如果存在冲突,args 中的参数会覆盖 pkgs 中的参数。
  3. 再之后,pkgs.callPackage fn args 会从上一步得到的 attribute set 中提取出 fn 函数的参数,并使用它们来执行 fn 函数。
  4. 函数执行结果是一个 Derivation,也就是一个 Nix 包。

那可以作为 pkgs.callPackage 参数的 nix 文件具体长啥样呢,可以去看看我们前面在 Nixpkgs 高级用法 - 简介 中举例过的 hello.nix fcitx5-rime.nix vscode/with-extensions.nix firefox/common.nix,它们都可以被 pkgs.callPackage 导入。

比如说我们自定义了一个 NixOS 内核配置 kernel.nix,并且将开发版名称与内核源码作为了可变更参数:

nix
{
  lib,
  stdenv,
  linuxManualConfig,

  src,
  boardName,
  ...
}:
(linuxManualConfig {
  version = "5.10.113-thead-1520";
  modDirVersion = "5.10.113";

  inherit src lib stdenv;

  # file path to the generated kernel config file(the `.config` generated by make menuconfig)
  #
  # here is a special usage to generate a file path from a string
  configfile = ./. + "${boardName}_config";

  allowImportFromDerivation = true;
})

那么就可以在任意 Nixpkgs Module 中使用 pkgs.callPackage ./hello.nix {} 来导入并使用它,并且替换它的任意参数。

nix
{ lib, pkgs, pkgsKernel, kernel-src, ... }:

{
  # ......

  boot = {
    # ......
    kernelPackages = pkgs.linuxPackagesFor (pkgs.callPackage ./pkgs/kernel {
        src = kernel-src;  # kernel source is passed as a `specialArgs` and injected into this module.
        boardName = "licheepi4a";  # the board name, used to generate the kernel config file path.
    });

  # ......
}

就如上面所展示的,通过 pkgs.callPackage 我们可以给 kernel.nix 定义的函数传入不同的 srcboardName,来生成不同的内核包,这样就可以使用同一份 kernel.nix 来适配不同的内核源码与不同的开发板了。

pkgs.callPackage 的优势在于:

  1. Derivation 的定义被参数化,定义中的所有函数参数就是 Derivation 的所有依赖项,这样就可以很方便的分析 Derivation 之间的依赖关系。
  2. Derivation 的所有依赖项与其他自定义参数都可以很方便地被替换(通过使用 pkgs.callPackage 的第二个参数),Derivation 定义的可复用性大大提升。
  3. 在实现了前两条功能的情况下,并未增加代码的复杂度,所有 pkgs 中的依赖项都可以被自动注入,不需要手动传递。

因此我们总是推荐使用 pkgs.callPackage 来定义 Derivation。

参考