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.

Rubygems Good Practice

Rubygems provides two things for the Ruby community.

  1. A remote repository/packaging format and installer
  2. A runtime dependency manager

The key to good rubygems practice is to treat these two elements of Rubygems as separate from each other. Someone might use the Rubygems packaging format and the Rubygems distribution but not want to use the Rubygems runtime.

And why should they? The Rubygems runtime is mainly responsible for setting up the appropriate load-paths, and if you are able to get the load paths set up correctly, why should you care about Rubygems at all?

In other words, you should write your libraries so that their only requirement is being in the load path. Users might then use Rubygems to get your library in the load path, or they might check it out of git and add it themselves.

It sounds pretty straight-forward but there are a few common pitfalls:

Using gem inside your gems

It’s reasonably common to see code like this inside of a gem:

gem "extlib", ">= 1.0.8"
require "extlib"

This should be entirely unnecessary. While using Kernel.gem in an application makes perfect sense, gems themselves should use their gem specification to provide dependent versions. When used with Rubygems, Rubygems will automatically add the appropriate dependencies to the load path. When not using Rubygems, the users can add the dependencies themselves.

Keep in mind that whether or not you use Rubygems, you can use require and it will do the right thing. If the file is in the load path (because you put it there or because Rubygems put it there), it will just work. If it’s not in the loadpath, Rubygems will look for a matching gem to add to the load path (by overriding require).

Rescuing from Gem::LoadError

This idiom is also reasonably common:

begin
  gem "my_gem", ">= 1.0.6"
  require "my_gem"
rescue Gem::LoadError
  # handle the error somehow
end

The right solution here is to avoid the gem call, as I said above, and rescue from plain LoadError. The Rubygems runtime sometimes raises Gem::LoadError, but that inherits from regular LoadError, so you’re free to rescue from that and catch cases with and without the rubygems runtime.

Conclusion

Declare you gem version dependencies in your gem specification and use simple requires in your library. If you need to catch the case where the dependency could not be found, rescue from LoadError.

And that’s all there is to it. Your library will work fine with or without the Rubygems runtime :)

7 Responses to “Rubygems Good Practice”

There was a rant along these lines some months ago. I gave me one of those *HEADDESK* “OMG you’re right!” type epiphanies and I’ve been doing exactly this ever since. It’s easy. Listen to this man!

One thing to address would be the reason people do the ‘gem “abcd”, “>=1.0.2″‘ in their lib file.

The most common reason is while development, but the way to solve this is to use the gem specification and activate the dependencies in the Rakefile and spec/spec_helper.rb.

Using a Gemfile like in the wycats/bundler and then something like: ‘Gem::Manifest.activate’ would do nicely.

I really like this idea, it also enables others to simply use the gem even if they do not seem to have the correct version of another gem installed, or to unpack a gem etc.

Why are you using loading of mocha gem in ActiveSupport TestCase class definition?
http://github.com/rails/rails/commit/7583a24ee0ea85d55a5e235c3082f1b67d3d7694

It seems not according to your recommendation here :)

Because of rescuing of LoadError I several time had difficulties to understand why ActiveRecord unit tests are failing with strange MethodMissing errors.

Why not fail early if rubygems is not there? It will be just another dependency.
Also in ruby1.9 rubygems is built-in right?

If we try to gracefully handle a rubygems error, do i need to know about how ruby gems resolves the dependencies? Although, in my opinion it would be worthwhile to not use rubygems only if it decreases the memory consumption.

i looked at:
http://github.com/fabien/minigems/tree/master
http://rubyforge.org/pipermail/rubygems-developers/2008-July/003988.html

i think this is a bit pre-mature. specifically i think the approach of pushing gem’s load paths onto $LOAD_PATH is massively flawed and predict it will be removed. i have already had issues with it several times where gem ‘a’ finds a resource erroneously in gem ‘b’s directory. it would be trival (like simply making the gem call take a block – doh!) for rubygems to do a push/pop of the load path during gem loading to provide isolation during loading for guaranteed dependency resolution and i’ve hacked many dep problems into submission be doing precisely this.

if you consider the ramifications (which i think few even understand) that the current gem loading has, which is that:

“requiring dependency ‘A’ means that that client code should then look for each and every other resouce *first* in ‘A’s private lib directory regardless of whether is has anything to do with ‘A’ or not” is just so patently wrong i think it will not stand the test of time — saying that i want to load gem ‘A’ doesn’t mean, to any sane person, that i want to load any other gem/lib i require from gem ‘A’s shit if it happens to provide it.

a larger reason to specify the gem version is that gem authors are bad, really, really bad, at understanding the semantics of version numbers – specifically with regard to interface/implementation issues. i seriously doubt if 1 in 50 ruby developers could explain the ’1′, ’4′, and ’2′ in 1.4.2. rails itself has suffered badly from this – otherwise you’d be working on rails 7.0 or something ;-) github has compounded this issue by forcing developers to bump version numbers to force a gem build without really specifying how or why – just that it should be ‘higher’.

most of the shops i’ve worked at have literally had to re-invent rubygems packaging or a least maintain their own gem servers for just this reason.

i agree with you regarding sane rescuing of LoadError and minimizing over specification of versions though.

For the LoadError stuff, how about use Gem.available? instead?

Matt

Leave a Reply

Archives

Categories

Meta