Twitter   GitGub   Email

Part 1: Quickly packaging services using Nix flakes

Even though Nix has the most and most up-to-date packages, there is always some software you use that is not included yet. You could of course just download a binary, but if you are reading this you probably would like a more declarative way of dealing with the software you use. Thankfully, Nix flakes make it quite easy and quick to package most software. You can even add a NixOS module in the same flake to tell the OS how to configure the service and run it as a systemd service.

The rest of this article assumes that you have a basic understanding of Nix flakes and, for example, already used a flake as a NixOS or home-manager configuration. It’s basically the tutorial I wish I had after setting up my own systems as flakes and needed to run some unpackaged software.

Below we will make a Nix flake for float. It’s a small web service written in golang that you can use to set up a homepage for your homeserver. I chose this as an example because its repo is out of my control, which probably is the case for the software you want to package as well. It’s also configured via a small .yaml file, which is a common case we should cover. Finally, its whole purpose is to run as a service and serve a web page, a good excuse to learn how to do that.

It’s probably a good idea to skim over the float’s readme before continuing - it’ll only take a minute!

For reference later, the code used in this example is residing here.

Packaging non-go application

While we will focus on a go package, almost all of what is written below still applies for rust, node or even haskell packages. See the further reading section below for pointers after you’ve gotten the basics from the float example.

Packaging a go application

These days, most go programs are quite simple. They consist of a main.go with the entry point of the application and accompanying go.mod and go.sum files defining and locking dependencies (analagous to node’s package.json and package-lock.json). Nixpkgs includes a helper function (pkgs.buildGoModule) that takes all of these and just spits out a compiled app we can use in the rest of Nix. Let’s use it to make a package for float!

{
  description = "minimalist Configurable Homelab Start Page";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-22.05";
  };

  outputs = {nixpkgs, ...}: let
    # you can also put any architecture you want to support here
    # i.e. aarch64-darwin for never M1/2 macbooks
    system = "x86_64-linux";
    pname = "float";
  in {
    packages.${system} = let
      pkgs = nixpkgs.legacyPackages.${system}; # this gives us access to nixpkgs as we are used to
    in {
      default = pkgs.buildGoModule {
        name = pname;
        src = pkgs.fetchFromGitHub {
          owner = "aaqaishtyaq";
          repo = pname;
          rev = "v0.0.3";
          sha256 = pkgs.lib.fakeSha256;
        };

        vendorSha256 = pkgs.lib.fakeSha256;
      };
    };
  };
}

Now that’s quite a lot, let’s take it step by step.

{
  description = "minimalist Configurable Homelab Start Page";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-22.05";
  };
  ...

Here we set a small description of our flake, and define our inputs. In this case we only need nixpkgs for some helper functions.

  outputs = {nixpkgs, ...}: let
    # you can also put any architecture you want to support here
    # i.e. aarch64-darwin for never M1/2 macbooks
    system = "x86_64-linux";
    pname = "float";
  in {
    ...
  };
}

Here we define our output function that gets passed the inputs as an arg, and set some variables we will refernce later. Note that there are ways to easily make a package for all systems at once, but I left that out to keep it simple. I will leave some further reading material at the end of the post.

Finally, we are getting to the meaty part of actually describing our package!

    packages.${system} = let
      pkgs = import nixpkgs {inherit system;}; # this gives us access to nixpkgs as we are used to
    in {
      default = ...;
    };

Here we set the outputs of our flake, namely the packages attribute set. This name is convention and specifies what packages for what system this flake provides. default is also a convention, it’s the package that will get build when you run just nix build . without specifying anything else.

  pkgs.buildGoModule {
    name = pname;
    src = pkgs.fetchFromGitHub {
      owner = "aaqaishtyaq";
      repo = pname;
      rev = "v0.0.3";
      sha256 = pkgs.lib.fakeSha256;
    };
    vendorSha256 = pkgs.lib.fakeSha256;
  };

Here we call the pkgs.buildGoModule helper function to actually build float. We have to specify some mandatory arguments: the name of our package, where to get the source from and the sha256 of the packages’ dependencies. We can handily fetch the source directly from GitHub with the pkgs.fetchFromGitHub function.

Now, you might be rightly wondering about all these hashes and the reference to pkgs.lib.fakeSha256. The environment in which Nix evaluates our expressions does not really have access to the internet, except when we provide the hash of what we download before. This makes sure that all our builds are reproducible, and we never have to worry about it resulting in something we didn’t want. More info at the of end of the post!

But where do we get these hashes from? Isn’t that tedious to calculate? Here the fake hashes come to the rescue. Just run nix build .. Now, you will see an error message like:

> nix build .
error: hash mismatch in fixed-output derivation '/nix/store/4kq7wvibcdc10nxcw991cf5yp13y1862-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-3gMP9VjC8+u41gvzT45LflqZ4uk5+tObBtlJO5SCjwQ=

This is nix telling us: Hey, you wanted to download something, but it didn’t match the hash you provided. The helpful part is that it actually gives us the hash it actually downloaded, so we can copy it and substitute it for the sha256 field in fetchFromGitHub. Repeat this step one more time afterwards to get the hash for vendorSha256 as well.

Note: You probably want to verify these hashes match what they should be (i.e. by checking the commit hash on GitHub itself) to make sure nothing funky got introduced somehow.

Now you can run nix build . build one last time and everything succeed. Congrats! You now have the build float binary in ./result/bin/float. Go ahead and try to execute it.

Using this package directly

Now that we defined this package, we probably want to use it in our NixOS or home-manager configuration. …

Adding a NixOS module for float

This is covered in part 2 of this series. Click here to get there!

Further reading

  1. Helper functions to build packages in other languages are nicely covered in this version of the nix manual. Here is rust for example: https://ryantm.github.io/nixpkgs/languages-frameworks/rust/#rust
  2. The flake-utils repo provides some helper functions to generate package definitions for all systems: https://github.com/numtide/flake-utils
  3. More info on Nix hashes: https://nixos.wiki/wiki/Nix_Hash