the callPackage pattern
how nixpkgs wires dependencies together automatically.
how nixpkgs wires dependencies together automatically.
callPackage is why a nixpkgs package file can start by naming its dependencies:
{ lib, stdenv, fetchurl }:
stdenv.mkDerivation {
pname = "hello";
version = "2.12.1";
src = fetchurl {
url = "mirror://gnu/hello/hello-2.12.1.tar.gz";
hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
};
}
there are no imports for stdenv, fetchurl, or lib. the file asks for them as function arguments. nixpkgs calls the function with matching values from the package set.
nix can inspect an attribute-set function's argument pattern:
$ nix eval --json --expr 'builtins.functionArgs ({ stdenv, fetchurl, lib ? null, ... }: stdenv)'
{"fetchurl":false,"lib":true,"stdenv":false}
false means the argument is required. true means it has a default.
the real hello package in the pinned nixpkgs revision asks for eight arguments:
$ nix eval --impure --json --expr \
'builtins.functionArgs (import /nix/store/6bwyhcqsr8pj2gyvdzsr742v9jf2zn52-source/pkgs/by-name/he/hello/package.nix)'
{
"callPackage": false,
"fetchurl": false,
"hello": false,
"lib": false,
"nixos": false,
"stdenv": false,
"testers": false,
"versionCheckHook": false
}
all eight are required.
hello asking for hello is intentional. the package uses it in passthru.tests, where the test should refer to the final package.
lazy evaluation makes that possible.
the real implementation lives in lib/customisation.nix. stripped down to the interesting part:
callPackageWith = autoArgs: fn: args:
let
f = if isFunction fn then fn else import fn;
fargs = functionArgs f;
allArgs = intersectAttrs fargs autoArgs // args;
in
makeOverridable f allArgs
three inputs:
| name | meaning |
|---|---|
autoArgs | the set to pull missing arguments from. normally pkgs |
fn | a package function or a path to one |
args | explicit arguments from the caller |
callPackage is callPackageWith with pkgs already supplied:
callPackage = callPackageWith pkgs;
so this:
callPackage ./package.nix { }
means this:
callPackageWith pkgs ./package.nix { }
pkgs is the automatic argument set. explicit arguments are merged on top.
take a small package function:
{ stdenv, fetchurl, lib }:
stdenv.mkDerivation { ... }
functionArgs returns:
{
stdenv = false;
fetchurl = false;
lib = false;
}
intersectAttrs fargs pkgs keeps the attributes from pkgs whose names appear in the function argument pattern:
{
stdenv = pkgs.stdenv;
fetchurl = pkgs.fetchurl;
lib = pkgs.lib;
}
then explicit arguments are merged on top:
intersectAttrs fargs pkgs // args
right side wins, as with any // merge.
stdenv=?fetchurl=?lib=?openssl=?stdenvfrompkgsfetchurlfrompkgslibfrompkgsopensslfromargsstdenvpkgs.stdenvfetchurlpkgs.fetchurllibpkgs.libopensslpkgs.opensslif a package exposes an optional dependency:
{ stdenv, fetchurl, openssl ? null }:
stdenv.mkDerivation {
configureFlags = lib.optional (openssl != null) "--with-openssl=${openssl.dev}";
}
you can leave it at the default:
callPackage ./package.nix { }
or pass the dependency when calling the package:
callPackage ./package.nix {
openssl = pkgs.openssl;
}
the explicit argument set wins over automatic arguments too:
callPackage ./package.nix {
stdenv = pkgs.clangStdenv;
}
the package function is the same. the argument set changed. if the derivation uses that value, the .drv changes.
if a required argument is not in pkgs and not passed explicitly, evaluation fails before a build can start.
{ stdenv, doesNotExist }:
stdenv.mkDerivation { ... }
called with this:
callPackage ./package.nix { }
there is no pkgs.doesNotExist, and the caller did not pass doesNotExist manually. nix cannot call the function.
the failure happens during evaluation. no .drv gets written.
stdenvyesnopassedfetchurlyesnopasseddoesNotExistnonoerrorcallPackageWith does not return f allArgs directly. it wraps the call:
makeOverridable f allArgs
that wrapper is why packages have .override:
hello.override {
fetchurl = myFetchurl;
}
.override calls the original package function again with a changed argument set.
if the override gives the package the same value it already had, the derivation path does not change:
$ nix eval --impure --raw --expr '
let pkgs = import /nix/store/6bwyhcqsr8pj2gyvdzsr742v9jf2zn52-source {
system = "aarch64-darwin";
};
in (pkgs.hello.override { fetchurl = pkgs.fetchurl; }).drvPath
'
/nix/store/5mh0djicybhh26h0m8qciaizc1iwsy83-hello-2.12.1.drv
same fetchurl, same inputs, same .drv.
change a real argument and the path changes:
pkgs.hello.override {
stdenv = pkgs.clangStdenv;
}
now stdenv is different. the package function is called again, and the derivation records a different compiler environment.
.override changes the function arguments before the derivation is created.
.overrideAttrs changes the attributes passed to mkDerivation.
they operate at different layers.
for hello, these are function arguments:
{ callPackage, lib, stdenv, fetchurl, nixos, testers, versionCheckHook, hello }:
these are derivation attributes:
{
pname = "hello";
version = "2.12.1";
src = fetchurl { ... };
doCheck = true;
}
use .override when the thing you want to change is a package-function argument, like a dependency, builder, or feature flag.
use .overrideAttrs when you want to change the derivation itself, like source, patches, build flags, phases, or metadata.
nixpkgs has too many packages for manual wiring to stay readable.
without callPackage, every package entry would repeat its dependencies:
hello = import ./hello/package.nix {
inherit lib stdenv fetchurl nixos testers versionCheckHook;
inherit hello;
};
with callPackage:
hello = callPackage ./hello/package.nix { };
the dependency list lives in the package file. the package set supplies matching names. overrides only mention the values that change.
overlays and overrides covers changes made after nixpkgs has built the package set.