Saturday, January 8, 2011

More hoboization

I was playing with Processing.js and decided that it would be easier to use a Rails server to fiddle with the prototypes than installing an apache server or ftping to a host.  So I returned to one of my trial apps, leapercan, and tried to run the server.  It bombed out.

Yet I hadn't touched that code tree! When side-effects happen just because, is an obvious sign that you have violated (or never adopted) the axiom of closure. I happen to think closure is a good thing in general, when you can get it. I'd much rather trade extra disk space to get it than my time in debugging strange errors.

It happens that we use RVM, the Ruby Version Manager. We also use Bundler. What we haven't been using is RVM's Gemsets. Other developers with whom I've been pairing with over the past few months maintain a practice of always using the @global gemset, and use Bundler to maintain the gems. In this practice, RVM is relegated to maintaining only distinctive Ruby versions.  This is supposed to work in theory because Bundler version control for the gems used by an application.

The root cause for the failure?  My Leapercan application was based on Hobo 1.0.x, using Rails 2.x and Ruby 1.8.7. With a Splotchup application I tried out Hobo 1.3.x, using Rails 3.x and Ruby 1.8.7. Starting to see the trouble yet?

Bundler is fine for fetching and installing specified versions of gems. But in practice I have found that it did not maintain two different versions of the same gem, each for a different application. I'm told that it should, and that the ".lock" file should fixate the versions of the gems in use. But when I switched my working context, things just broke. My working hypothesis is that my Gemfile did not specify a version for some gems, and the use of a single gemset allowed bundling from one app to affect the gems v another app.

Using @global gemset is a good idea when you run a single Ruby version for a single application, and want to save space. It is a bad idea otherwise.

My solution? Impose closure. I don't care if I lose a little, or a lot, of disk space with separate gem copies. I want my application development and source tree completely and utterly self-contained and self-consistent. The way to accomplish closure is to get rid of global defaults and spurious sharing.
  1. Don't rely on the System ruby. Unfortunately, if you've done anything interesting already you'll have to hack some directories to get rid of the system gems. Ironically enough Mac OSX lacks even a half-baked GUI for managing system add-on components. It would be better to fix RVM to avoid the real problem, which is that it does not sandbox out the system gems, but I had already done the removal before thinking of that approach.

    sudo mv /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8 \/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8.default
    sudo mkdir  /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8 
  2. Use RVM to install each Ruby version you will use

    rvm install 1.8.7
    rvm install 1.9.2

  3. For each application, create a gemset; empty the global gemset for the ruby it will use.

    rvm use 1.8.7
    rvm gemset empty global
    rvm gemset create splotchup
    rvm gemset create leapercan

  4. When you start developing, switch to the gemset automatically

    cd ~/workspace/splotchup
    echo "rvm use ruby-1.8.7@splotchup" > .rvmrc

    cd ~/workspace/leapercan
    echo "rvm use ruby-1.8.7@leapercan" > .rvmrc
  5. Install Bundler, then let Bundler take care of the rest; avoid using gem to install gems other than initially installing bundler.

    cd ~/workspace/splotchup
    gem install bundler  # if you haven't already
    bundle install

    cd ~/workspace/leapercan
    gem install bundler
    bundle install

  6. If you maintain different mainline branches of development (an unnecessary complication in my book), create and use a corresponding empty gemset.
There now. My apps are completely isolated from one another. A purist might object that I'm duplicating a lot of code here, but there are two answers to this:
  • So What? Most ruby gems are small. The supposed space saving is a false economy, since the bugs introduced by violating closure cost much more. 
  • There is no duplication and this is not a violation of DRY. Application-specific gemsets are necessary and sufficient because the gems are a factorable part of the application source, not universal attributes of the environment.

No comments: