Gems & Bundler

Ruby Intermediate/Advanced — Gems & Bundler
Ruby Intermediate/Advanced
Course 2 · Chapter 7 · Gems & Bundler

💎 Gems & Bundler

Chapter 6 used RSpec without explaining how it actually gets onto a machine or into a project. This chapter covers that: gems, Ruby's packaged-library format, RubyGems, the tool that installs them, and Bundler, the tool that pins exactly which versions a project depends on — then closes by building a tiny gem of your own.

📦 What Is a Gem?

A gem is a packaged Ruby library — code plus metadata (name, version, dependencies, author) bundled into a single distributable file — published to the central registry at rubygems.org. This is Ruby's direct equivalent of PHP's Composer packages (hosted on Packagist) or Python's pip packages (hosted on PyPI). RSpec, which Chapter 6 used freely, is itself just a gem like any other.

$ gem install rspec # installs the latest version globally on your machine $ gem list # lists every gem currently installed

📋 Gemfile — Declaring a Project's Dependencies

Installing gems globally works for quick experiments, but real projects need every dependency and its version declared somewhere, so anyone (including you, six months later) can reproduce the exact same setup. That's what a Gemfile does:

# Gemfile source "https://rubygems.org" gem "rspec", "~> 3.12" gem "httparty" gem "rake"

~> 3.12 is a pessimistic version constraint — allow any 3.12.x patch update, but never jump to 3.13. This is directly comparable to a composer.json's version constraints or a Python requirements.txt/pyproject.toml entry.

🔒 Bundler and Gemfile.lock

$ bundle install

Bundler reads the Gemfile, resolves a complete, consistent set of exact versions for every gem and every gem those gems themselves depend on, installs them, and writes the precise result to Gemfile.lock. That lock file — comparable to Composer's composer.lock or Poetry's poetry.lock — is what guarantees everyone working on the project, and your production server, all use the exact same gem versions, not just "whatever satisfies the constraints today."

⚠ Always commit Gemfile.lock to version control

It's tempting to .gitignore a lock file since it's "generated," but doing so defeats the entire purpose of Bundler. Without Gemfile.lock committed, two different machines running bundle install against the same Gemfile could resolve to different gem versions if anything was published to RubyGems in between — the exact "works on my machine" problem lock files exist to prevent.

▶️ bundle exec

$ bundle exec rspec spec/

bundle exec runs a command using the exact gem versions locked in Gemfile.lock, rather than whatever version happens to be installed globally on the machine. Python's rough equivalent is activating a virtual environment before running a command; PHP typically doesn't need an analogous step, since Composer's vendor/autoload.php is usually required directly by the application code itself rather than needing a wrapper command.

📥 require vs require_relative

require "json" # a gem or standard-library file, found via Ruby's load path require_relative "lib/helper" # a local file, relative to the CURRENT file's location

Use require for gems and standard-library code; use require_relative for your own project's files, since it resolves relative to wherever the requiring file actually lives rather than the current working directory a script happens to be run from.

🏗️ Building Your Own Gem

$ bundle gem greeter # scaffolds a new gem project

This generates a standard structure: a lib/ directory for the actual code, a greeter.gemspec file describing the gem's metadata, plus a README, license, and test setup. The .gemspec is the gem's equivalent of a composer.json or Python pyproject.toml:

# greeter.gemspec Gem::Specification.new do |spec| spec.name = "greeter" spec.version = "0.1.0" spec.authors = ["Philip Osztromok"] spec.summary = "A tiny gem that says hello." spec.files = ["lib/greeter.rb"] spec.add_dependency "json", "~> 2.0" end # lib/greeter.rb module Greeter def self.say_hello(name) "Hello, #{name}!" end end
$ gem build greeter.gemspec # produces greeter-0.1.0.gem $ gem install ./greeter-0.1.0.gem # installs it locally for testing

From there, publishing to rubygems.org with gem push makes it installable by gem install greeter from anywhere — the exact same distribution path every gem used throughout this entire course, including RSpec itself, went through.

📜 Package Management, Side by Side

Composer vs pip/Poetry vs RubyGems/Bundler

ConceptPHP (Composer)Python (pip/Poetry)Ruby (RubyGems/Bundler)
Central registryPackagistPyPIRubyGems.org
Declare dependenciescomposer.jsonrequirements.txt / pyproject.tomlGemfile
Exact locked versionscomposer.lockpoetry.lock (or pip freeze)Gemfile.lock
Run with locked versionsUsually automatic via autoloadActivate a virtual environmentbundle exec

💻 Coding Challenges

Challenge 1: Write a Gemfile

Write a Gemfile for a small command-line project that needs rspec for testing (pinned with a pessimistic constraint to the 3.x line), rake for task running (any version), and a made-up gem my_project_utils pointed at a local path instead of RubyGems using path: "../my_project_utils".

Goal: Get comfortable writing realistic Gemfile syntax, including version constraints and local path dependencies.

→ Solution

Challenge 2: require vs require_relative

Sketch out two small files: lib/greeting.rb defining a Greeting module with a hello method, and main.rb in the project root that needs to use it. Write the correct require_relative line in main.rb, and explain in a comment why require (not require_relative) would be the wrong choice here.

Goal: Solidify when to reach for each of the two require variants.

→ Solution

Challenge 3: Sketch a Tiny Gem

Write the .gemspec and the single lib/ file for a gem called temp_converter that exposes one class method, TempConverter.celsius_to_fahrenheit(c). Include a name, version, summary, and the files list in the gemspec.

Goal: Practice the minimum shape a real, publishable gem needs.

→ Solution

💎 Gemfile.lock Is the Whole Point

It's worth restating: a Gemfile alone only says what's allowed — a range of acceptable versions. Gemfile.lock says what's actually installed, down to the exact version number, for every direct and indirect dependency. Reproducibility — the guarantee that "it works on my machine" also means "it works on every machine" — comes entirely from that lock file being committed and respected, not from the Gemfile by itself.

🎯 What's Next

Next chapter — the final chapter of this course: Ruby Idioms & Style — Rubocop conventions, the &. safe navigation operator, and a last look back at common habits worth unlearning from PHP and Python.