Nix is a cross-platform package manager and domain-specific language used to define packages. In this article, I will demonstrate how to write and contribute Nix packages to Nixpkgs, the world's largest software repository. I assume that you, dear reader, already know the basics of software development. Nix brings a number of advantages over comparable packaging solutions, most importantly:
- more reproducible builds
- a large and welcoming community
- better build sandboxing
- ephemeral development environments
Recently at work I needed to debug several cloud-based virtual networks.
Since AWS security groups block ICMP traffic by default, I could not
simply ping
resources to test that they were reachable. I ended up explaing
to a colleague how to use telnet
for the task and realized
that I wanted a better tool for the job, so I wrote one.
This is a very simple Go project, and once I had tagged a release I was
content with, I decided to package it for Nix. I started looking in the
Nixpkgs repository for an example Go package, and then realized there
was a dedicated section on Go in the friendly manual:
The function buildGoModule builds Go programs managed with Go modules. It builds Go Modules through a two phase build:
- An intermediate fetcher derivation. This derivation will be used to fetch all of the dependencies of the Go module.
- A final derivation will use the output of the intermediate derivation to build the binaries and produce the final output.
This function handles most of what I needed to do, so let's clarify that paragaph a bit: in Nix,
a derivation is a specification that describes how to build, configure, and install something;
an attribute set is a collection of key-value pairs, e.g., { hello = "world"; foo = "bar"; }
.
The Nix language provides a built-in function called derivation
that takes an attribute set as
input and produces an attribute set as output, along with the side-effect of building, etc.
Here's a simplified example of a derivation:
# This is a comment.
# This file describes a function that takes a an attribute
# set containing `input` and `another` as input.
{ input, another }:
# And here is the body of the function:
{
input {
details = "of how to build ${output}";
}
}
# For more examples and explanations of Nix syntax, see:
# https://learnxinyminutes.com/docs/nix/
So to get started, I forked the Nixpkgs GitHub repository and then cloned it.
Next, I looked for a good place to put my package in the directory hierarchy,
choosing pkgs/tools/networking/knock
. I created the directory and wrote this
package.nix
file in it:
{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "knock";
version = "0.0.2";
src = fetchFromGitHub {
owner = "nat-418";
repo = pname;
rev = "a3749685381cae178bb5836c67645e0fce7aa1d0";
hash = "";
};
vendorHash = "";
meta = with lib; {
description = "A simple CLI network reachability tester";
homepage = "https://github.com/nat-418/knock";
license = licenses.bsd0;
changelog = "https://github.com/nat-418/knock/blob/${version}/CHANGELOG.md";
maintainers = with maintainers; [ nat-418 ];
};
}
Most of what you see above should be obvious: pname
is the name of the package, etc.
The rec
marks the buildGoModules
function call as recursive. The with lib;
means that
the following attribute set names don't need too specify lib.foo
, just foo
.
You may have noticed that I left the hash
and vendorHash
attributes with empty
strings. This is because when I run the build command on this package, I will get
messages telling me what their hashes are:
$ nix-build -E "with import <nixpkgs> {}; callPackage /home/nat/Code/nixpkgs/pkgs/tools/networking/knock/package.nix {}"
warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
these 3 derivations will be built:
/nix/store/hndxx3rg07wdm6diaifrv8372d8vgzyq-source.drv
/nix/store/4c6pgnwkbpcx7aspb691c2bgrmkh5l09-knock-0.0.2-go-modules.drv
/nix/store/agj3630xz1hadr7w5vknqg0v1l7za1cd-knock-0.0.2.drv
building '/nix/store/hndxx3rg07wdm6diaifrv8372d8vgzyq-source.drv'...
trying https://github.com/nat-418/knock/archive/a3749685381cae178bb5836c67645e0fce7aa1d0.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 4530 0 4530 0 0 6397 0 --:--:-- --:--:-- --:--:-- 6397
unpacking source archive /build/a3749685381cae178bb5836c67645e0fce7aa1d0.tar.gz
error: hash mismatch in fixed-output derivation '/nix/store/hndxx3rg07wdm6diaifrv8372d8vgzyq-source.drv':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-VXrWphfBDGDNsz4iuUdwwd46oqnmhJ9i3TtzMqHoSJk=
error: 1 dependencies of derivation '/nix/store/agj3630xz1hadr7w5vknqg0v1l7za1cd-knock-0.0.2.drv' failed to build
After updating the first hash I ran it again to get the second in the same way.
After my build succeeds, I can see in my current working directory a result
symlink to the Nix store, so I can execute ./result/bin/knock
and make sure
the program works correctly:
$ ./result/bin/knock localhost:1222
Failed: connection refused.
At this point, I wanted to add one more detail: installing the manpage as I had seen in an example Go package I mentioned above.
{ lib, buildGoModule, installShellFiles, fetchFromGitHub }:
buildGoModule rec {
pname = "knock";
version = "0.0.2";
src = fetchFromGitHub {
owner = "nat-418";
repo = pname;
rev = "a3749685381cae178bb5836c67645e0fce7aa1d0";
hash = "sha256-VXrWphfBDGDNsz4iuUdwwd46oqnmhJ9i3TtzMqHoSJk=";
};
vendorHash = "sha256-wkSXdIgfkHbVJYsgm/hLAeKA9geof92U3mzSzt7eJE8=";
outputs = [ "out" "man" ];
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installManPage man/man1/knock.1
'';
meta = with lib; {
description = "A simple CLI network reachability tester";
homepage = "https://github.com/nat-418/knock";
license = licenses.bsd0;
changelog = "https://github.com/nat-418/knock/blob/${version}/CHANGELOG.md";
maintainers = with maintainers; [ nat-418 ];
};
}
I added the installShellFiles
input and corresponding details in the derivation
to instal the manpage correctly. I built the package again, committed my changes,
and pushed to my fork of Nixpkgs. Note: the convetnion for commits of new packages
is to format the message $package_name: init at $version_number
.
I opened a pull request with the same naming convention and gave the homepage of the project (my GitHub repository) and a brief description of it. After I opened the pull request, some automated CI builds started running. When they passed, I posted my pull request to the review request thread on the Nix Discourse forum and logged off for the night. When I checked the PR the next day, another Nixpkgs contributor had some suggestions for changes which I accepted. The CI ran again, and by the end of the day a second reviewer had accepted and merged my PR into the master branch.
Now the Nix CI, Hydra, needed to churn and I followed the progress using this
PR tracker. At time of writing, knock
has been pulled into the unstable
branch and will be a part of the next stable relase of NixOS, which happens
every six months. I went from "hey I have an idea for this tool" to "this is
published and installable" in a matter of days.
Hopefully, you can see that writing a Nix pacakage definition is fairly straightforward: you just need to know how to build your package, and the very basics of the Nix language to implement what you know in code. That being said, my experience described above was easier than a first-timers would be since I am already in the maintainers file and I knew that I needed to post my PR to discourse. It can be confusing at first to figure out the community conventions around this process, but my experience has been very positive with everyone I have interacted with: usually people are very professional, willing to teach and correct without acrimony.
If you want advice on or help with your own package, join the NixOS Matrix channel or the Nix Nerds channel. For more details on how to contribute and packaging conventions, see this document. Also check nix.dev for more guides on how to use Nix effectively.