First Class vendor/gems

A common way to add gems to vendor/gems is by using the gem unpack command.
cd vendor/gems
gem unpack the_gem_name

And then loading them up using an approach like this, this, or something like this:

Dir.glob(File.dirname(__FILE__) + "/../vendor/gems/*").each do |path|
  gem_name = File.basename(path.gsub(/-[\d\.]+$/, ''))
  $LOAD_PATH << path + "/lib/"
  require gem_name
end

This feels like it should be unnecessary since Rubygems comes with a method to load a gem (gem). It's really a second class way of using gems.

Here's a solution I've been using for a while to keep gem code in vendor/gems, but still load them using the regular gem method.

First, a rake task to install gems.

namespace :gem do
  desc "install a gem into vendor/gems"
  task :install do
    if ENV["name"].nil?
      STDERR.puts "Usage: rake gem:install name=the_gem_name"; exit 1
    end
    sh "gem install #{ENV['name']} --install-dir=vendor/gems --no-rdoc --no-ri"
  end
end

Then, a little Rubygems magic to change GEM_PATH (I placed it at the top of environment.rb).

require "rubygems"
Gem.clear_paths
gem_paths = [
  File.expand_path("#{File.dirname(__FILE__)}/../vendor/gems"),
  Gem.default_dir,
]
gem_paths << APPLE_GEM_HOME if defined?(APPLE_GEM_HOME)
Gem.send :set_paths, gem_paths.join(":")

Finally, using gems works in typical Rubygems fashion.

# somewhere in the code base
gem "hpricot", "0.6"
require "hpricot"

I like this gem install approach better than the gem unpack one. Gem dependencies are being added to Rails in 2.1, which should make this unnecessary. When I first looked at the code, it was using gem unpack, but also extracting the gem specification. I'm not sure if setting gem_path like this could simplify the Rails implementation or not.

I only tested this approach on a Rails 2.0.2 app, using Rubygems 0.9.5 and 1.1.1. One additional benefit is that if you want to make sure your app doesn't depend on any locally installed gems, you can make vendor/gems the only directory in the gem path (although CruiseControl should be helping you with that problem anyway).