Rails Gem Dependencies and Plugin Errors

Rails 2.1 introduced the ability to specify which RubyGems are required by your application, making it much easier to replicate an environment across developer machines and servers. In environment.rb you can list dependencies as follows:

Rails::Initializer.run do |config|
  config.gem 'haml', :version => '>= 2.0.1' #any version >= to 2.0.1
  config.gem 'pdf-writer', :lib => "pdf/writer" #latest version
  config.gem 'will_paginate', :version => '2.2.2' #identical version
end

However, if you have a plugin dependent on a gem which is not yet installed, you’ll more than likely see something like this when running the new rake gems command.

rake aborted!
no such file to load -- some_missing_gem

For example, during an upgrade of a Rails 1.x project which uses the Railspdf plugin, a reference to ‘pdf/writer’ led to the aborted rake task.

vendor/plugins/railspdf/lib/railspdf.rb:

require 'pdf/writer'

Usually, running a rake task like rake gems initializes all Rails plugins. How do we know this? Adding “—trace” to the end of your rake task will show you the backtrace of everything that’s happening. You’ll notice that the framework calls rails/railties/lib/initializer.rb, responsible for loading plugins. So in this example, while Rails was trying to load the Railspdf plugin, it could not find the required pdf-writer gem.

A quick fix is to update the plugin to check for the presence of the gem:

vendor/plugins/railspdf/lib/railspdf.rb:

require 'pdf/writer' if defined? PDF::Writer

(Thanks to Dan Munk for this tip!)

The disclaimer here being that you should make rake gems a required part of your application deployment process. Otherwise, your plugin functionality will fail at runtime if the required gem is missing. Interestingly, Rails does a check for missing gems upon startup but does not prevent Mongrel from running.

Halting Mongrel if Gem Dependencies are Not Met

So what if you do want Mongrel to abort if any dependencies are missing? If your application includes a frozen copy of the Rails framework, you can edit vendor/rails/railties/lib/initializer.rb by adding a call to ‘abort’ method (an alias for Process::abort) to the existing check_gem_dependencies method.

def check_gem_dependencies
  unloaded_gems = @configuration.gems.reject { |g| g.loaded? }
  if unloaded_gems.size > 0
    @gems_dependencies_loaded = false
    # don't print if the gems rake tasks are being run
    unless $rails_gem_installer
      puts %{These gems that this application depends on are missing:}
      unloaded_gems.each do |gem|
        puts " - #{gem.name}"
      end
      puts %{Run "rake gems:install" to install them.}
+     abort
    end
  else
    @gems_dependencies_loaded = true
  end
end

If you’d like to take a less intrusive approach (or aren’t including a copy of Rails in the vendor directory), you can monkey patch check_gem_dependencies within your application.

Create a new file in app/config named check_gems.rb and add the following:

unless defined?(Rake)
  module Rails
    class Initializer
      alias old_check_gem_dependencies check_gem_dependencies
 
      def check_gem_dependencies
        old_check_gem_dependencies
        unloaded_gems = @configuration.gems.reject { |g| g.loaded? }
        abort if unloaded_gems.size > 0
      end
    end
  end
end

Then, add the following line to environment.rb:

require File.join(File.dirname(__FILE__), 'check_gems')

The net result is that if any ‘unloaded’ gems are found, the current Ruby process will end.

For those of you wondering if the above could be added to a Rails initializer, the answer is no. The current version of the Rails Initializer class will not load any application-specific initializers if any gem dependencies are not met.

Additional Rails 2 Resources

The PeepCode Rails 2.1 PDF has good coverage about Gem dependencies and freezing Rails.

Initializers are discussed in Stop Littering In Your Environment File.

Monkeypatching has an entry in the Wikipedia and is covered from a Rails perspective in The Virtues of Monkey Patching.


About this entry