Harry Marr

Rubies and Bundles

RVM has long been an essential tool in the Rubyist’s toolbox. It was a huge leap forwards, but it has been increasingly falling out of favour. Recently, a large chunk of the Ruby community moved to rbenv. Touted as the nimble successor to RVM, rbenv has a much leaner feature set, and takes a rather different approach to managing multiple Ruby versions. Whereas RVM would override shell builtins such as cd, rbenv provides shims - small executables that select the correct Ruby version when run.

I won’t go over all the differences between RVM and rbenv; it’s a topic that has been covered many times before.

For better or worse, I made the switch to rbenv. It was a relatively simple transition, but there were a few gotchas along the way.

Gemsets and Bundler

RVM provided gemsets - isolated collections of gems, that make it possible to install separate versions of gems. This is one of the many RVM features that you won’t find present in rbenv.

Most modern Ruby projects use bundler to manage their dependencies. The naive way of using bundler with RVM is to create a gemset per project, and use bundler to install the project’s dependencies in to the gemset.

$ git clone git://github.com/person/some-project
$ cd some-project
$ rvm gemset create some-project
$ bundle install

However, bundler allows you to install the dependencies in a directory of your choice, rather than dumping them in $GEM_HOME. This means you can have a bundle directory per-project, obviating the need for gemsets.

$ git clone git://github.com/person/some-project
$ cd some-project
$ bundle install --path=.bundle

When you invoke bundle install with the --path option, a config file will be placed in the .bundle directory. Bundler reads this config file to figure out where the gems are stored.

Note: for projects that don’t use bundler, there is a plugin that adds gemsets to rbenv. Although I’d suggest skipping that and setting up bundler instead.

bundle exec everything

This setup works well, but has one annoying side effect: any binaries that are installed (e.g. rspec, foreman, cap) are no longer available in your shell’s path. To run them, bundle exec must be prepended to the command. These binaries wouldn’t work as rbenv shims for a couple of reasons. Firstly, rbenv isn’t aware of the binaries unless they’re installed to the standard location. Secondly, even if rbenv could find the binaries, it wouldn’t know how to load the rest of the gems in the bundle.

Fortunately, bundler provides a mechanism for generating its own shims. When bundle install is run the with the --binstubs option, a ‘stub’ will be created for every installed binary, that initialises the bundle, making the other gems available.

$ bundle install --binstubs=.bundle/binstubs
$ ./.bundle/binstubs/rspec spec

After adding the binstubs directory to the PATH, the binaries may be run exactly as you’d expect:

$ echo "PATH=.bundle/binstubs:$PATH" >> ~/.bashrc
$ source ~/.bashrc
$ rspec spec  # yay!

Now the only nuisance is remembering to run bundler with the --path and --binstubs options. Conveniently, both of these options can be set in the global Bundler config file:

$ cat ~/.bundle/config
---
BUNDLE_PATH: .bundle
BUNDLE_BIN: .bundle/binstubs

And that’s it. It requires a little more understanding than RVM does, but it feels like a much cleaner solution overall. If you’ve come up with a different solution to this problem, I’d love to hear it.