09 · Gems & Bundler Basics¶
A gem is a Ruby package — a library someone else wrote that you can install and use in your own code. Bundler manages exactly which gems (and versions) a project depends on.
Installing a gem directly¶
require loads a library — either from Ruby's standard library (like
json or date) or from an installed gem.
Why Bundler exists¶
Installing gems globally with gem install works for quick experiments, but
real projects need reproducible dependencies — the exact same gem
versions on every machine that runs the code. Bundler solves this with a
Gemfile and a Gemfile.lock.
Gemfile¶
# Gemfile
source "https://rubygems.org"
gem "colorize"
gem "httparty", "~> 0.21" # pessimistic version constraint
gem "rspec", group: :test # only installed in the :test group
Running bundle install reads the Gemfile, resolves compatible versions
for every gem, installs them, and writes the exact resolved versions to
Gemfile.lock — that lock file is what guarantees everyone on the team (and
your production server) gets identical versions.
Version constraint syntax¶
| Syntax | Meaning |
|---|---|
gem "foo" |
Any version |
gem "foo", "1.2.3" |
Exactly this version |
gem "foo", ">= 1.2" |
This version or newer |
gem "foo", "~> 1.2" |
>= 1.2, < 2.0 (pessimistic — allows patch/minor updates only) |
~> ("twiddle-wakka") is the most common in practice — it allows safe patch
and minor updates while protecting against unexpected breaking major-version
bumps.
Running code with Bundler¶
bundle exec ensures your script uses the exact gem versions locked in
Gemfile.lock, rather than whatever happens to be installed globally on the
system — always prefer this over a bare ruby command once a Gemfile
exists.
Requiring gems in your code¶
# my_script.rb
require "bundler/setup" # restricts loading to only Gemfile-listed gems
require "colorize"
require "httparty"
puts "Ready!".green
response = HTTParty.get("https://api.github.com")
puts response.code
Gem groups (e.g., test-only dependencies)¶
How It Actually Works¶
require and require_relative aren't preprocessor includes — each call
asks Kernel#require to check $LOADED_FEATURES (an array of already-loaded
absolute paths); if the file isn't there, Ruby reads it, compiles it to
YARV bytecode, evaluates the whole file top to bottom (which is how
class/def/constant definitions actually take effect), then records the
path so re-requiring the same file is a no-op. RubyGems hooks into this by
adding each installed gem's lib/ directory to $LOAD_PATH so require
"gem_name" can find it. Bundler goes one step further: Gemfile.lock
pins exact resolved versions of every gem and its transitive dependencies,
and bundle exec (or Bundler.require in bundle/setup) rewrites
$LOAD_PATH at runtime to point only at those locked versions — which is
why running a script without bundle exec can silently load a different,
system-installed version of a gem than the one your Gemfile.lock pinned.
Cheat sheet¶
| Task | Command |
|---|---|
| Install a gem globally | gem install name |
| Install project dependencies | bundle install |
| Run a script with locked versions | bundle exec ruby script.rb |
| Add a gem | Add a line to Gemfile, then bundle install |
| Pessimistic version constraint | gem "name", "~> 1.2" |
| Group gems (e.g. test-only) | group :test do ... end |
🔀 See this in another language¶
Exercise¶
Create a Gemfile requiring the json gem (part of the standard library,
but a good practice example) with a ~> version constraint. Run bundle
install to generate Gemfile.lock. Then write a script that uses
require "json" to parse a JSON string like '{"name": "Ada", "age": 30}'
into a Ruby hash and print each key/value pair.