MP

Installing a Specific Version of a Package with Nix

A common question when starting out with nix is "how do I install some particular version of some package?" This is not only a surprisingly complicated problem to solve for new nix users, but also a surprisingly difficult question to find answers to on your search engine of choice.

Here, I'm going to cover a few different ways of installing a particular package version using nix, highlighting the pros and cons of each. I'll be focusing on declarative nix configuration, with the nix repository pinned to a particular version using either the older niv or the newer flakes. The examples I show will be for installing a package into a development environment. See my other post here for instructions on how to set up a basic dev environment. I'll cover some basic setup again here, but see that post for rationale and discussion.

Contents

Philosophy: Why Is this so Hard?

Before we get to the examples, I think it's worth discussing why this is not more straightforward.

My personal theory here is that people (like myself) come to nix thinking about it as though it's equivalent to their other experiences with "declarative, reproducible" version management: lockfiles. I can tell cargo, pip, or node to install a specific set of versions for all of the packages that I need, so that my application will behave consistently. I have all versions of all packages at my disposal, because I am the one verifying that they all work together to produce some useful result.

Nixpkgs though is actually more like (and indeed is) the package manager in a Linux distribution, such as apt or yum. These package managers give you access to a suite of known good versions of packages, which have been tested to work well together and to provide a functional system. In the lockfile analogy, the Linux system itself is the final useful result, and the set of packages used to produce that are what you gain access to via the package manager. So, nixpkgs gives you the vetted lockfile of packages needed to produce some version of NixOS (the operating system).

Just like in your day-to-day Linux desktop installation, installing a custom version of a package that's not in the standard repositories involves a bit more ceremony than apt install rustup. You often will need to clone the source, build the package from scratch, and then copy the build artifacts into the appropriate locations on your system. If you're lucky, you can just download a pre-built binary. Nix allows you to do both of these things, in addition to some fancy tricks not available on standard Linux distros. Not only that, it gives you the tools to do the exact same thing declaratively, and in a way that can't bork your entire system by breaking system dependencies!

However, the trick is that nix was built to be a package manager for a Linux distribution. When you step into the role of building derivations, you're stepping into a role like a debian maintainer, not the role of a user installing packages on the Linux desktop. Most of the nix docs are written with this audience in mind, and so it can be a huge slog to figure out how to do some (supposedly) simple task. Luckily, it's getting easier, and the docs are getting better all the time. And in the meantime, there are helpful blogs like this one!

Setup: a Reproducible Dev Environment

First, you'll want to have your reproducible dev environment with pinned versions of the nixpkgs repository. Again, see here for a more in depth discussion of the general project structure. While I'll cover both flakes and niv throughout, I would recommend going with flakes unless you have a good reason not to.

Setup: Flakes

Run nix flake init in your project directory, and then git add flake.nix. You'll almost always want the following boilerplate:

{
  description = "some project";

  inputs = {
   # Your preferred primary nix relesae
   nixpkgs.url = "nixpkgs/release-22.11";
   # Proivdes legacy compatibility for nix-shell
   flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
   # Provides some nice helpers for multiple system compatibility
   flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils, flake-compat }:
    # Calls the provided function for each "default system", which
    # is the standard set.
    flake-utils.lib.eachDefaultSystem
      (system:
        # instantiate the package set for the supported system, with our
        # rust overlay
        let pkgs = import nixpkgs { inherit system; };
        in
        # "unpack" the pkgs attrset into the parent namespace
        with pkgs;
        {
          devShell = mkShell {
            # Packages required for development.
            buildInputs = [
              # Add your system dependencies here
              bashInteractive
              coreutils
              fd
              gnumake
              gnused
            ];
          };
        });
}

Add this to shell.nix for legacy compatibility with nix-shell commands, if needed (if you do not need nix-shell compatibility, you can remove flake-compat from the flake inputs):

