Compile Ruby using Nix
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 theshell.nix
anyway - You don't need to use
configurePhase
you can useconfigureFlags
(I have not tested, but I preferconfigurePhase
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)