Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; his 9-to-5 home is at the startup he founded, Tilde Inc.. There he works on Skylight, the smart profiler for Rails, and does Ember.js consulting. He is best known for his open source work, which also includes Thor and Handlebars. He travels the world doing open source evangelism and web standards work.

Require Order

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 
end
 
# application.rb 
Bundler.require(:default) 
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" 
Bundler.require(:test)

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" 
Haml.init_rails(binding)

Bundler would handle this in Bundler.require.

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.

I’m not sure how this would work in Ruby 1.9, which eliminates gem activation in the case of the most recent version of a gem by adding the most recent version of all gems to the load path. I’m uncomfortable assuming
that simply because Rails is installed, an application that uses Haml wants the Rails integration. Again, this wouldn’t be an issue with 1.8 or Bundler, but is an issue with the way 1.9 handles gems.

Come to think of it, I’m not sure how 1.9 handles a situation where a most-recently-installed gem depends on an older version of a gem than the most recently available. Wouldn’t it happily require the gem and then explode at runtime?

Update: Yes. We filed a bug with Ruby: http://redmine.ruby-lang.org/issues/show/3140

Archives

Categories

Blogroll

Meta