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.
This is my strategy for using Bundler, and for me it's always been smooth sailing. Give it a try.
First, the quick summary:
- Run
bundle config path .bundle
from your home directory1 2 - Run
bundle install
in all of your projects - Always run
bundle
(notbundle update
) unless you're explicitly upgrading gem versions. - 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.
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).
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
.)
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.
In Step 1, second sentence, shouldn't it be "user-global BUNDLER configuration file" instead of "user-global GIT configuration file"?