Technical topics (hopefully) explained in plain language.

Saturday, January 10, 2009

vendor everything should include Rubygems itself

There's a lot of talk about the vendor everything approach for Rails. That is, you put all gems that your Rails app depends on into vendor (also called freezing). The newer versions of Rails even include rake tasks to help with specifying gem dependencies and freezing them into vendor/gems.

The problem is, Rails is tightly bound to features in Rubygems itself. For example, the feature I mentioned above ("config.gem") only works for Rubygems version > 1.1.1. Even if you aren't using any Rubygem-ish features of Rails, it'll break for real old versions of Rubygems, such as
undefined method `loaded_specs' for Gem:Module (NoMethodError)
when using Rails 2.1.0 and Rubygems <= 0.9.0.

So why is this an issue?

First of all, it's a philosophical thing. We're trying to isolate our Rails app from system changes, right? We freeze Rails into vendor, we freeze gems into vendor, and yet we're still beholden to the Rubygems system? Why?

More practically, what if you're running on a host that has old software? Or, for reasons out of your control, you have to deploy your Rails app to an old system? In my case, I'm using Rails 2.1.0 but have to deploy to an Ubuntu Feisty (!) system. Feisty's Rubygems is 0.9.0.

What to do?

All of the "vendor everything" articles I've found just talk about vendoring the gems-- none of them ever talk about vendoring Rubygems itself (except, in a roundabout fashion, here).

To the extent that anyone addresses this issue, mostly they mention doing a "gem update --system" or somesuch. Sadly this doesn't usually work. It seems to break Rubygems on Feisty (assuming you even have rights to update Rubygems in the first place), and on newer systems you'll get this error:
gem update --system is disabled on Debian. RubyGems can be updated using the official Debian repositories by aptitude or apt-get.
which locks you into whatever the latest package is for your distro.

So I had to do this myself. It turns out its not that hard!

First we have to find where the Rubygems code is. If you look in /usr/lib/ruby/1.8/ you'll see a bunch of files. Then copy these files
rbconfig/
rubygems/
rubygems.rb
ubygems.rb
to vendor/rubygems (keeping the structure intact, of course).

Now we have to tell our Rails app to look in vendor for Rubygems rather than use the system version. We do this by adding vendor/rubygems to the load path ($:) variable:
RUBYGEMS_VENDORED = File.join(RAILS_ROOT, 'vendor/rubygems')
$:.insert(0, RUBYGEMS_VENDORED)
But where do we put this code? My first thought was to put it in config/environment.rb, but this doesn't work. It's not early enough. We need to put this at the top of config/boot.rb (despite what the "do not edit" comment says). The reason for this is that boot.rb does some Rubygems stuff before it loads environment.rb.

Now you should be able to run your Rails app anywhere-- even on systems that don't even have Rubygems installed. Your only system dependency should be Ruby itself (and possibly rake).


No comments:

Blogger Syntax Highliter