Your friends at Viget present Extend, a Code & Technology Blog

Bundler Best Practices

Bundler is a great tool to have in the Ruby toolbox, but it's also a bit mysterious to some developers. "Oh cool, I put my gems in this file, bundle install, and that's it. Wait, what's this Gemfile.lock thing? Should that go in my repo? What's the difference between bundle install and bundle update? How do I install my gems when I deploy? Where are my pants?" Let's take a tour of Bundler and find the answers to some of these questions.

Check-in your Gemfile.lock

You've just created a shiny new Gemfile and specified your app's gems. You run bundle install, and Bundler creates a file named `Gemfile.lock`. What is that for?

According to the Bundler website,

Bundler manages an application's dependencies through its entire life across many machines systematically and repeatably.

Basically, Gemfile handles the "manages an application's dependencies" part and Gemfile.lock handles the "systematically and repeatably" part.

Here's what happens: when you run bundle install, Bundler installs those gems and records the version number for each one in Gemfile.lock. Now, Bundler has a record of the gem version of each dependency it is managing, and it can now check the gem store against this record when running future commands. More importantly, if you check out someone else's project and it has a Gemfile.lock file, Bundler will install the exact same gems the other developer is using.

Should you keep your Gemfile.lock in version control? Yes! It's a critical part of getting the most benefit from Bundler. By doing so, you guarantee that every developer working on the project and every deployment of the app will use exactly the same third-party code. How awesome is that?

Gem Development

Steve Loveless mentioned in the comments this post by Yehuda Katz that says you should not check-in your Gemfile.lock if you are using Bundler while developing a gem. The basic idea is: when developing an app, you want to lock all of your dependencies down so they cannot change, to ensure that if a dependency releases an incompatible update, your app is insulated. However, when developing a gem, you want to be aware of those incompatibilities, so you can resolve them.

Also, when developing a gem, you should specify dependencies in your gem's .gemspec. Your Gemfile should look like this:

source 'http://rubygems.org'

gemspec

This tells Bundler to use the .gemspec file when managing dependencies.

Thanks for the tip, Steve!

Install vs. Update

The most common question I've heard about Bundler is about the difference between bundle install and bundle update.

In a nutshell: bundle install handles changes to the Gemfile and bundle update upgrades gems that are already managed by Bundler.

Here's an example. Suppose your Gemfile looks like this:

gem 'httparty'
gem 'mocha'

When you run bundle install, httparty, mocha, and their respective dependencies will be installed.

Now, suppose you need to use an older version of mocha. You update the Gemfile and specify the version:

gem 'httparty'
gem 'mocha', '0.9.9'

bundle install will detect the updated Gemfile, install version 0.9.9 of mocha, and update the Gemfile.lock.

Now, let's say you decide not to use mocha at all. Remove the dependency from the Gemfile, run bundle install, and Bundler will remove mocha from the Gemfile.lock.

(Note that Bundler will install gems but it will not uninstall them. When you remove a dependency, you must uninstall the gems yourself.)

All of that was done with bundle install; so what does bundle update do? Let's say that when we first installed httparty, the latest version was 0.7.0. Now, the latest version is 0.7.4, and we'd like to upgrade our app to use that. Run bundle update httparty and Bundler will upgrade httparty to 0.7.4 and update the Gemfile.lock.

(Note that running bundle update with no arguments will upgrade all of your app's gems, which is almost certainly not what you want to do.)

Here are the rules:

  1. Always use bundle install
  2. If you need to upgrade a dependency that Bundler is already managing, use bundle update <gem>.
  3. Don't run bundle update unless you want all of your gems to be upgraded.

Deployment the Bundler way

When it's time to deploy, it's a good idea to install gems as part of your app, separate from the gems in the global gem store. This keeps your app isolated from changes to system gems and from other apps on the same box. This can be done with RVM and gemsets, but that is a lot of machinery to configure and maintain.

Bundler provides a simple way to do this, and with no work required: by running bundle install --deployment, Bundler will install gems into vendor/bundle instead of using the global gem store.

Better still, if you deploy with Capistrano, add require 'bundler/capistrano' to your Capfile and Bundler will automatically do this for you whenever you run cap deploy.

One thing to remember: if you run an executable from one of your gems as part of your deploy, use bundle exec (e.g. bundle exec jammit) to ensure that the executable from the bundled gem is used.

Bonus tip! Using the :path option

Here's a bonus tip, because I'm nice like that. Sometimes, you need to use a gem that isn't published anymore, or that has a bug, or that needs a modification specific to your app. How should you add it as a dependency?

One easy way to do this is to fork it on GitHub, make the changes, and use the forked repo's URL in the :git option when you specify the gem in the Gemfile. But maybe you don't want to keep that repo around for the lifetime of the app, or the source isn't on GitHub, or you don't want to publish your app-specific changes. In this case, a better solution would be to package the gem as part of your app.

Turns out, there's a simple way to do just that: the gem method takes a :path option, which is a path to the directory on the local file system containing the gem. Simply add the gem source to your project (I like to put it in vendor/gems/<name>) and then in your Gemfile, specify the path:

gem 'mocha', :path => File.join(File.dirname(__FILE__), 'vendor', 'gems', 'mocha')

Now the project comes packaged with the modified gem, instead of relying on an outside resource. Neat!

~ * ~

What are your favorite Bundler tips? Share them in the comments below.


Get More From Viget

Subscribe to get our monthly newsletter and occasional special announcements.