#201 Bundler (revised)
Bundler is a great way to manage Ruby Gem dependencies and it was recently updated to version 1.1. In this episode we’ll cover some of its new features and the basics of using Bundler in a Rails application.
To get the latest release, which at the time of writing is 1.1.2, we run this command:
$ gem install bundler
Next we’ll create a new Rails application so that we have something to work with.
$ rails new blog
When we create a new Rails application bundle install
is run so that all the gems the application depends on are installed. This includes all the gems listed in the gemfile and their dependencies. One nice thing about Bundler 1.1 is that the install
command is now much faster. We can see this by uncommenting a gem in the gemfile, say the capistrano
gem, and then running bundle install
again. Bundler 1.1 will only fetch the metadata for the gems that it needs and not for every gem known to man.
Version Numbers
When we add a gem to the gemfile we don’t need to specify a version number. If we omit the version number Bundler will use the latest released version by default. If we run bundle install
again later after a new version of a gem has been installed the version that was first installed will stay. The reason for this is that the version number of the installed version for each gem in the application is stored in a Gemfile.lock
file. This file specifies the version of each gem that’s used in the project.
GEM remote: https://rubygems.org/ specs: actionmailer (3.2.2) actionpack (= 3.2.2) mail (~> 2.4.0) actionpack (3.2.2) activemodel (= 3.2.2) activesupport (= 3.2.2) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.1) rack (~> 1.4.0) rack-cache (~> 1.1) rack-test (~> 0.6.1) sprockets (~> 2.1.2) # rest of file omitted.
Whenever we run bundle install
(or the shorter bundle
command, which does the same thing) in our application’s directory we can be sure that the same version of each gem is installed no matter which system we’re on. If a new version of a gem is released and we want to use it in our application we can use bundle update
and pass it the name of the gem.
$ bundle update capistrano
This will update the gem to the latest version and also update Gemfile.lock
so that the new version number of the updated gem is stored there. We can also run bundle update
without a gem name to update all the application’s gem to the latest version. If we want more control over the version of a gem that’s used in a project we can pass a version number in as a a second argument like this:
# Deploy with Capistrano gem 'capistrano', '2.11.2'
Now when we run bundle update
that version of the gem will be installed even if a newer version is available. A common practice when specifying a version number is to prefix it with ~>
. When we use it the gem will only be updated if only the last part of the version number has changed.
# Deploy with Capistrano gem 'capistrano', '~> 2.11.2'
Now, if version 2.11.3
of Capistrano is released then bundle update will install it, but when version 2.12.1
comes out this version won’t be installed. This helps to prevent large updates that may not be backwards compatible from being installed. This tilde operator always works on the last supplied number in the version number so if we don’t specify the revision release version then the minor version becomes the last number.
# Deploy with Capistrano gem 'capistrano', '~> 2.11'
Now, bundle update
will update the gem to any new version 2.xx
release but not to version 3.0.0
when that is released.
There are other options that we can pass in to gem
. For example we can use the require
option to specify which file should be loaded in case it doesn’t match the name of the gem. There’s also a very useful git
option which lets us specify a Git repository instead of a released gem.
# Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git'
If there’s a bug in the current version of a gem that’s been fixed but the new version hasn’t yet been released we can point the gem to the Git repository to get the latest committed version. If we want to change the behaviour of a gem we can fork the project, make the changes then point the gem to our Git repository instead. This is a really powerful option and in Bundler 1.1 it’s even more convenient as there’s a new github
option. We pass this a user name and a project name and it will use these to fetch the repository from Github. We can also use a branch
option to fetch a different branch or ref
to get a specific commit SHA.
# Bundle edge Rails instead: # gem 'rails', :github => 'rails/rails', branch: '3-2-stable', ref: '123456'
The path
option is also very useful. If the project is on our local system we can specify a path and this will be used instead of the actual gem.
gem 'rails', path: '~/code/rails'
We need to be careful with this option. If we commit it to our Git repository we’re sharing something that’s specific to our system and the code may well not exist at that path on anyone else’s computer.
Groups
Groups are another useful feature of Bundler. A Rails application’s gemfile has one group defined by default, called assets
.
# Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer' gem 'uglifier', '>= 1.0.3' end
Groups help control when a gem should be installed or loaded. We can make as many groups as we want but Rails has some default behaviour for certain groups. For example let’s say that we want to use Capybara in only the test
environment. We can add this gem to a group by using the group option.
gem 'capistrano', group: :test
The assets
group is loaded in both the test
and development
environments but not in production. We can see this behaviour if we take a look at the config/application.rb
file.
if defined?(Bundler) # If you precompile assets before deploying to production, use this line Bundler.require(*Rails.groups(:assets => %w(development test))) # If you want your assets lazily compiled in production, use this line # Bundler.require(:default, :assets, Rails.env) end
This file has a call to Bundler.require
and we can pass in the groups we want to be loaded here. The Rails.groups
method is a way of saying that Bundler should only load the gems in the assets
group in the development and test
environments. We can control exactly which groups Bundler loads here.
When we run bundle install
it installs gems from all groups by default. We can customize this behaviour by passing in various options which we can see by running bundle help install
. The options listed include a without
option that we can use to exclude groups that we don’t want to be included in the installation. If we use Capistrano for deployment we don’t have to worry about this too much in production as it will automatically.
If we run bundle help
without any arguments we’ll get a full list of the commands that we can run. Bundler 1.1 has a new outdated
command and this will give us a list of all the gems that have newer versions than those used in the project.
$ bundle outdated Fetching gem metadata from https://rubygems.org/........ Outdated gems included in the bundle: * sprockets (2.3.1 > 2.1.2)
Bundle exec
Next we’ll talk about bundle exec
. This is the command that every Ruby developer loves to hate but we’ll try to explain why it’s necessary. Some gems provide an executable that we can run from the command line, rake
being one of the most common. Let’s see which version of it we’re running.
$ rake --version rake, version 0.9.3.beta.1
We have the latest beta version installed but if we look in the lock file for this app we’ll see that it depends on version 0.9.2.2
.
rake (0.9.2.2)
If we prefix the call to rake
with bundle exec
it will force the version to the version on which our application depends.
$ bundle exec rake --version rake, version 0.9.2.2
If we try to run a Rake task with the default version of Rake we’ll see an error.
$ rake db:migrate rake aborted! You have already activated rake 0.9.3.beta.1, but your Gemfile requires rake 0.9.2.2. Using bundle exec may solve this.
We need to run this command through bundle exec
for it to work.
$ bundle exec rake db:migrate
So this is why it’s important to prefix gem commands with bundle exec
to ensure you’re using the version of the command that the gem depends on. But whatever the reason for this it’s still a pain to have to type bundle exec
every time we want to run a command. We could shorten this through a shell alias to something like be but it’s still something we need do remember every time. A nice solution to this is provided by Oh My ZSH. If we enable its Bundler plugin it will alias common gem commands and automatically run them through bundle exec
when we’re inside a Bundler project. With this enabled when we run rake
directly it will use bundle exec
behind the scenes without us having to run it.
% rake --version rake, version 0.9.2.2
If you don’t want to use Oh My ZSH you could probably adapt this plugin script to fit your environment.
An alternative way to run gem commands through Bundler is to use binstubs. We can set these up by running this command:
$ bundle install --binstubs
This generates a local bin
directory with executables for all the gems.
$ ls bin cap rackup rake2thor sass thor capify rails rdoc sass-convert tilt erubis rake ri scss tt
If we try running one of these it will be run through Bundler just as if we’d used bundle exec
. This is especially useful in production where we can point something to one of these scripts and not have to worry about running it through bundle exec
.
Configuring Gems
We’ll finish by showing what you can do in cases where a gem won’t compile properly and it needs to be configured. Nokogiri can often be troublesome as it’s particular about which versions of libxml it works with. In Bundler we can use the bundle config
command and pass in build.nokogiri
(or whatever the name of the gem is). Any further options we pass to this command will be passed in when the gem is installed. This command will install Nokogiri with the version of libxml that’s installed through Homebrew.
$ bundle config build.nokogiri --with-xml2-include=/usr/local/Cellar/libxml2/2.7.8/include/libxml2 --with-xml2-lib=/usr/local/Cellar/libxml2/2.7.8/lib --with-xslt-dir=/usr/local/Cellar/libxslt/1.1.26
This option is specific to our system so the config options aren’t stored in the project but under the ~/.bundle/config
directory.
$ cat ~/.bundle/config ---BUNDLE_BUILD__NOKOGIRI: --with-xml2-include=/usr/local/Cellar/libxml2/2.7.8/include/libxml2 --with-xml2-lib=/usr/local/Cellar/libxml2/2.7.8/lib --with-xslt-dir=/usr/local/Cellar/libxslt/1.1.26
This should help if you ever run into problems when compiling a gem.