Skip to content

Instantly share code, notes, and snippets.

@parkeristyping
Last active May 18, 2016 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save parkeristyping/f45be69bc04ffe4f200f88e6b59f8a42 to your computer and use it in GitHub Desktop.
Save parkeristyping/f45be69bc04ffe4f200f88e6b59f8a42 to your computer and use it in GitHub Desktop.
Notes on Bundler for a Lunch and Learn

What is Bundler?

  • It’s the canonical package manager for Ruby
  • Works hand-in-hand with RubyGems
  • Bundler + RubyGems is kind of like:
    • Virtualenv + Pip for Python
    • NPM for NodeJS
    • Leiningen for Clojure

Quiz 1: How many lines of code in Bundler?

A)   3,921
B)  11,239
C)  34,980 X
D) 121,120

Quiz 2: How many contributors?

A)    79
B)   492 X
C)   733
D) 1,214

What is a gem?

/dice
  dice.gemspec
  /lib
    dice.rb
# /dice/dice.gemspec
Gem::Specification.new do |s|
  s.name = "dice"               # required
  s.summary = "I'm a test gem"  # required
  s.author = "Parker"           # required
  s.version = "0.0.0"           # required
  s.files = ["lib/dice.rb"]
  s.add_development_dependency "pry", "0.5.0"
  s.add_runtime_dependency "andand"
end
# /dice/lib/dice.rb
class Dice; def self.roll; rand(1..6); end; end

Use gem build to create a packaged .gem file.

E.g. $ gem build /dice/dice.gemspec gives us:

/dice
  dice-0.0.0.gem
  dice.gemspec
  /lib
    dice.rb

How do I install a gem?

$ gem install dice installs the gem and any needed runtime dependencies:

/gems/2.3.0
  /cache
    dice-0.0.0.gem
    andand-1.3.3.gem
  /gems/dice-0.0.0
    /lib
      dice.rb
  /specifications
    dice-0.0.0.gemspec
    andand-1.3.3.gemspec
  /gems/andand-1.3.3
    ... contents of andand ...
  /doc
    ... documentation ...

How do I use a gem?

And now you have access to it:

$ irb
irb> require 'dice'
=> true
irb> Dice.roll
=> 3

Back to Bundler

RubyGems gives us the ability to create and install gems.

But, what we’ve seen so far has been at a pretty global level - gem install impacts an entire Ruby installation.

Bundler enables project-level gem management, primary through two commands:

  1. bundle install - Install some gems specified for your project
  2. bundle exec - Run a Ruby process using specified gems

More on bundle install

# /my_project/Gemfile
# Set the source for Bundler to look for gems
source "https://rubygems.org"
gem "require_all"

In addition to installing any needed gems, running bundle install creates a new file Gemfile.lock:

/my_project
  Gemfile
  Gemfile.lock

Gemfile.lock

GEM
  remote: https://rubygems.org/
  specs:
    require_all (1.3.3)

PLATFORMS
  ruby

DEPENDENCIES
  require_all

BUNDLED WITH
   1.12.4

More on the Gemfile - versioning

# /my_project/Gemfile
source "https://rubygems.org"

gem "require_all" # Will use most recent version from source
gem "require_all", "1.2.0" # Will only use that exact version
gem "require_all", ">=1.2.0" # >, <, >=, <= are all supported
gem "require_all", "=1.2.0" # and = for that matter
gem "require_all", ">=1.2.1", "<1.3.0"
gem "require_all", "~>1.2.1" # same as last line
gem "require_all", "<=1.3.0", ">1.2.1"
gem "require_all", "<=1.3.0", ">1.2.1", ">1.2.0" # sure
gem "rails", "3.0.0.beta3"

Running bundle install with no Gemfile.lock gets the highest available version that meet your criteria, and saves that version information, along with the source it was downloaded from, to a new Gemfile.lock.

Running bundle install when there is a Gemfile.lock already adds new gems to the Gemfile.lock but does not automatically upgrade versions if new ones have been available.

An example:

  1. Your Gemfile looks like below, and you have no Gemfile.lock yet:
    # /my_project/Gemfile
    source "https://rubygems.org"
    gem "andand", ">= 1.0.0"
        
  2. You run bundle install, which creates the following, since the most recent version of the andand gem on https://rubygems.org is 1.2.0:
    # /my_project/Gemfile.lock
    GEM
      remote: https://rubygems.org/
      specs:
        andand (1.2.0)
    ...
        
  3. Suppose andand’s author(s) pushes a new version, 1.2.1, to https://rubygems.org after you ran bundle install
  4. If you run bundle install again, nothing will be updated in your Gemfile.lock, you won’t install version 1.2.1, bundle exec will still use andand v1.2.0, you won’t pass go and collect $200, etc.
  5. To get the new version, you have to run either bundle update or just bundle update andand (if you want to limit your update to just andand), which will install the new version and update your Gemfile.lock
  6. And of course, bundle update only works because of the open-ended requirement in the Gemfile (~”>= 1.0.0”). Had the requirement been ~">= 1.0.0", "< 1.2.1"~, ~bundle update would never update the gem past “1.2.0”.

Semantic versioning and the squiggle

Semantic versioning

  • MAJOR.MINOR.PATCH
  • From 1.0.0…
    • 1.0.1 for a patch with no impact on API
    • 1.1.0 if you added features w/o breaking existing API
    • 2.0.0 if users are going to have to update their code

The squiggle (pessimistic operator)

  • ”~> 1.2.3” …is the same as… “>= 1.2.3”, “< 1.3”
  • ”~> 1.2” …is the same as… “>= 1.2”, “< 2.0”
  • ”~> 1” …is the same as… not really getting this are we?

More on the Gemfile - groups

