I previously wrote about using Node Version Manager (NVM) with yarn and Nix shells. This post is about the evolution of this approach into managing any version of Node with Nix shells without the need for NVM anymore.

First I’ll define some things.

Then I’ll get to the good bit!

What is Nix?

Nix is a cross-platform package manager that utilises a purely functional deployment model. A Nix package describes how it should be built and the exact state of its required dependencies. Nix uniquely hashes each dependency and links the package to its declared dependency through some ninja symlinking. Nix provides the ability to describe and build reliably reproducible state for an environment.

What is a Nix shell?

Similar to a Nix package, a Nix shell is describes all of the packages that should be installed in a (mostly) isolated shell that Nix will build for you. Nix shells provide the ability to create development environments with just the dependencies a project requires, or to use packages that you don’t want to install globally. Think of Nix shells as a different approach to what a Docker container can do for isolated, reproducible development environments

What is Node?

Node is an asynchronous event-driven JavaScript runtime released in 2009 that is based on the V8 Javascript engine most famously used by the Chromium project and it’s offshoots (eg. Chrome). Node allows javascript projects to be built and run on any major platform by bundling a whole swathe of platform specific bindings for OS operations.

What is Node Version Manager?

As with any piece of software Node has had plenty of different versions built and released since its inception. Ordinarily running multiple versions of the same software in a single environment is always a little tricky. Node Version Manager (NVM) is a tool that makes that relatively simple to do with Node.

Managing Node versions with Nix alone

I’ve been using NVM to manage mutiple Node versions on my machine in conjunction with Nix shells, but with Nix alone I should be able to achieve the same outcomes. This is very much a part of my Nix journey and out of the box nixpkgs only ships the latest minor/patch versions of each major Node version so I was still using NVM. I wanted to investigate and learn before attempting to do.

To take my next steps on my Nix jounrney and stop using NVM I wanted to understand a bit more about a complex derivation, how exactly Node in nixpkgs was defined and why arbitrary versions weren’t easy to get hold of.

I wont pretend to understand the Nix language or derivations entirely just yet but walking through the source of the Node_X derivations available in nixpkgs (Github) helped me. I could see that a utility package already exists that can be used to build any version of Node (Gihtub) and that the published Node_X packages explicitly only build the latest versions. Supplying only the latest versions makes sense from a security standpoint but on the ground some projects just require very specific Node versions that tools like NVM make it trivial to access.

Can I take the same approach to build a specific version that I need? Yes, yes I can! And it looks like this:

{ pkgs ? import <nixpkgs> {} }:
with pkgs;

let
  buildNodejs = pkgs.callPackage <nixpkgs/pkgs/development/web/Nodejs/Nodejs.nix> {
    icu = icu68;
    python = python2;
  };

  # For per version syntax check similar to https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/web/Nodejs/v12.nix
  # Use git history of the file to find specific version syntax
  Nodejs-12_19_0 = buildNodejs {
    enableNpm = true;
    version = "12.19.0";
    sha256 = "1qainpkakkl3xip9xz2wbs74g95gvc6125cc05z6vyckqi2iqrrv";
    patches = pkgs.lib.optional pkgs.stdenv.isDarwin <nixpkgs/pkgs/development/web/Nodejs/bypass-xcodebuild.diff>;
  };

in mkShell {
  buildInputs = [
    Nodejs-12_19_0
  ];
}

As the comment suggests I source the hash and syntax from the history of nixpkgs because, well, why not. You can also see that a patch file is referenced when building Node for Darwin (MacOS) in this way, this is required for versions 10, 12 and 14 as far as I know. This is sourced from nixpkgs but be aware that it may not be needed for the every major Node version.

Unfortunately this approach builds the requested Node version from source. This a reproducible process and the expected behaviour for a Nix binary but ordinarily nixpkgs leverages remote caches of derivation outputs. Building from source does take time, sometimes a lot of time. It also means that the first time a specific version of Node is required by Nix on a machine (or since the local cache was cleared) it takes that long.

This where I am at the moment and, after those long initial builds for the specific Node versions I need for the projects I am currently a part of, it works well. I am able to stop using NVM and manage Node versions in Nix shells with Nix alone.

Summary

Using NVM with Nix shells was a helpful transition to using Nix to orchestrate my environments, at least in part. As my confidence grew with Nix and time allowed a bit more exploration I tried to move past that and remove the need for NVM entirely. The solution I am using is not the best as each version of Node is built from source the first time it is required. I think there is an opportunity here to delve into the Nix language a bit more so I can write a derivation that will pull down a prebuilt Node tarball or a cached output and install it from that instead.

One thought I’ve had is, would it be worth exposing via nixpkgs every version of Node? This could surely be achieved dynamically and would leverage the caches that are already in place for prebuilt binaries. I don’t know enough about the nixpkgs ecosystem yet to understand the costs and tradeoffs with that approach so it might not be a great idea.

A follow up note

I am still using the approach outlined in this post, though I have found some resources since writing (but before publishing) this post that suggest there is a better solution:

I am going to experiment with some of these different approaches and likely write a follow up to this outlining where I got to.

Get in contact

If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.