Skip to content

Instantly share code, notes, and snippets.

@glv
Last active March 16, 2018 14:50
Show Gist options
  • Save glv/6004197 to your computer and use it in GitHub Desktop.
Save glv/6004197 to your computer and use it in GitHub Desktop.
This is a thing I wrote over a year ago for the internal LivingSocial wiki. There's nothing LS-specific about it, and it seems generally useful, so …

Trouble-Free Bundler

Every couple of weeks, I hear someone complaining about some difficulties with Bundler. Yesterday, it happened twice. But somehow I just never have those difficulties. I'm not saying Bundler is perfect; certainly in its early days it wasn't even close. But for the past two years it's been incredibly solid and trouble-free for me, and I think a large part of the reason is the way I use it. Bundler arguably does too much, and just as with Git, a big part of it is knowing what not to do, and configuring things to avoid the trouble spots.

My Strategy

This is my strategy for using Bundler, and for me it's always been smooth sailing. Give it a try.

First, the quick summary:

  1. Run bundle config path .bundle from your home directory1 2
  2. Run bundle install in all of your projects
  3. Always run bundle (not bundle update) unless you're explicitly upgrading gem versions.
  4. Optionally, stop using gemsets.

The rest of this section is a longer version, explaining the rationale. (This is essentially the same as the "Recommended Workflow" in the bundler-update man page, but not as terse. If you like terse, go type bundle help update instead.)

Step 1: Configuring Bundler

Right now, run bundle config path .bundle from your home directory.1 2 This adds a line to your user-global bundler configuration file (~/.bundle/config). The effect is that bundle install will always run as if you'd specified the --path .bundle option. What does that do? It tells Bundler not to install gems in your default gem repository (whether that's a gemset or not); rather, Bundler will create a project-specific gem repository in the .bundle directory and manage gems there.

This is an example of what Bundler calls "remembered option"—it's a sticky setting. Bundler remembers that you asked for a project-specific gem repository for this project, so any future bundler commands in the project will automatically operate within .bundle.

Step 2: Updating projects to obey the new configuration

Now go run bundle install in all of your projects.

Step 3: How to handle Gemfile changes

Never run bundle update. (Well, almost never ... see below.) Instead, whenever you update the Gemfile, or when Bundler tells you a gem is not found, just type bundle.

What's the difference? bundle update ignores Gemfile.lock. It looks at all of the project's gems and updates them to the latest, supposedly greatest versions that are compatible with the specified version constraints and that satisfy all of the transitive dependencies, and then regenerates Gemfile.lock. Even if the project has carefully locked down all of the Gem versions in Gemfile, this can result in upgrading some of those gems' dependencies, which is both a nuisance (because now everyone on the project has to go through a more time-consuming update of multiple gems) and dangerous (because it will happen in production as well, and one of those upgrades could break something).

In contrast, bundle is the same as bundle install, which honors Gemfile.lock. The job of bundle install is simply to make the smallest number of changes necessary to make everything consistent. That means first resolving any changes you made to Gemfile, and making the minimal necessary changes to Gemfile.lock to reflect those changes. Then it installs, upgrades, or removes any gems necessary to make your project gem repo match what's specified in Gemfile.lock exactly.

If you're the one who made the change to the Gemfile, you'll need to run bundle (or bundle install, if you prefer being explicit).3 But if someone else made the change and you just pulled it down, you can run bundle --local. The gems will all be cached in vendor/cache within the project, and bundle --local simply uses the cached files and avoids the time-consuming calls to rubygems.org and other gem sources. If in doubt, you can just run bundle --local; if the right gems aren't in vendor/cache it will fail quickly, and then you can run the more time-consuming bundle.

Step 4: Consider abandoning gemsets

Now, if you'd like, you can stop using gemsets and get rid of all of them. I recommend that you give it a try.

How do you do that, when every project's .rvmrc file instructs RVM to create and use a gemset? I did it by switching to rbenv (and I didn't install rbenv's gemset plugin). If you want to stick to RVM, do this:

echo "export rvm_ignore_gemsets_flag=1" >> ~/.rvmrc

and then exit your shell and restart (or whatever it is you do to get RVM to reload its configuration).

You don't have to stop using gemsets. If you do just the first three steps, but decide to continue using gemsets for your own purposes, that's great and things will work fine. But if your big reason for using gemsets had to do with managing bundler, or keeping project dependencies separate, you don't need them anymore.

The Benefits

A clean separation between "the project's gems" and "my gems" is a big win.

This way, when you install some gem-based programming tool (such as Nick's "jsonpretty" or Evan's "gx") it's useful everywhere, not just in the projects where you've remembered to install it. Of course, you will have to install it for the different Ruby versions you use.

And that also means we can get rid of stuff like this snippet from the Gemfile of one of our projects:

# dev utilities
%w[ cheat foreman hirb lunchy open_gem ruby-debug ruby-prof wirble yard ].each do |name|
  gem name
end

At least some of those gems are developer preferences, not project dependencies. If you like the "cheat", "hirb", and "open_gem" gems, they should be available everywhere on your system, not just in the gemset for that project. Just gem install them!

Finally, and most importantly, Bundler will behave better for you. You will have to use bundle exec for things like rake and resque-web, but you should have been doing that anyway. I'm not just being pedantic when I say that. If you were able to run those commands without bundle exec, you were just getting lucky, and some flaky, strange things were probably happening. Sooner or later, it would fail in confusing ways.4

