2 min read

Ruby Require Order Problems

Bundler has inadvertantly exposed a number of require order issues in existing gems. I figured I'd take the opportunity to talk about them. There are basically two kinds of gem ordering issues:

Missing Requires

Imagine a gem that uses nokogiri, but never requires it. Instead, it assumes that something that is required before it will do the requiring. This happens relatively often with Rails plugins (which were previously able to assume that they were loaded at a particular time and place).

A well-known example of this problem is gems that didn't require "yaml" because Rubygems required it. When Rubygems removed its require in 1.3.6, a number of gems broke. In general, bundler assumes that gems require what they need. If gems do so, this class of ordering problems is eliminated.

Constant Definition as API

This is where a gem checked for defined?(SomeConstant) to decide whether to activate some functionality.

This is a bit tricky. Essentially, the gem is saying that the API for using it is "require some other gem before me". Personally, I consider that to be a problematic API, because it's very implicit, and can be hard to track down exactly who did what require.

A better solution is to provide an explicit hook for people to activate the optional functionality. For instance, Haml
provides: Haml.init_rails(binding) which you can run after activating Haml.

This is slightly more manual than some would like, but the API of "make sure you require the optional dependencies before me" is also manual, and more error-prone.

Even if Bundler "respected require order", which we plan to do (in some way) in 0.10, it's still up to the user of the gem to ensure that they listed the optional gem above the required gem. This is not ideal.

A workaround that works great in Bundler 0.9 is to simply require the order-dependent gems above Bundler.require. We do this in Rails, so that gems can test for the existence of the Rails constant to decide whether to add optional Rails dependencies.

require "rails/all" 
Bundler.require(:default, Rails.env) 

In the case of shoulda and mocha, a better solution could be:

# Gemfile 
group :test do 
  gem "shoulda" 
  gem "mocha" 
  # possible other gems where order doesn't matter 

# application.rb 
Bundler.require(Rails.env) unless Rails.env.test? 

# test_helper.rb 
# Since the order matters, require these gems manually, in the right order 
require "shoulda" 

In my opinion, if you have gems that specifically depend on other gems, it is appropriate to manually require them first before automatically requiring things using Bundler.require. You should treat Bundler.require as a shortcut
for listing out a stack of requires.

Possible solutions?

One long-term solution, if we get gem metadata in Rubygems 1.4 (or optional dependencies down the line), would be to specify that a gem has optional dependencies on another gem and specify a file to run both gems are available.

For instance, the Haml gem could say:

# gemspec 
s.integrates_with "rails", "~> 3.0.0.beta2", "haml/rails" 

# haml/rails.rb 
require "haml" 
require "rails" 

Bundler would handle this in Bundler.require (or possibly Bundler.setup).

If this feature existed in Rubygems, it would work via gem activation. If the Haml gem was activated after the Rails gem, it would require "haml/rails" immediately.

If the Haml gem was activated otherwise, it wouldn't do anything until the Rails gem was activated. When the Rails gem was activated, it would require the file.