# Legacy compat for folks not on nix with flakes.
#
# flake-compat reads the flake and provides shellNix (for nix-shell) and defaultNix
# (for nix-build). We only need shell here, since we're only using nix for the
# dev environment atm.
(import (
  fetchTarball {
    url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
    sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
  src =  ./.;
}).shellNix

And I highly recommend using direnv to make activating the nix environment seamless (don't forget to install an editor plugin!). All you need is the following in your .envrc:

use flake

Setup: Niv

First, you'll need to install niv on your system. If you're using nix, you can do that with nix-env -iA nixpkgs.niv. Follow the instructions at the repo, and then add the following in shell.nix:

# This file returns the evaluation of `mkShell`, which is the same thing we
# set to the `devShell` propety in the flake output above. The difference
# is that instead of our inputs being pinned by a flake, they are pinned in
# the `nix/sources.nix` file that niv generates.

let
  sources = import ./nix/sources.nix;
  # this is assuming you used the default `nixpkgs` name for nixpkgs. We can
  # call this function with no arguments, since we don't have any overlays.
  pkgs = import sources.nixpkgs {};
in
# "unpack" the pkgs attrset into the parent namespace, so we can use like
# `mkShell` instead of `pkgs.mkShell`
with pkgs;
  mkShell {
    # Your desired packages go here
    buildInputs = [ bashInteractive fd gnumake ];
  }

Option One: A Different Nixpkgs Version

Often the easiest option, one way to go about installing a particular version of a package is just to use whichever version of nixpkgs contained that package version! You can use this both for pinning to an older version, by pinning to some previous revision of nixpkgs, and for using a more current version, by pinning to unstable or even master.

The general pattern here is to add an input (flakes) or a source (niv) pointing to the alternative revision in question, and then install the package from that input/source.

When looking for versions of packages in the previous stable, current stable, or unstable branches, you can check here. When looking for an older or specific version of a package, you can check here to find a nix revision with the version you're looking for.

In these examples, we'll include examples of both installing a package from an alternative nix branch (unstable) and for installing from a particular revision. We'll install rust-analyze from unstable. For the particular revision, we'll be using a revision that includes version 8.3.2 of the fd utility for finding files: bf972dc380f36a3bf83db052380e55f0eaa7dcb6.

Different Nixpkgs Version: Flakes

Add the new sources to the inputs:

  inputs = {
   nixpkgs.url = "nixpkgs/release-22.11";
   
   # Unstable nix. You can name this whatever you want. We'll call it nixpkgs-unstable.
   nixpkgs-unstable.url = "nixpkgs/nixpkgs-unstable";
   
   # Some particular revision for installing fd
   nixpkgs-fd = "github:NixOS/nixpkgs/bf972dc380f36a3bf83db052380e55f0eaa7dcb6";
  };

And then use the new inputs in the buildInputs for the dev shell after instantiating the package sets:

  outputs = { self, nixpkgs, nixpkgs-unstable, nixpkgs-fd, flake-utils, flake-compat }:
    flake-utils.lib.eachDefaultSystem
      (system:
        let 
          pkgs = import nixpkgs { inherit system; };
          pkgs-unstable = import nixpkgs-unstable { inherit system; };
          pkgs-fd = import nixpkgs-fd { inherit system; };
        in
        with pkgs;
        {
          devShell = mkShell {
            # Packages required for development.
            buildInputs = [
              pkgs-unstable.rust-analyzer  # install rust-analyzer from unstable
              pkgs-fd.fd  # install fd from the appropriate revision
            ];
          };
        });

Different Nixpkgs Version: Niv

Niv is also straightforward. We'll use the CLI to add both the unstable branch and a particular revision for fd:

$ niv add NixOS/nixpkgs --name nixpkgs-unstable --branch nixpkgs-unstable
$ niv add NixOS/nixpkgs --name nixpkgs-fd --rev bf972dc380f36a3bf83db052380e55f0eaa7dcb6

From there, we now have nixpkgs-unstable and nixpkgs-fd attributes in our sources.nix. We'll import them in the same way we do regular nixpkgs and use them in our build inputs:

let
  sources = import ./nix/sources.nix;
  pkgs = import sources.nixpkgs {};
  pkgs-unstable = import sources.nixpkgs-unstable {};
  pkgs-fd = import sources.nixpkgs-fd {};
in
with pkgs;
  mkShell {
    # Your desired packages go here
    buildInputs = [ 
      pkgs-unstable.rust-analyzer  # install rust-analyzer from unstable
      pkgs-fd.fd  # install fd from the appropriate revision
    ];
  }

Option Two: Someone Else's Overlay

Sometimes, someone else already maintains an overlay to help you install alternative versions of a package. This is limited to a small set of packages for which installing alternative versions is common enough to justify the community effort, but if your package is among that set, this is generally as easy as using an alternative nixpkgs revision. Some packages that have well-supported overlays include:

An overlay provides additional or overridden attributes in the package set to which the overlay is applied. So, for example, the emacs overlay provides a new attribute emacs-nox, which is emacs compiled with no support for non-terminal operation, as well as the standard emacs attribute. The rust overlay provides a rust-bin attribute that includes the Rust compiler, cargo, and so on.

We'll use the rust overlay as an example.

Someone Else's Overlay: Flakes

Add the overlay to your inputs:

  inputs = {
   rust-overlay.url = "github:oxalica/rust-overlay";
  };

Then use it in your outputs:

  outputs = { self, nixpkgs, flake-utils, rust-overlay, flake-compat }:
    flake-utils.lib.eachDefaultSystem
      (system:
        let pkgs = import nixpkgs {
              inherit system;
              # Include the overlay in one of your nixpkgs configs. It is
              # recommended to use the same nixpkgs set from which you're 
              # installing rust-analyzer!
              overlays = [ rust-overlay.overlay ];
            };
        in
        with pkgs;
        {
          devShell = mkShell {
            buildInputs = [
              rust-bin.stable.latest.default  # this package is provided by the overlay
              rust-analyzer
            ];
          };
        });

Someone Else's Overlay: Niv

Essentially the same deal. Add the overlay to your sources:

$ niv add oxalica/rust-overlay --name rust-overlay

And then import and use from sources.nix:

let
  sources = import ./nix/sources.nix;
  rust-overlay = import sources.rust-overlay;

  pkgs = import sources.nixpkgs {
    overlays = [ rust-overlay ]
  };
in
with pkgs;
  mkShell {
    # Your desired packages go here
    buildInputs = [ 
      rust-bin.stable.latest.default  # this package provided by the overlay
    ];
  }

Option Three: Override Package Attributes

If you can't find an existing revision or overlay, the next easiest option is to override an existing package derivation's attributes to have it build a different version of the package in question, and then to install that new version in your build inputs.

This option requires some knowledge of the nix language and the ability to introspect existing derivations. It is my strong recommendation that you check out the nixpkgs repository locally in order to examine existing derivations, but you can also use the nix search website to find a package and then use the "Source" link to get to the derivation.

Different kinds of packages have slightly different tooling and will require different attributes to be overridden, so we'll look at a few classes of them. Since most of the variation here comes from the type of package being installed, we'll quickly cover the differences between flakes and niv first, and then dive into specific examples.

For flakes:

  outputs = { self, nixpkgs, flake-utils, rust-overlay, flake-compat }:
    flake-utils.lib.eachDefaultSystem
      (system:
        let 
          pkgs = import nixpkgs { inherit system; };
          # We define a new derivation by overriding attributes of an existing package
          my-pkg-my-version = pkgs.my-pkg.overrideAttrs (oldAttrs: {
            newAttr = "new-value";
          });
        in
        with pkgs;
        {
          devShell = mkShell {
            buildInputs = [
              # And install it
              my-pkg-my-version
            ];
          };
        });

For niv:

let
  sources = import ./nix/sources.nix;
  pkgs = import sources.nixpkgs {};
  my-pkg-my-version = pkgs.my-pkg.overrideAttrs (oldAttrs: {
    newAttr = "new-value";
  });
in
with pkgs;
  mkShell {
    # Your desired packages go here
    buildInputs = [ 
      my-pkg-my-version
    ];
  }

Override Package Attributes: "Standard" C/C++ Package

Most C/C++ packages are installed by cloning the repository and running ./configure, make, and make install. This is a very common pattern and is well abstracted in nix via the mkDerivation function. As an example, let's consider installing a particular version of jq. You can find its derivation here. This turns out to be a great example, because it's not quite as straightforward as a version override usually is, so we get to explore a little build debugging.

Let's say we want to install version 1.5rc2.

When we look at the derivation, most of it is not version specific. The only part we need to change is here:

  version = "1.6";

  src = fetchFromGitHub {
    owner = "stedolan";
    repo = "jq";
    rev = "${pname}-${version}";
    hash = "sha256-CIE8vumQPGK+TFAncmpBijANpFALLTadOvkob0gVzro";
  };

We can do this like so, putting this in the appropriate place for your flake or niv setup:

let 
  jq-1_5rc2 = pkgs.jq.overrideAttrs (oldAttrs: rec {
    pname = "jq";
    version = "1.5rc2";
    src = pkgs.fetchFromGitHub {
    owner = "stedolan";
      repo = "jq";
      rev = "${pname}-${version}";
      hash = "";
    };
  });

We put an empty string for the hash so that we can get the correct one from the error output:

warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
error: hash mismatch in fixed-output derivation '/nix/store/vlbp5iijglh2vx6crcng1gjazsbr10z7-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-JsgJhwI3cpVbkJdGECEnA8CDDeqaCktoEgfSbf4vhvg=

We can then put that in for the hash, and everything should just work when we go to enter our dev shell...

error: builder for '/nix/store/nfzfdmksfajaxsh11q7kj3l9z2awwhp3-jq-1.5rc2.drv' failed with exit code 1;
       last 10 log lines:
       > unpacking source archive /nix/store/93d946ryl3khzfr9r9b7v0hb9j589qh6-source
       > source root is source
       > patching sources
       > applying patch /nix/store/s0nsdqgd0x6ivb2kzgdzxz700irvvi69-fix-tests-when-building-without-regex-supports.patch
       > patching file Makefile.am
       > Hunk #1 FAILED at 130.
       > 1 out of 1 hunk FAILED -- saving rejects to file Makefile.am.rej
       > patching file configure.ac
       > Hunk #1 FAILED at 278.
       > 1 out of 1 hunk FAILED -- saving rejects to file configure.ac.rej
       For full logs, run 'nix log /nix/store/nfzfdmksfajaxsh11q7kj3l9z2awwhp3-jq-1.5rc2.drv'.

Oh no! What's the deal?? It looks like we're failing to apply a patch specified in the original derivation. Let's take a look and see what it is:

  patches = [
    (fetchpatch {
      name = "fix-tests-when-building-without-regex-supports.patch";
      url = "https://github.com/stedolan/jq/pull/2292/commits/f6a69a6e52b68a92b816a28eb20719a3d0cb51ae.patch";
      sha256 = "pTM5FZ6hFs5Rdx+W2dICSS2lcoLY1Q//Lan3Hu8Gr58=";
    })
  ];

If we look at that PR, we can see that patch comes from 2021, while the version of jq we're trying to install comes from 2015. Let's remove it and see if it builds! We can update our override like:

let 
  jq-1_5rc2 = pkgs.jq.overrideAttrs (oldAttrs: rec {
    pname = "jq";
    version = "1.5rc2";
    src = pkgs.fetchFromGitHub {
    owner = "stedolan";
      repo = "jq";
      rev = "${pname}-${version}";
      hash = "sha256-JsgJhwI3cpVbkJdGECEnA8CDDeqaCktoEgfSbf4vhvg=";
    };
    patches = [];  # no patches!
  });

Ah, another failure:

error: builder for '/nix/store/zrjl079rry6p62aarvjb1a7fjjzfkq35-jq-1.5rc2.drv' failed with exit code 1;
       last 10 log lines:
       > checking for remainder... yes
       > checking for thread-local storage... yes
       > checking whether byte ordering is bigendian... no
       > checking that generated files are newer than configure... done
       > configure: creating ./config.status
       > config.status: creating Makefile
       > config.status: executing depfiles commands
       > config.status: executing libtool commands
       > building
       > rm: cannot remove './modules/oniguruma': No such file or directory
       For full logs, run 'nix log /nix/store/zrjl079rry6p62aarvjb1a7fjjzfkq35-jq-1.5rc2.drv'.

Okay, it's easy to find where this is coming from in the derivation:

  # paranoid mode: make sure we never use vendored version of oniguruma
  # Note: it must be run after automake, or automake will complain
  preBuild = ''
    rm -r ./modules/oniguruma
  '';

Probably this is another thing that's been introduced since version 1.5, so let's override preBuild in our override:

let 
  jq-1_5rc2 = pkgs.jq.overrideAttrs (oldAttrs: rec {
    pname = "jq";
    version = "1.5rc2";
    src = pkgs.fetchFromGitHub {
    owner = "stedolan";
      repo = "jq";
      rev = "${pname}-${version}";
      hash = "sha256-JsgJhwI3cpVbkJdGECEnA8CDDeqaCktoEgfSbf4vhvg=";
    };
    patches = [];  # no patches!
    preBuild = "";  # no pre-build commands
  });

Woo! It works! Okay, let's check and make sure we have the right version of jq in our dev shell:

$ jq --version
jq-1.6

...huh. That's weird. We know we had to have build the right one, because we specified the SHA for the jq-1.5rc2 revision. So what gives? Let's look at the derivation again. When we do, we see something suspicious:

  # Upstream script that writes the version that's eventually compiled
  # and printed in `jq --help` relies on a .git directory which our src
  # doesn't keep.
  preConfigure = ''
    echo "#!/bin/sh" > scripts/version
    echo "echo ${version}" >> scripts/version
    patchShebangs scripts/version
  '';

Oh ho! There's some weirdness going on with the version. This is a tricky thing with overrideAttrs, that other attributes that reference the overridden attributes aren't necessarily reevaluated. Let's try patching that, setting it to exactly the same thing, but in a context where it will use our version:

let 
  jq-1_5rc2 = pkgs.jq.overrideAttrs (oldAttrs: rec {
    pname = "jq";
    version = "1.5rc2";
    src = pkgs.fetchFromGitHub {
    owner = "stedolan";
      repo = "jq";
      rev = "${pname}-${version}";
      hash = "sha256-JsgJhwI3cpVbkJdGECEnA8CDDeqaCktoEgfSbf4vhvg=";
    };
    patches = [];  # no patches!
    preBuild = "";  # no pre-build commands
    preConfigure = ''
      echo "#!/bin/sh" > scripts/version
      echo "echo ${version}" >> scripts/version
      patchShebangs scripts/version
    '';
  });

Now, when we enter our dev shell:

$ jq --version
jq-1.5rc2

At last!

Technically, the last step is unnecessary (the actual installed jq was the right version even when it was saying 1.6), but it's probably worth it to avoid confusion.

So now we're done! I'll note that this was a fairly complicated example of a package override, but hopefully the walkthrough of the debugging process is helpful. Usually, you only need the first step of overriding the pname, version, and src attributes!

Just to check, let's arbitrarily pick another typical package and give it a shot real quick. We'll install an alternative version of gnused. You can see its derivation here. Already, we can see this one is likely to be simpler, since there are are no patches and no fancy attributes like preBuild. The current src distribution looks like this:

  src = fetchurl {
    url = "mirror://gnu/sed/sed-${version}.tar.xz";
    sha256 = "0cznxw73fzv1n3nj2zsq6nf73rvsbxndp444xkpahdqvlzz0r6zp";
  };

I went and dug around in the mirror and randomly picked version 4.2.2 (from 2012) to install. Here's our override:

let
  gnused-4_2_2 = pkgs.gnused.overrideAttrs (oldAttrs: rec {
    version = "4.2.2";
    src = pkgs.fetchurl {
      url = "mirror://gnu/sed/sed-${version}.tar.xz";
      sha256 = "";
    };
  });

Note again we're setting sha256 to "" so that the error will tell us the correct SHA.

And, it should just work...

       > error: cannot download sed-4.2.2.tar.xz from any mirror

Cue shocked Pikachu face.

Okay, this one is easy. Looking at the mirror, we see that they only started publishing tar.xz packages with sed 4.3. Prior to that it was always tar.gz. So we just need to update the URL:

gnused-4_2_2 = pkgs.gnused.overrideAttrs (oldAttrs: rec {
  version = "4.2.2";
  src = pkgs.fetchurl {
    url = "mirror://gnu/sed/sed-${version}.tar.gz";
    sha256 = "";
  };

Et voila

$ sed --version
sed (GNU sed) 4.2.2

Override Package Attributes: Go Package

Lots of command line utilities are written in Go, and nix has good abstractions around building Go packages. Let's look at how to install a specific version of terragrunt, a staple in the DevOps world. Its derivation looks pretty simple! The only thing we see that looks unfamiliar is a vendorSha256, which I don't know offhand what it is, but let's do our normal override process and see what happens, trying to install terragrunt 0.36.2.

We'll make an override like so, using it in the appropriate place in your flake or your shell.nix file as described above:

terragrunt-0_36_2 = pkgs.terragrunt.overrideAttrs (oldAttrs: rec {
  pname = "terragrunt";
  version = "0.36.2";

  src = pkgs.fetchFromGitHub {
    owner = "gruntwork-io";
    repo = pname;
    rev = "v${version}";
    sha256 = "";
  };
});

Note as usual we set our sha256 to the empty string, so we will get an error with the correct SHA.

Let's see what we see when we try to build our dev env:

error: hash mismatch in fixed-output derivation '/nix/store/1ca0y9ws97csxwg0x2z48mf7prrd1n9f-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-Iv9ZQoU/mMYdxBuPfoYc/zQXQ14FmDBfoFwxnESC6Ns=

Nice! We can replace the SHA in our override with that and then try again.

This time we succeed, but we have some weirdness:

$ terragrunt --version
terragrunt version v0.35.20

Weird! Even older than we were trying to get.

Hm, if we look around the derivation again, we see that the vesion variable is actually used in a couple places:

  ldflags = [ "-s" "-w" "-X main.VERSION=v${version}" ];

  doInstallCheck = true;
  installCheckPhase = ''
    runHook preInstallCheck
    $out/bin/terragrunt --help
    $out/bin/terragrunt --version | grep "v${version}"
    runHook postInstallCheck
  '';

Let's try sticking those into our override also:

terragrunt-0_36_2 = pkgs.terragrunt.overrideAttrs (oldAttrs: rec {
  pname = "terragrunt";
  version = "0.36.2";

  src = pkgs.fetchFromGitHub {
    owner = "gruntwork-io";
    repo = pname;
    rev = "v${version}";
    sha256 = "sha256-Iv9ZQoU/mMYdxBuPfoYc/zQXQ14FmDBfoFwxnESC6Ns=";
  };
  ldflags = [ "-s" "-w" "-X main.VERSION=v${version}" ];
  installCheckPhase = ''
    runHook preInstallCheck
    $out/bin/terragrunt --help
    $out/bin/terragrunt --version | grep "v${version}"
    runHook postInstallCheck
  '';
});

Now, when we build, we get the version we expect!

$ terragrunt --version
terragrunt version v0.36.2

(relatively) easy peasy!

Override Package Attributes: Rust Package

Similarly, lots of of command line utilities are now written in Rust. Let's take a look at fd, which we were also looking at above. Here is the derivation. Looks pretty straightforward, but unfortunately overriding Rust package attributes is not as easy as it should be.

The buildRustPackage function generates a derivation, and while you can override those attributes that are passed to mkDerivation like you normally would, other attributes that are passed to rust-specific tooling cannot be directly overridden. The most important one is generally cargoSha256, which is the SHA of the cargo lockfile for the package. See this poist for more details.

The gist is that we want to start with an overlay that looks like this: Note that we use "" for both the fetchFromGitHub call and for the nested override for cargoDeps:

fd-8_3_2 = pkgs.fd.overrideAttrs (oldAttrs: rec {
  pname = "fd";
  version = "8.3.2";

  src = pkgs.fetchFromGitHub {
    owner = "sharkdp";
    repo = "fd";
    rev = "v${version}";
    sha256 = "";
  };
  
  cargoDeps = oldAttrs.cargoDeps.overrideAttrs (_: {
    inherit src;
    # This is where `cargoSha256` winds up being passed. We need to override
    # it directly rather than overriding `cargo256` at the parent level.
    outputHash = "";
  });
});

Running it, we get our first SHA for the package source:

error: hash mismatch in fixed-output derivation '/nix/store/c8515fg97nh69vk9qxlj97k183rm9drl-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-aNAV0FVZEqtTdgvnLiS1ixtsPU48rUOZdmj07MiMVKg=

If we replace that and run again, we get another error, this time for the SHA for the Cargo.lock file.

error: hash mismatch in fixed-output derivation '/nix/store/inxfs19llvfxkz7bwr483y4wrm0r4dvi-fd-8.3.1-vendor.tar.gz.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-twGv6ABjBH2wkPuthAZRZeM8hXb10uggSkKNJR6L/b0=

Once we replace those and re-build, we should have our desired version of fd!

$ fd --version
fd 8.3.2

Overlays: Using Your New Pin Everywhere

Once you have a derivation for your package version, you can install it directly in your buildInputs for your dev environment. This will make your package available in that environment, so for example when we've added our custom fd-8_3_2 to the build inputs, running fd uses our custom version. This is often enough, but sometimes you want to make sure that not only your interactive dev environment but also any other package that depends on the package in question uses your custom version. To accomplish this, we use an overlay.

An overlay is a function that takes the old package set and returns an attribute set that will be be merged with the old package set to produce a new one. It takes two arguments, often called self and super: the package set being generated and the package set being replaced. So, let's build an overlay that replaces the canonical version of each of our example packages that we built above.

let my-overlay = self: super: {
  fd = fd-8_3_2;
  jq = jq-1_5rc2;
  terragrunt = terragrunt-0_36_2;
}

We can then specify this overlay when instantiating a nixpkgs package set. For flakes:

  outputs = { self, nixpkgs, flake-utils, flake-compat }:
    # Calls the provided function for each "default system", which
    # is the standard set.
    flake-utils.lib.eachDefaultSystem
      (system:
        # instantiate the package set for the supported system, with our
        # rust overlay
        let 
          my-overlay = self: super: {...};  # definition from above
          pkgs = import nixpkgs { 
            inherit system; 
            # Specify your overlay as one of the package sets overlays
            overlays = [ my-overlay ];
          };
        in
        # "unpack" the pkgs attrset into the parent namespace
        with pkgs;
        {
          devShell = mkShell {
            # Packages required for development.
            buildInputs = [
              # Any reference to the overridden attributes, whether here or in
              # other packages, will use our versions
              fd
              jq
              terragrunt
            ];
          };
        });

And for niv, in shell.nix:

let
  sources = import ./nix/sources.nix;

  my-overlay = self: super: {...};  # definition from above
  pkgs = import sources.nixpkgs {
    overlays = [ my-overlay ]
  };
in
with pkgs;
  mkShell {
    buildInputs = [ fd jq terragrunt ];
  }

Summary

There are some things we haven't covered here: installing closed source binaries, more complicated overrides, and so on. I may update this post later with more content, so stay tuned!

Created: 2022-12-27

Tags: nix