There are multiple ways to make bundle exec easier. I use a smart Bash function that wraps certain commands and figures out what to do. Alternatively, you can use Bundler's binstubs (which is a whole 'nuther article).

Upgrading Gems

One more thing: above, I said you should almost never run bundle update. But clearly, we do have to update gems, and we should do it fairly regularly, because it's easier to upgrade if you follow along, as opposed to lagging behind and then someday being forced to upgrade from a seriously out-of-date gem. So what should you do?

If you lock down your Gemfile to explicit versions, you can just update the version of a selected gem in Gemfile and then run bundle. That will make the smallest number of upgrades necessary to bring that gem to the requested version. Then you can test and, if all seems well, commit and deploy.

My preferred method is to not lock things down too tightly in Gemfile. Then, when I want to try upgrading a gem (let's use the "chronic" gem as an example) I'll type bundle update chronic. Bundler will update just that gem to the latest version. (It might have to also update some of chronic's dependencies, but that will be true in either case.) Then, if testing doesn't uncover any problems, I can commit and deploy. This method, by the way, only works if nobody on the team ever uses bundle update except when explicitly trying to upgrade out-of-date gems. But that's the right way to use it anyway, and it's easy to catch and recover from mistakes. In either method, you only upgrade gems when you choose to.

Both of these methods update Gemfile.lock, so the production system and all of your colleagues will end up with the exact same mix of versions you tested with.

(A quick note, thanks to Mark McSpadden: if you ever accidentally type bundle install something instead of bundle update something, Bundler will install all of your gems in a directory called "something" and reset things so that that's your project gem repository. Why? Because bundle install something is an old, deprecated syntax for bundle install --path something. If this happens, just remove the new something directory, type bundle install --path .bundle, and you're back in business, having lost nothing but a bit of time. Just try not to accidentally type bundle install app.)

Backing Out of a Gem Upgrade

What if something goes wrong? The upgrade might break something that didn't show up in your testing. If that happens, simply check out the most recent versions of Gemfile and Gemfile.lock that were working correctly, run bundle, and deploy. You'll be back to the exact same gem version mix that was working before. Note that simply reverting the commit where you upgraded gems will have the desired effect. If you find problems right away, before committing the changes, just do git checkout Gemfile Gemfile.lock && bundle and you'll be in good shape.


1 RubyMine (4.0.x) doesn't see .bundle and will nag you about missing gems and won't allow you to browse the source of them. If you use RubyMine, instead of .bundle you'll need to use a directory name that doesn't start with a dot, such as gems (or perhaps zzgems to force search hits to the bottom of the results). If you do this, be sure to add gems or zzgems to .gitignore in your home directory, so you don't accidentally check your bundle in.

2 If you are a frequent user of ack, you may now start seeing search results from your gems. If you'd like to avoid seeing this output, add export ACK_OPTIONS=--ignore-dir=.bundle to your bash config file. Happy ACKing!

3 When I'm getting started with a new project, I always type bundle install, but then for the rest of the life of that project I type bundle, even though they do the same thing. Why? Because, based on the description I just gave of what bundle install does, I think it's misnamed, and so using bundle install for minor updates messes with my mental model. I know the two ways of invoking the "install" subcommand do the same thing, but using them differently in those two distinct circumstances just feels right to me. YMMV.

4 Just last week (as of 2013-08-16) a colleague said "I always thought of bundle exec as a hack ... as in, 'Oh, gems are messed up, just run bundle exec'". The truth is that if you aren't running bundle exec, gems are always messed up; you're just not seeing visible symptoms. Using the strategy recommended here (especially if you go all the way and abandon gemsets) ensures that if you aren't using bundle exec things will fail fast, rather than seeming to work and hiding the problems.

@danrabinowitz
Copy link

In Step 1, second sentence, shouldn't it be "user-global BUNDLER configuration file" instead of "user-global GIT configuration file"?

@glv
Copy link
Author

glv commented Dec 27, 2013

You're right, @danrabinowitz … fixed!

@chrismo
Copy link

chrismo commented May 5, 2014

bundle binstubs [gemname] will add a special executable script to ./bin folder, that allows you type bin/[gemname] instead of bundle exec [gemname] every time. For example, bundle binstubs rake adds bin/rake to your filesystem and now you can use bin/rake instead of bundle exec rake.

NOTE: Bundler allows you to run bundle install --binstubs and it will add ALL executable gems into the bin directory. That's fine, until you start using Rails 4. Rails 4 got into the bin stubs business, creating a bin/rails file, and after invoking bundle install --binstubs Bundler will re-write bin/rails every time you invoke bundle, breaking the rails command. More history here: http://clabs.org/blog/BinRailsAndBundler. The recommendation for Rails 4 is to selectively binstub gems, using the bundle binstubs [gemname] and don't do it for the rails gem. Some people have complained about that - in practice, I find I don't have many gems that need bin-stubbing, so I'm ok with it.

@josh-m-sharpe
Copy link

@glv Great write up. I've found that when I run bundle update <gem_name> bundler will actually attempt to update that gem, and all of its dependencies. If you want bundler to only attempt to update the one gem in question try this: bundle update --source <gem_name>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment