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.
I install rubies with
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
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.
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.
Once I’ve got as many rubies as I can eat installed, I use
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
looks like this:
That gives me a totally contained environment with the
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
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
chruby has support for switching gem sets with
don’t use it. I prefer to have a
$GEM_HOME in each project
directory, right next to the source. This keeps everything nicely
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:
then you get the following directory structure:
1 2 3 4 5
.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:
bundler gem will then get installed into
If you’ve been following along, you can probably predict the next
step: running a subshell with
$GEM_HOME set properly. That’s simple,
If I’ve just launched a new terminal and want to load up the complete
environment for a project, combining the
calls gives me this:
Pick the ruby, set up the gemset, and launch the subshell. Simples.
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
That will create this structure:
1 2 3 4 5 6
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:
…and there’s my subshell with the right ruby and gemset selected.
You can leave
bash off the command if you want -
will default to launching
$SHELL. I’ve added the
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
make sure it’s locally ignored.
That’s really all there is to it. Counting up the lines of source
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.
bin/activate script in
$GEM_PATH to be the
$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
…which takes most of the pain away. Maybe I’ll add
gemsh at some point, but for now I just don’t feel the
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:
- turn a
- to install the contents of that
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.