This post has been released in the summer of nix blog on 2022-09-08. There the article has gone through thorough reviews and a lot of editorial changes such that we decided to release it with the reviewers mentioned as coauthors there.
This article is the original version of that article with only minor changes, mostly typofixes.
In nixpkgs
, there is a massive use of the
callPackage
function, which provides us with a lot of benefits.
Basic examples
Though before even discussing the benefits, lets see how it actually gets used.
Given are these 2 files hello.nix
and default.nix
:
Usually I would prefer the usage of nix flakes, though that would introduce a
lot of boilerplate in the examples for no reason, therefore I will use diamond
paths and import
for simplicity.
let pkgs = import <nixpkgs> {}; inpkgs.callPackage ./hello.nix {}
{writeShellScriptBin}:writeShellScriptBin "hello" ''echo "hello, world!"''
Building using nix-build
will produce ./result/bin/hello
, and running it will
nicely greet you.
As you can see, writeShellScriptBin
gets passed in by callPackage
automatically.
For this simple setup, having to create an extra file, seems to be a lot of boilerplate, though if you continue reading, you will see, it is worth it!
1. Benefit: parametrized builds
Now lets change the default.nix
:
let pkgs = import <nixpkgs> { }; in{hello = pkgs.callPackage ./hello.nix { };}
Now we build using nix-build -A hello
, the outcome will be the same as above.
Now to "parametrize" the build we also change the hello.nix
a bit:
{ writeShellScriptBin, audience ? "world"}:writeShellScriptBin "hello" ''echo "hello, ${audience}!"''
Building this will still yield the same output as before, though now things get
interesting, alter your default.nix
yet another time:
let pkgs = import <nixpkgs> { }; in{hello = pkgs.callPackage ./hello.nix { };people = pkgs.callPackage ./hello.nix { audience = "people"; };}
Building via nix-build -A people
will now yield a script that prints "hello,
people" instead.
We can use the very same syntax to also overwrite the automatically discovered
arguments like writeShellScriptBin
, though that doesn't make sense here.
Though, for a Go program that expects buildGoModule
it is common to see some
expression like callPackage ./go-program.nix { buildGoModule = buildGo116Module; }
to enforce a certain Go compiler version.
2. Benefit: overrides
As a consequence from the parametrized builds, we can also change the value of
the parameters after the fact, using the derivations override
function.
Consider this new default.nix
:
let pkgs = import <nixpkgs> { }; inrec {hello = pkgs.callPackage ./hello.nix { };people = pkgs.callPackage ./hello.nix { audience = "people"; };folks = hello.override { audience = "folks"; };}
Building and running the folks
attribute, will give again a new version of the
script.
All the other parameters will remain the same as the have been when hello
was
instantiated.
This is especially useful and often seen on packages that provide a whole lot of options to optimize the build.
An example to mention here is the neovim
attribute in nixpkgs, which has has
some overrideable arguments like extraLuaPackages
, extraPythonPackages
, or
withRuby
.
3. Benefit: modifiable
And now I want to introduce one of my most favorite benefits:
You can actually create your own version of callPackage
, which comes in quite
handy when you have large sets where the attributes to be built depend on each
other.
In the next examples I will not implement or show the "called" files, as I think they are not necessary to understand the point I want to make.
Consider the following initial version:
let pkgs = import <nixpkgs> { }; inrec {a = pkgs.callPackage ./a.nix { };b = pkgs.callPackage ./b.nix { inherit a; };c = pkgs.callPackage ./c.nix { inherit b; };d = pkgs.callPackage ./d.nix { };e = pkgs.callPackage ./e.nix { inherit c d; };}
Here you have to remember passing required arguments that are not in nixpkgs' toplevel manually.
This can become quite tedious quickly, especially the larger the set becomes.
Therefore we can use lib.callPackageWith
to create our own callPackage
version.
letpkgs = import <nixpkgs> { };callPackage = lib.callPackageWith (pkgs // packages);packages = {a = callPackage ./a.nix { };b = callPackage ./b.nix { };c = callPackage ./c.nix { };d = callPackage ./d.nix { };e = callPackage ./e.nix { };};inpackages
Our modified callPackage
now will exactly "know" how to resolve the dependencies
through the set defined by pkgs // packages
.
Nix' laziness does us a good favour here and makes this actually possible.
Summary
So using callPackage
doesn't only make your code uniform to what you see a lot
in nixpkgs already, it also gives you some things for free:
- parametrized builds
- overrideable builds
- cleaner implementation of large interdepending package sets
Further reading
There is also callPackages
and lib.callPackages
which do a pretty similar
thing, though they expect that the returnvalue is not a package, but a
packageset.
Each of the attributes in the returned set will then be overrideable as if you
had called callPackage
on that.
{ runCommand }:{a = runCommand "a" { } "echo a > $out";b = runCommand "b" { } "echo b > $out";}
$ nix-build -E 'with import <nixpkgs> {}; (callPackages ./callpackages.nix { }).a.override { }'this derivation will be built:/nix/store/4sjzxijjfamjqgr8237lr638b8qkabnk-a.drvbuilding '/nix/store/4sjzxijjfamjqgr8237lr638b8qkabnk-a.drv'.../nix/store/4n3w8mkswwpfa1vvx3012xbaqskflg2z-a$ nix-build -E 'with import <nixpkgs> {}; (callPackages ./callpackages.nix { }).a.override { runCommand = runCommandCC; }'this derivation will be built:/nix/store/qj9hg9qiahggi4yk6qsh4wv33jl33f36-a.drvbuilding '/nix/store/qj9hg9qiahggi4yk6qsh4wv33jl33f36-a.drv'.../nix/store/50llkafby4vci46qda0xlva24mlghwr0-a
As you can see, 2 different paths have been produced, due to the fact that we
replaced the runCommand
.