How require loads a gem

03 Nov 2017

In modern versions of Ruby, you can use the good old require method to load a gem. For instance, if you've got the gem activesupport installed, you can require everything inside of activesupport (including the kitchen sink) with this line:

require 'active_support/all'

You might've just tried to open up irb and run that line, and it might've worked for you... assuming you have activesupport actually installed. It works on my machine, at least.

But how does require know where to find gems' files in Ruby? Wouldn't those files need to be on the load path? Well, thanks to a cheeky hack in RubyGems code, no, those files don't need to be on the load path. Instead, these gems' lib directories are added to the load path as they're needed. I'll show you how.

A default load path

When you initialize irb it already has some directories added to its load path, which you can see with this code:

p $LOAD_PATH

My list looks like this:

[
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/gems/2.4.0/gems/did_you_mean-1.1.0/lib",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/x86_64-darwin16",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/site_ruby",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/vendor_ruby/2.4.0",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/vendor_ruby/2.4.0/x86_64-darwin16",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/vendor_ruby",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/2.4.0",
  "/Users/ryanbigg/.rubies/ruby-2.4.1/lib/ruby/2.4.0/x86_64-darwin16"
]

These paths make it possible for me to do things like require 'net/http' (haha just kidding I use rest-client) and require 'csv'. At least one of those directories contains files called net/http.rb and csv.rb which makes this possible.

But none of these directories include a file called active_support/all, so how does require 'active_support/all still work?!

The cheeky hack

The "cheeky hack" in the bundled RubyGems code is shown here in all its glory. The comment at the top of this file gives away what happens:

When you call require 'x', this is what happens:

  • If the file can be loaded from the existing Ruby loadpath, it is.
  • Otherwise, installed gems are searched for a file that matches. If it's found in gem 'y', that gem is activated (added to the loadpath).

I won't walk through the whole thing -- consider it homework! -- but the short version is that RubyGems checks to see if there are any unresolved dependencies and if there's not, then it will try a regular require. This results in a LoadError being raised, which is then rescued a little further down.

This error message is checked to see if it ends with the path that we passed in, and if it does then it calls Gem.try_activate(path). This method will activate any gem that matches the specified path. Inside of the activesupport gem, it has a file called 'active_support/all', and so the activesupport gem will be activated here.

Activating a gem adds that gem's lib directory to the load path, which will then make requiring any of that gem's files possible.

Once the gem is activated, this require method tries to require the path once more. Due to the gem being activated, it is now possible to require 'active_support/all'.

blog comments powered by Disqus