Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.
Ruby Require Order Problems
April 17th, 2010
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:
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
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.
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
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 (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.