what is bundlerEnv doing?

Published 2020-07-19 on Farid Zakaria's Blog

The Nix wiki is pretty great for a lot of technical content however it sometimes fails to gently walk users through how something works.

I’ve been doing some Ruby work as part of my day-to-day job and wanted to better understand Nix’s approach to Ruby.

If you Google “Nix Ruby”, you’ll inevitably find the Packaging/Ruby Nix wiki page.

The page walks the reader quickly through some Nix incantations, including calling a utility bundix without much explanation. I hope the remaining portion of this post serves as a better guide & deeper understanding of what’s happening.

Setup

First let’s setup the most simple bundler project, using only a single gem: hello-world.

Let’s initialize our directory to create the Gemfile & add our hello-world dependency.

bundle init
bundle add hello-world

Create an extremely minimal Ruby script main.rb.

require 'rubygems'
require 'bundler/setup'

require 'hello-world'

Finally let’s run the utility bundix; with more explanation to follow. You may not have bundix in your current environment, I found it useful just to run it directly from the nix-shell.

nix-shell -p bundix --run 'bundix -l'

Bundix’s role is very simple. It traverses your Gemfile.lock, and generates a file gemset.nix to be consumed by the subsequent Nix Ruby functions.

gemset.nix should be seen simply as the Nix transformation of the _Gemfile.lock_in the Nix language.

{
  hello-world = {
    groups = ["default"];
    platforms = [];
    source = {
      remotes = ["https://rubygems.org"];
      sha256 = "141r6pafbwjf8aczsilxxhdrdbbmdhimgbsq8m9qsvjm522ln15p";
      type = "gem";
    };
    version = "1.2.0";
  };
}

With all that fanfare out of the way, time to write the shell.nix file. Below is an extremely minimal shell.nix to get started.

{ pkgs ? import <nixpkgs> { } }:
with pkgs;
with stdenv;
let
  app = bundlerEnv {
    name = "my-app";
    ruby = ruby;
    gemdir = ./.;
  };
in mkShell {
  name = "bundler-shell";
  buildInputs = [app bundix ruby];
}

You can change the assignment of Ruby to any version you want.

At this point, things worked. Great! However I was pretty confused as to what was going on. Let’s dig in!

bundlerEnv is defined in ruby-modules/bundled-common/default.nix in nixpkgs.

One of the first thing the derivation does, is call a function to determine the 3 important files: Gemfile, Gemfile.lock & gemset.nix.

Our example above didn’t mention anything of the sort, so by default, it will find them according to the gemdir.

gemset.nix

The gemset.nix file is used to create derivations for each gem referenced by downloading the source files into the /nix/store.

bundleEnv

With all the gems downloaded, a buildEnv (symlink of multiple derivations) is created.

/nix/store/ygzpgzxm25j6lfyad3zxr6rm2psahjlz-fzakaria.com/
|-- bin
|   |-- bundle
|   |-- bundler
|   `-- hello-world
`-- lib
    `-- ruby
        `-- gems
            `-- 2.6.0
                |-- bin
                |   |-- bundle -> /nix/store/niy7ivnph74z02kf8cvh9c4kz0i70nqp-bundler-2.1.4/lib/ruby/gems/2.6.0/bin/bundle
                |   |-- bundler -> /nix/store/niy7ivnph74z02kf8cvh9c4kz0i70nqp-bundler-2.1.4/lib/ruby/gems/2.6.0/bin/bundler
                |   `-- hello-world -> /nix/store/hym2fy3yiy706rm5hmwg6gmyrg8zipqm-ruby2.6.6-hello-world-1.2.0/lib/ruby/gems/2.6.0/bin/hello-world
                |-- build_info
                |-- doc
                |-- extensions
                |-- gems
                |   |-- bundler-2.1.4 -> /nix/store/niy7ivnph74z02kf8cvh9c4kz0i70nqp-bundler-2.1.4/lib/ruby/gems/2.6.0/gems/bundler-2.1.4
                |   `-- hello-world-1.2.0 -> /nix/store/hym2fy3yiy706rm5hmwg6gmyrg8zipqm-ruby2.6.6-hello-world-1.2.0/lib/ruby/gems/2.6.0/gems/hello-world-1.2.0
                `-- specifications
                    |-- bundler-2.1.4.gemspec -> /nix/store/niy7ivnph74z02kf8cvh9c4kz0i70nqp-bundler-2.1.4/lib/ruby/gems/2.6.0/specifications/bundler-2.1.4.gemspec
                    `-- hello-world-1.2.0.gemspec -> /nix/store/hym2fy3yiy706rm5hmwg6gmyrg8zipqm-ruby2.6.6-hello-world-1.2.0/lib/ruby/gems/2.6.0/specifications/hello-world-1.2.0.gemspec

The gems directory, has the symlinks to every gem declared in the Gemfile through the gemset.nix.

Finally a custom bundler script is provided which makes sure to setup the correct GEM_HOME and Gemfile path.

#!/nix/store/arz0swkk693spw100q9d472816krr6x6-ruby-2.6.6/bin/ruby
#
# This file was generated by Nix.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] = "/nix/store/czr32qd8zl96yzcpmdjik8malfnzfhdp-gemfile-and-lockfile/Gemfile"
ENV.delete 'BUNDLE_PATH'
ENV['BUNDLE_FROZEN'] = '1'

Gem.paths = { 'GEM_HOME' => "/nix/store/ygzpgzxm25j6lfyad3zxr6rm2psahjlz-fzakaria.com/lib/ruby/gems/2.6.0" }

$LOAD_PATH.unshift "/nix/store/niy7ivnph74z02kf8cvh9c4kz0i70nqp-bundler-2.1.4/lib/ruby/gems/2.6.0/gems/bundler-2.1.4/lib"

require 'bundler'
Bundler.setup()

load Gem.bin_path("bundler", "bundler")

Voila! You can run the Ruby application.

nix-shell --pure

bundle exec ruby main.rb
> hello world!
> this is hello world library