How Does Rails Handle Gems?

A few weeks ago, I wrote about how RubyGems manages Ruby’s load path. But Rails doesn’t use RubyGems directly – it uses Bundler to manage its gems.

If you don’t know how Bundler works, the way gems get pulled into Rails might seem a little too magical. How does adding a line to a Gemfile get code into your app? How do Bundler, Rails, and RubyGems work together to make handling dependencies easy?

Why Bundler?

I think of Bundler as a strict gem manager. That is, Bundler helps you install the right versions of the gems you need, and forces your app to only use those versions.

This turns out to be really helpful. To understand why, you have to go back to the world before Bundler.

Before Bundler, it was still pretty easy to install the right versions of your gems with some kind of setup script:

bin/setup
gem install rails -v 4.1.0
gem install rake -v 10.3.2
...

(That is, as long as Rails 4.1’s dependencies don’t conflict with Rake 10.3.2’s dependencies!)

But what happens when you’re working on a few different Rails apps, each depending on different versions of gems? Unless you’re really careful, you’ll run into the terrible gem activation error:

gem_error
Gem::Exception: can't activate hpricot (= 0.6.161, runtime), 
already activated hpricot-0.8.3

Ugh. That message still gives me nightmares. It usually meant you’re in for a day of installing and uninstalling gems, so you can get just the right versions on that machine. And all it takes is an accidental gem install rake to completely mess up all of your careful planning.

rvm gemsets helped with this problem for a while. But they needed some time to set up, and if you accidentally installed into the wrong gemset, you’d be back to the same problem. With Bundler, you rarely have to think about your dependencies. Your apps usually just work. And Bundler takes a lot less setup than gemsets did.

So, Bundler does two important things for you. It installs all the gems you need, and it locks RubyGems down, so those gems are the only ones you can require inside that Rails app.

How does Rails use Bundler?

At its core, Bundler installs and isolates your gems. But that’s not all it does. How does the code from the gems in your Gemfile make it into your Rails app?

If you look at bin/rails:

bin/rails
#!/usr/bin/env ruby
begin
  load File.expand_path("../spring", __FILE__)
rescue LoadError
end
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require 'rails/commands'

You’ll see that it loads Rails by requiring ../config/boot. Let’s look at that file:

config/boot
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

Hey, it’s Bundler! (Also, I just learned you can choose a different Gemfile to use by setting the environment variable BUNDLE_GEMFILE. That’s pretty cool.)

bundler/setup does a few things:

  • It removes all paths to gems from the $LOAD_PATH (which reverses any load path work that RubyGems did).
  • Then, it adds the load paths of just the gems in your Gemfile.lock back to the $LOAD_PATH.

Now, the only gems you can require files from are the ones in your Gemfile.

So all the gems you need are on your load path. But when you use RubyGems by itself, you still have to require the files you need. Why don’t you have to require your gems when you use Rails with Bundler?

Take a quick look at config/application.rb, which runs after Rails boots:

config/application.rb
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

It’s Bundler again! Bundler.require requires all the gems in all the groups you pass to it. (By “groups”, I mean the groups you specify in your Gemfile.)

Which groups are in Rails.groups, though?

railties/lib/rails.rb
   # Returns all rails groups for loading based on:
    #
    # * The Rails environment;
    # * The environment variable RAILS_GROUPS;
    # * The optional envs given as argument and the hash with group dependencies;
    #
    #   groups assets: [:development, :test]
    #
    #   # Returns
    #   # => [:default, :development, :assets] for Rails.env == "development"
    #   # => [:default, :production]           for Rails.env == "production"
    def groups(*groups)
      hash = groups.extract_options!
      env = Rails.env
      groups.unshift(:default, env)
      groups.concat ENV["RAILS_GROUPS"].to_s.split(",")
      groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) }
      groups.compact!
      groups.uniq!
      groups
    end

Well, that explains that. Rails.groups is going to be [:default, :development] when you’re running Rails in development mode, [:default, :production] in production mode, and so on.

So, Bundler will look in your Gemfile for gems belonging to each of those groups, and call require on each of the gems it finds. If you have a gem nokogiri, it’ll call require "nokogiri" for you. And that’s why your gems usually just work in Rails, without any extra code on your part.

Know your tools

If you understand the tools you use well, it’ll be easier to work with them. So if you find yourself using something all the time, it’s worth taking a few minutes to dig into it a little more.

If you’re working in Ruby and Rails, you’ll use gems every day. Take the time to learn them well!

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments