Better Code

Swimming in Someone Else's Pool

How I Ruby, Part 1: Development

| Comments

In this article I’ll describe the toolset I use to do ruby development. It’s not complicated. It is pleasingly robust. I’ve hacked together a couple of simple tools to make it easier.

I do all my development (in fact, all my everything) on Debian stable (currently Wheezy), but everything here should apply equally to Ubuntu and, with a following wind, OS X.

ruby-install

I install rubies with ruby-install from here. This is a simple installer which doesn’t need re-installing or upgrading whenever a new ruby release comes out.

When run as root, it installs an interpreter under /opt/rubies. As a non-root user, they go under $HOME/.rubies. For development on my own machines, I typically don’t care which of these I go for.

ruby-install will also happily install to a different chosen location, which allows for a neat trick I’ll come back to later.

I like ruby-install because it takes care of remembering which system dependencies each ruby needs. I can never remember the exact list of Debian packages MRI needs to work properly out of the box, so this avoids a google trip each time I set up a new machine.

chruby

Once I’ve got as many rubies as I can eat installed, I use chruby from here to select which one is active at any given time. I don’t use the chruby function to do so, though. The advice given here is good. I do all my ruby project work in subshells, which I launch with chruby-exec. It looks like this:

1
chruby-exec 1.9.3-p429 -- bash

That gives me a totally contained environment with the $PATH set correctly to use the chosen ruby. This, to me, is a far better way to arrange the environment than relying on a function. The problem with using a function is that when you want to switch back to your original settings, you’re relying on the function being able to accurately undo the changes it made. If you’ve got anything else modifying the same environment variables, the chances of getting this wrong go up dramatically.

With a subshell, when I want to switch environments all I need to do is kill that subshell. Ctrl-d. Any changes I (or any other tool) have made are just thrown away, there’s no need to track any state. The top-level shell is treated as immutable, so you can never get stuck.

gemsh

While chruby has support for switching gem sets with chgems, I don’t use it. I prefer to have a $GEM_HOME in each project directory, right next to the source. This keeps everything nicely separated. gemsh (from here) is a little tool I wrote along the same sorts of lines as Python’s virtualenv to set this up for me. If you run this:

1
gemsh .gems

then you get the following directory structure:

1
2
3
4
5
.gems/
  bin/
    activate
    exec
  gem_home/

.gems/gem_home is where gem install will install gems to. .gems/bin/activate is a chunk of shell script you can source into a shell to set the environment variables to make that happen. .gems/bin/exec is an executable script which sources .gems/bin/activate, and then exec’s its arguments.

All very simple. Here’s how I might install a gem:

1
.gems/bin/exec gem install bundler

…and the bundler gem will then get installed into .gems/gem_home.

If you’ve been following along, you can probably predict the next step: running a subshell with $GEM_HOME set properly. That’s simple, too:

1
.gems/bin/exec bash

If I’ve just launched a new terminal and want to load up the complete environment for a project, combining the chruby-exec and gemsh calls gives me this:

1
chruby-exec 1.9.3-p429 -- .gems/bin/exec bash

Pick the ruby, set up the gemset, and launch the subshell. Simples.

Simpleserer

I got bored of typing out chruby-exec.... .gems/bin/exec... every time, so I wrote a little tool to wrap the two together. It’s called rv, and you can get it here. To set up a project, you call rv-init with the ruby version you want to use:

1
rv-init 1.9.3-p429

That will create this structure:

1
2
3
4
5
6
.rv/
  1.9.3-p429/
    bin/
      exec
      activate
    gem_home/

Under the bonnet, rv-init just calls gemsh. Now, if you want to be able to run your project under more than one version of ruby, you can just run rv-init again, and give it a different version string.

Now when I come to a project, I run:

1
rv 1.9.3-p429 bash

…and there’s my subshell with the right ruby and gemset selected. You can leave bash off the command if you want - rv 1.9.3-p429 will default to launching $SHELL. I’ve added the $VIRTUAL_ENV variable (set by gemsh) to my prompt, so I can tell when I’ve got a project environment activated.

The only thing remaining is to add .rv/ to .git/info/exclude to make sure it’s locally ignored.

That’s it

That’s really all there is to it. Counting up the lines of source across chruby, rv and gemsh, that’s as complete a ruby development environment manager as I need, in a couple of hundred lines of bash.

This simplicity is entirely intentional. The more time passes, the more I find myself believing in small tools doing one thing well.

Now, for what this setup doesn’t handle. The two features which play on my mind the most are $GEM_PATH support and auto-activation of project environments.

$GEM_PATH support is needed if, for instance, you want to install a single copy of bundler and have access to that copy from whichever environment you’re working in. It helps prevent duplicate installs (which waste space), and gives a separation between the tools you’re using to work on a project and the code dependencies of that project. The bin/activate script in gemsh hardwires $GEM_PATH to be the same as $GEM_HOME, so you really don’t have access to anything outside the project. This hasn’t irritated me enough yet to do anything about. You can pass a list of gems to install to rv-init like this:

1
rv-init 1.9.3-p429 bundler rails

…which takes most of the pain away. Maybe I’ll add $GEM_PATH support to gemsh at some point, but for now I just don’t feel the need.

As far as auto-activation of project environments goes, I don’t like the idea in concept, and have never really felt the need for it. Maybe it’s because most of the projects I work on don’t have a single “default environment”. I’m always testing against another version of ruby, either to try to flush out bugs (running threaded code on 1.8 is unexpectedly useful for this, it’s got a handy deadlock detector) or as preparation for the inevitable N+1 production ruby upgrade.

If I did feel the need for an auto-environment switcher, I’d probably reach for something like autoenv.

The neat trick with ruby-install

The fact that ruby-install doesn’t get between you and the $PREFIX-sensitivity of ruby’s install process means that we can, in principle, install ruby itself into the .rv directory. This is another trick borrowed from virtualenv; I’ve not tried it out yet, but there’s something very enticing about having the project, the interpreter and all its dependencies inside a single directory.

I’m planning on trying this in an experimental rv branch, but if someone else felt like trying it out first…

A final word on Bundler

Without mincing words, managing gem versions is a pain. I use bundler to install gems and lock their versions BUT I don’t use bundler’s cache, its binstubs, or anything other than to use it to:

  1. turn a Gemfile into a Gemfile.lock; and
  2. to install the contents of that Gemfile.lock into a $GEM_HOME.

I find this reduces the number of things that can possibly go wrong to a tolerable level.

I still believe that these two separate jobs should be done by small, independent tools, but that’s a thought for a future post.

ruby