Launching nix-shell with specific versions of packages

Hi all! Though I am happily running NixOS for all my general computing stuff, I’m just getting started with nix-shell for local isolated development environments. Somehow it feels like I am already using it wrong, so here we go:

Say I want to have an isolated shell environment for a Ruby project. From what I got in the documentation, I would start out with a simple shell.nix like

{ pkgs ? import <nixpkgs> {} }:
  pkgs.mkShell {
    nativeBuildInputs = [ pkgs.buildPackages.ruby_2_7 ];
}
  • which happens to give me (at the point of this writing) a Ruby 2.7.5p203.

But how would I go about and really pin the specific Ruby version I want, e.g. 2.7.4 or even an older 2.6.10? I found Hydra - nixos:release-21.11:nixpkgs.ruby_2_7.x86_64-linux - which seems to be job that builds the packages, but how would that translate to my shell.nix?

I found an old forum post – which goes into that direction, but it looks odd, since it looks like they build Ruby from source again :confused:

Best,
Bascht

1 Like

is there a meaningful difference between patch versions of ruby? AFAIK, the patch versions should be backported fixes.

For ruby 2.6, it was removed because it was EOL. You could probably checkout old nixpkgs release if you are desperate for that specific version.

It’s not so much that I need a specific old version, but I want to have a consistent version for everyone on the project.

By just specifying ruby_2_7 it could mean 2.7.3, which is already a year old or 2.7.6 which was released last week. So if I forget to run a nix-channel --update before entering nix-shell I could end up with a different patch version than a fellow team member. To reproduce failures, especially with third-party libraries I would say the patch version differences are meaningful.

With the current setup I could not really rely on two team members having the exact same version, which feels odd.

You could probably checkout old nixpkgs release if you are desperate for that specific version.

Would that work by using

xyz = import (fetchTarball https://github.com/NixOS/nixpkgs/archive/[old-sha].tar.gz)

and then referencing a sha that had the Ruby version I want?

Update: Gave it a try and it looks like this could work. Say I wanted to reproduce something on Ruby 2.7.4:

let  
  pkgs = import <nixpkgs> {};
  ruby_2_7_4 = import (fetchTarball https://github.com/NixOS/nixpkgs/archive/c6b949c09c791e6fe4d88d5c0971fdce83a22268.tar.gz) { }; 
  stdenv = pkgs.stdenv;

in stdenv.mkDerivation rec {
  name = "my-neat-little-ruby-app";
  buildInputs = [
    ruby_2_7_4.ruby
    ruby_2_7_4.bundler
    pkgs.libxml2
    pkgs.libxslt
    pkgs.zlib
    pkgs.bzip2
    pkgs.openssl
    pkgs.libffi
    pkgs.sqlite
    pkgs.imagemagickBig
    pkgs.pkgconfig
    pkgs.postgresql
  ];

gives me a local shell with the proper Ruby version and a matching bundler release.

It was a bit of a bumpy ride, but am I on the correct path? :grinning:

I want to have a consistent version for everyone on the project. (…) To reproduce failures, especially with third-party libraries I would say the patch version differences are meaningful.

That is a reasonable thing to want :slight_smile:

ruby_2_7_4 = import (fetchTarball https://github.com/NixOS/nixpkgs/archive/c6b949c09c791e6fe4d88d5c0971fdce83a22268.tar.gz) { };

I think that’s a fine approach!

There are a couple of alternatives. With ruby_2_7_4 = import (fetchTarball ..., while the ruby version is now fixed, different people on the project might still have different versions of, say, zlib. You could even get into situations where ruby was built with a different version of zlib than your project - which might lead to problems in some cases.

One solution is to avoid import <nixpkgs> {} entirely and take everything from that import (fetchTarball .... That locks things down further, and requires you to update the nixpkgs commit hash in the shell.nix each time you want to update the environment. This could be an advantage, but is also rather rigid (and leads to merge conflicts if you do it a lot - which also might or might not be an advantage depending on your viewpoint :wink: ).

Yet another approach, one that I like, is:

  • use ruby_2_7
  • document that your project is intended to build with the nixos-unstable channel (or whichever channel you choose)
  • log the specific commit of the channel when building

That way, your project ‘naturally’ follows the channel progression. Different people on the project might have slightly different versions based on how up-to-date their channel is, but if you need to reproduce problems you can always ask ‘what commit of nixpkgs did you build with’ and get exactly the same environment. If it’s a problem caused by a change in nixpkgs, you can even ‘git bisect’ nixpkgs to find out when/where it was introduced!

@raboof thank you very much for the detailed answer!

I think for now I will try to remove the import <nixpkgs> {} completely and just pin down what I can. :grin: That way the shell.nix feels a lot more like a “lock” file for all the dependencies needed and it should work well for smaller teams. Maybe we would then just pull from github.com/NixOS/nixpkgs into our own repository and push our own tags, e.g. 2022-04-25, so it’s easier to see how far you’re behind and you might end up with a conflict, but at least you don’t have to diff checksums. :grin:

But your second approach – basically documenting the requirements and letting people roll with whatever comes downstream – feels more natural to NixOS. I’ll have to think about it. The list of channels is global though, so they would need to add it to their system via nix-channel --add before entering the shell, no?

Personally I have the nixos-unstable branch checked out locally and in my /etc/nixos/configuration.nix did:

nix.nixPath = [
  "nixpkgs=/home/aengelen/nixpkgs" 
  "nixos-config=/etc/nixos/configuration.nix"
  "/nix/var/nix/profiles/per-user/root/channels"
];

And I just git pull && nixos-rebuild switch) every now and then. This works great for me, though I’m not sure how typical/common this is :wink: .

You can also nix-shell -I nixpkgs=/path/to/nixpkgs if you want different nixpkgs branches for different nix-shell invocations. Many ways to Rome :wink:

1 Like