Note: This is just one way to do it. I am sure there are others.

Nix is a great package manager. Period. I have been using it for almost everything I could do with pacman or docker.

The only problem I had so far was that while docker keeps record of everything, nix does not.

The biggest example was old versions of Ruby. I thought it would be a problem only for non-supported versions (for example: 1.9.3), but recently I run into an issue that wipped out even the version 2.7.1 (2.7.2 was released). Of course most 2.7.1 projects are compatible with 2.7.2, but it is annoying having to change back and forward the Gemfile

I had to run and compile myself the version I needed. While that is not really a major problem it is still some time I have to spend trying to fix my environment. Not exactly reproducible if you have to run to fix it.

After 3 or 4 major attempts to properly compile using nix instead of myself (or a tool like rbenv/rvm/chruby) I finally found a way.

First of all: how is nix for a development project?

First if all, I don't install all the tools I need in my machine as generation (e. g., install it and make it available in the shell for every session). Instead I have a shell.nix as this one (simplified):

with import <nixpkgs> {};

mkShell {
  buildInputs = [
    ruby_2_7
    readline
  ];

  shellHook = ''
    # Optional
    export GEM_HOME=/some/path/ruby27
    mkdir -p $GEM_HOME
    export GEM_PATH=$GEM_HOME
    export PATH=$GEM_HOME/bin:$PATH

    gem list -i ^bundler$ -v 2.2.22 || gem install bundler --version=2.2.22 --no-document
    export SOME_ENV=xxx
  '';
}

And whenever you need to activate this you can call:

$ nix-shell
true

[nix-shell:~/Dev/rubynz/membership-register]$

Or, if you use zsh like me:

$ nix-shell --command zsh
true
[nix-shell]$

But what happens when you, like me, need one previous version of the 2.7.x branch?

Compile Ruby using nix

The easier solution is basically changing your shell.nix to something like:

with import <nixpkgs> {};

mkShell {
  buildInputs = [
    (pkgs.callPackage ./ruby272.nix {}) # this is the difference
    readline
  ];

  shellHook = ''
    # Optional
    export GEM_HOME=/some/path/ruby272
    mkdir -p $GEM_HOME
    export GEM_PATH=$GEM_HOME
    export PATH=$GEM_HOME/bin:$PATH

    gem list -i ^bundler$ -v 2.2.22 || gem install bundler --version=2.2.22 --no-document
    export SOME_ENV=xxx
  '';
}

And the file ruby272.nix looks like:

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

pkgs.stdenv.mkDerivation {
  name = "ruby272";
  version = "2.7.2";

  src = pkgs.fetchurl {
    url = "https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.gz";
    sha256 = "6e5706d0d4ee4e1e2f883db9d768586b4d06567debea353c796ec45e8321c3d4";
  };

  phases = [
    "unpackPhase"
    "configurePhase"
    "buildPhase"
    "installPhase"
  ];

  buildInputs = [
    openssl
    zlib
    readline
    gdbm
  ];

  unpackPhase = ''
    tar xfz $src -C /build
  '';

  configurePhase = ''
    cd ruby-$version
    ./configure --prefix=$out --disable-install-doc --disable-install-rdoc
  '';

  buildPhase = ''
    make
  '';

  installPhase = ''
    make install
  '';
}

A few things to note:

  • You can call it whatever you want (name = "ruby272";). I prefer to avoid using _ so I don't think it is a package from the official repos
  • As far as I tested version is optional, but it makes life easier later on in the same script
  • You can pre build using nix-build ruby272.nix but you already includes it in the shell.nix anyway
  • You don't need to use configurePhase you can use configureFlags (I have not tested, but I prefer configurePhase anyway)
  • The download of the tgz is done and cached by nix 😉
  • Of course you know, but you can customize any step, for example to include default gems just change the installPhase
  • The building is cached between projects (e. g., you can have as many ruby272.nix as you want since they are the same it won't try to recompile)