Ruby Virtualenvs

A while back I found a command that removes all Ruby gems installed on a system when you’re using rbenv. It worked great, so I decided to build on top of it. After a bit of research, I found a much better solution to the root of my problems: sandboxing Ruby gems.

Ugh, Ruby…

If you’re anything like me, you can never do anything right on the first try using Ruby. At one point, I found myself needing a script to just nuke everything and start over… That’s when I found Ian Vaughan’s script that magically removes all gems. I was delighted to see that it worked perfectly on the first try, and went about the rest of my business.

Modifications

There were two ways though in which this script’s functionality differed from what I wanted it to do: it always removed all gems, and it left behind a .ruby_version file after it was used, clobbering any file that might have been there before.

In my updated script, you can specify a list of ruby versions as arguments, and it will only gems from those versions instead of all of them. Also, it saves and restores the value of the old .ruby_version file once it’s done.

The new script is available as a fork of the original Gist and also as a part of of my personal bin folder.

The Underlying Problem: Virtualenv’s in Ruby

After a bit of reflection, I realized I should be trying to solve the underlying problem: different projects had different dependencies, and gems from one project were bleeding into gems from another. If you’re a Python developer, you don’t have this issue: virtualenvwrapper, pip, and requirements.txt files make this a non-issue.

After looking into if there existed a similar Ruby solution, I came up with this blog post outlining how you can do the exact same thing using virtualenvs but with Ruby gems! Once again, it needed a little bit of modification so that everything works again as you’d expect when you deactivate. Add these lines to your virtualenv’s postactivate script:

export OLD_GEMHOME="$GEM_HOME"
export GEM_HOME="$VIRTUAL_ENV/gems"

export OLD_GEM_PATH="$GEM_PATH"
export GEM_PATH=""

export OLD_PATH="$PATH"
export PATH="$GEM_HOME/bin:$PATH"
$VIRTUAL_ENV/bin/postactivate

And then add this complementary section to your predeactivate script:

export GEM_HOME="$OLD_GEM_HOME"
unset OLD_GEM_HOME

export GEM_PATH="$OLD_GEM_PATH"
unset OLD_GEM_PATH

export PATH="$OLD_PATH"
unset OLD_PATH
$VIRTUAL_ENV/bin/predeactivate

Now, whenever you install gems, they’ll install to the folder $VIRTUAL_ENV/gems/ instead of the system’s location, so no gems bleed into another project!

One Step Further

Bringing up this web page, copying those snippets, and pasting them in the two necessary files every time is a bit tedious. To automate this process, we can tap into virtualenvwrapper’s configurability using hooks. Instead of dropping those snippets into $VIRTUAL_ENV/bin/{post,prede}activate,, place them in $VIRTUALENVWRAPPER_HOOK_DIR/{post,prede}activate.

Now every time you workon a virtualenv, the appropriate configuration will be set up. Note that this means every normal Python project you use will have this Ruby configuration added (not just the Ruby projects), but that shouldn’t matter because they interoperate nicely. If it’s really an issue, you can stick with the per-virtualenv solution above.

Note: a side effect of this nice sandboxing is that you can normally run commands without prefixing them with bundle exec ..., which is actually really handy.