pkgs.callPackage
pkgs.callPackage
被用于参数化构建 Nix 包,为了理解它的用处,我们首先考虑下不使用 pkgs.callPackage
的情况下,我们要如何定义一个 Nix 包(也就是 Derivation)。
1. 不使用 pkgs.callPackage
的情况
我们可以使用如下代码来定义一个 Nix 包:
pkgs.writeShellScriptBin "hello" ''echo "hello, ryan!"''
使用 nix repl
来验证一下,能看到它的执行结果确实是一个 Derivation:
› 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 表达式,我们可以在这里也使用这种方法来提升代码的可维护性:
- 将上面这一行 Derivation 的定义存放到单独的文件
hello.nix
中。- 但
hello.nix
自身的上下文中不包含pkgs
这个变量,所以需要修改下其内容,将pkgs
作为参数传递给hello.nix
。
- 但
- 在需要使用这个 Derivation 的地方,使用
import ./hello.nix pkgs
来导入它并使用pkgs
作为参数来执行其中定义的函数。
仍然使用 nix repl
来验证一下,能看到它的执行结果仍然是一个 Derivation:
› 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
中,这样做的缺点有:
hello
这个 derivation 的所有其他依赖项都只能从pkgs
中获取,耦合度太高。- 比如说我们如果需要其他自定义依赖项,就必须修改
pkgs
或者修改hello.nix
的内容,而这两个都很麻烦。
- 比如说我们如果需要其他自定义依赖项,就必须修改
- 在
hello.nix
变复杂的情况下,很难判断hello.nix
到底依赖了pkgs
中的哪些 Derivation,很难分析 Derivation 之间的依赖关系。
而 pkgs.callPackage
作为一个参数化构建 Derivation 的工具函数,可解决上述两个问题。首先看看源码中此函数的定义与注释 nixpkgs/lib/customisation.nix#L101-L121:
/* 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,它的工作流程是:
pkgs.callPackage fn args
会先判断fn
是一个函数还是一个文件,如果是文件就先通过import xxx.nix
导入其中定义的函数。- 第一步执行完毕得到的是一个函数,其参数通常会有
lib
,stdenv
,fetchurl
等参数,可能还会带有一些自定义参数。
- 第一步执行完毕得到的是一个函数,其参数通常会有
- 之后,
pkgs.callPackage fn args
会将args
与pkgs
这个 attribute set 合并。如果存在冲突,args
中的参数会覆盖pkgs
中的参数。 - 再之后,
pkgs.callPackage fn args
会从上一步得到的 attribute set 中提取出fn
函数的参数,并使用它们来执行fn
函数。 - 函数执行结果是一个 Derivation,也就是一个 Nix 包。
那可以作为 pkgs.callPackage
参数的 nix 文件具体长啥样呢,可以去看看我们前面在 Nixpkgs 高级用法 - 简介 中举例过的 hello.nix
fcitx5-rime.nix
vscode/with-extensions.nix
firefox/common.nix
,它们都可以被 pkgs.callPackage
导入。
比如说我们自定义了一个 NixOS 内核配置 kernel.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 {}
来导入并使用它,并且替换它的任意参数。
{ 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
定义的函数传入不同的 src
与 boardName
,来生成不同的内核包,这样就可以使用同一份 kernel.nix
来适配不同的内核源码与不同的开发板了。
pkgs.callPackage
的优势在于:
- Derivation 的定义被参数化,定义中的所有函数参数就是 Derivation 的所有依赖项,这样就可以很方便的分析 Derivation 之间的依赖关系。
- Derivation 的所有依赖项与其他自定义参数都可以很方便地被替换(通过使用
pkgs.callPackage
的第二个参数),Derivation 定义的可复用性大大提升。 - 在实现了前两条功能的情况下,并未增加代码的复杂度,所有
pkgs
中的依赖项都可以被自动注入,不需要手动传递。
因此我们总是推荐使用 pkgs.callPackage
来定义 Derivation。