Groups determine what gets required by Bundler.require, e.g. Bundler.require(:default, :production)

Note: Bundler.require is the same as Bundler.require(:default)

# /my_project/Gemfile
gem "pry" # this is in group :default, you know, by default

group :development, :test do
  gem "rspec"
end

gem "rack-cors", "~> 0.2.9", require: "rack/cors", group: :production
gem "sqlite", require: false
puts "Gemfile is just Ruby code, btw"

More on the Gemfile - sources

# /my_project/Gemfile
source "https://rubygems.org" # will check this second
source "https://ruby.taobao.org" # will check this first
ruby "2.1.5" # unrelated to sources, but worth mentioning

gem "rspec" # will use ruby.taobao.org
gem "pry", source: "https://rubygems.org" # will use rubygems.org
# You can also use a block format like this:
source "https://rubygems.org" do
 gem "rails"
 gem "sqlite"
end

Notes:

  • source order was behaving differently with localhost
  • don’t know how it would behave if one has > version

More on the Gemfile - source alternatives

# /my_project/Gemfile
ruby '1.9.3', :engine => 'jruby', :engine_version => '1.6.7'

gem "dice", path: "local_gems/dice"
gem "dice", path: "~/my_project/local_gems/dice"
path "./local_gems/dice" do
  gem "dice"
end

gem "rails", git: "https://github.com/rails/rails.git", branch: "2-3-stable"
gem "rails", git: "git://github.com/rails/rails.git", ref: "4aded"
gem "rails", github: "rails", tag: "v3.0.0"
# etc

More on bundle exec

From the docs: “This command executes the command, making all gems specified in the Gemfile available to require in Ruby programs.”

Some further notes:

  • It doesn’t just use the Gemfile
    • It scans the Gemfile to ensure the Gemfile.lock is still valid
    • But then it also uses the Gemfile.lock to get exact versions
    • You can see this by trying bundle exec before bundling
  • Beyond just providing the gems, it also guards against gems that might be installed but are not required

Bundler setup

Pre-reqs:

  • Pry is installed
  • There is a Gemfile and Gemfile.lock, but Pry not in them
# /my_project/w_setup.rb
require 'bundler/setup'
require 'pry'
# /my_project/wo_setup.rb
require 'pry'
$ ruby w_setup.rb #=> error
$ ruby wo_setup.rb #=> no error
$ bundle exec ruby wo_setup.rb #=> error

More on Bundler setup

You can be more specific with Bundler.setup

# /my_project/w_setup.rb
require 'bundler'
Bundler.setup(:default)
require 'pry'

Let’s play around a bit

  • Install our gem using local gem server
  • Install from a different source https://ruby.taobao.org
  • Bump version and use our gem from path
  • Bump version and install our gem from public github repo
  • Bump version and install our gem from private github repo
  • Create a new gem that conflicts with our require statement
  • Try to install gems with conflicting dependencies
  • Specify two sources, where the 1st has a more recent version

Notes:

  • Installations from source use the .gem file
  • Installations from path / git:
    • Use .gemspec (not .gem) and are annotated with a ! in the Gemfile.lock
    • Don’t handle interconnected dependencies
  • Installations from git:
    • Install to a different directory
  • Pass github credentials in like BUNDLE_GITHUB__COM=”username:password”

Other Bundler commands

  • bundle show - Show the path to the “bundled” version of a gem
  • bundle binstubs - Provide wrappers for executables like rake, which ensure the bundled version is used
  • bundle package - Create project-local copies of gems
  • bundle update - Update gem dependencies to latest versions
  • bundle check - Check if all required gems are installed
  • bundle outdated - List bundled gems with updates available
  • bundle clean - Remove unused gems
  • bundle init - Create example Gemfile
  • bundle gem - Create gem skeleton
  • bundle config - Show Bundler configuration
  • bundle console - Alias for bundle exec irb
  • bundle viz - Create a gem dependency map
  • bundle help - Help
  • bundle inject - Add a gem to the Gemfile
  • bundle open - Open gem directory in default editor
  • bundle version - Show Bundler version
  • bundle platform - Show bundled platform specs

Bundler and Rails

Not much magic, really…

# /config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
# /config/application
Bundler.require(*Rails.groups)

Note: Rails.groups gets set via ENV["RACK_ENV"]

Gemfile order

Gemfile order shouldn’t matter, but we’ve all probably run into situations where it sure seemed to. So, what’s going on?

  • The order of gems in your Gemfile does determine in what order Bundler.require requires the gems
  • So, if there is a gem that refers to, say, ActiveSupport, or one of its provided methods like .try, it can throw errors
  • This will only happen if the gem itself doesn’t explicitly have ~require ‘active_support’~, though
  • The lesson here is, when writing gems, you can’t just specify requirements in your runtime dependencies. You must require.

Quiz 3

# /my_project/thing_one.rb
require_relative "thing_two"
puts "one"
# /my_project/thing_two.rb
require_relative "thing_one"
puts "two"

What does $ ruby thing_one.rb do?

Quiz 3 - Answer

# /my_project/thing_one.rb
puts "one - #{Time.now.to_f} - #{require_relative 'thing_two'}"
# /my_project/thing_two.rb
puts "two - #{Time.now.to_f} - #{require_relative 'thing_one'}"
$ ruby thing_one.rb
one - 1463494329.522492 - false
two - 1463494329.423543 - true
one - 1463494329.348982 - true

Other weirdness

  • If you put this in your Gemfile, sometimes it will puts “no error” then “error” without breaking. So, I think they must have bare rescues.
    if [true, false].sample
      puts "error"
      1 / 0
    else
      puts "no error"
    end
        
  • Bundler seems to require things to be in /lib but not RubyGems
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment