Skip to content

Instantly share code, notes, and snippets.

@nat-418
Last active April 24, 2024 14:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nat-418/a0297d5b966f8412a154352bef839101 to your computer and use it in GitHub Desktop.
Save nat-418/a0297d5b966f8412a154352bef839101 to your computer and use it in GitHub Desktop.
How to build with Nix

How to build with Nix

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

Writing a Nix package

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.

Wrapping up

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment