Gems and packaging
The Ruby ecosystem revolves around gems — packaged, versioned libraries — and the conventional tooling: RubyGems (the package registry and command-line tool), Bundler (project-level dependency management), RubyGems.org (the public registry), and version managers (rbenv, rvm, chruby, asdf) for managing multiple Ruby versions per project. The principal project files: Gemfile (declares dependencies), Gemfile.lock (records resolved versions), .gemspec (gem metadata for libraries being published), .ruby-version (specifies the Ruby version). The combination — gems as the unit of distribution, Bundler for dependency management, version managers for toolchain isolation, the conventional project layout — is the substance of Ruby’s package ecosystem.
require and require_relative
The principal loading mechanisms:
require "json" # standard library or gem
require "active_support/all" # gem (with subpath)
require_relative "lib/helpers" # relative to current file
require_relative "../config"
The require:
- Searches
$LOAD_PATH. - Loads the file once (idempotent — subsequent
requireis a no-op). - Returns
trueon first load,falseon subsequent.
The require_relative (Ruby 1.9+):
- Resolves relative to the current file (not relative to the working directory).
- The conventional form for project-internal loading.
For dynamic loading (re-evaluating each call), load:
load "config.rb" # always re-loads
The conventional discipline uses require and require_relative; load is reserved for development and tests.
Gems
A gem is a packaged Ruby library; the conventional file layout:
my_gem/
├── my_gem.gemspec
├── Gemfile
├── Gemfile.lock
├── lib/
│ ├── my_gem.rb # main entry point
│ └── my_gem/
│ ├── version.rb
│ ├── client.rb
│ └── helpers.rb
├── bin/
│ └── my_gem # executable
├── test/ or spec/
└── README.md
The principal entry point is lib/<gem_name>.rb; the conventional contents:
# lib/my_gem.rb
require "my_gem/version"
require "my_gem/client"
require "my_gem/helpers"
module MyGem
# public API
end
Gemfile
The Gemfile declares project dependencies:
source "https://rubygems.org"
ruby "3.4.0" # required Ruby version
gem "rails", "~> 8.0"
gem "puma", ">= 6.0"
gem "redis", "~> 5.0"
group :development do
gem "rubocop"
gem "byebug"
end
group :test do
gem "rspec", "~> 3.13"
gem "factory_bot", "~> 6.5"
end
group :development, :test do
gem "pry-byebug"
end
# Gem from git:
gem "my_lib", git: "https://github.com/user/my_lib", branch: "main"
gem "my_lib", git: "https://github.com/user/my_lib", tag: "v1.2.3"
# Local path (development):
gem "my_other", path: "../my_other"
# Specific version:
gem "exact", "1.2.3"
gem "min", ">= 1.0"
gem "twiddle", "~> 1.2" # 1.2.x but < 2.0
gem "twiddle", "~> 1.2.3" # 1.2.x but < 1.3
Version specifiers
| Spec | Meaning |
|---|---|
"1.2.3" | exact |
"~> 1.2.3" | >= 1.2.3, < 1.3 |
"~> 1.2" | >= 1.2, < 2.0 |
">= 1.0" | minimum |
"< 2.0" | maximum |
">= 1.0", "< 2.0" | both |
The ~> (twiddle-wakka) is the conventional choice for bounded version compatibility.
Bundler
Bundler manages dependencies declared in Gemfile:
bundle install # install deps; updates Gemfile.lock
bundle install --path vendor # install to local vendor directory
bundle update # update all to latest matching constraints
bundle update rails # update specific gem
bundle exec rails server # run with the bundle's gems
bundle exec rspec
bundle exec ruby my_script.rb
bundle outdated # show outdated gems
bundle list # list installed
bundle show rails # show install path
bundle clean # remove unused
bundle init # create empty Gemfile
bundle add some_gem # add to Gemfile and install
The bundle exec ensures the right gem versions are loaded; without it, the system Ruby’s gems are used.
Gemfile.lock
The lockfile records exactly which versions were installed:
GEM
remote: https://rubygems.org/
specs:
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
...
puma (6.5.0)
nio4r (~> 2.0)
nio4r (2.7.4)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
puma (>= 6.0)
rails (~> 8.0)
BUNDLED WITH
2.6.1
The Gemfile.lock is conventionally committed to version control — admits reproducible builds across machines.
.gemspec for libraries
For libraries being published as gems:
# my_gem.gemspec
require_relative "lib/my_gem/version"
Gem::Specification.new do |spec|
spec.name = "my_gem"
spec.version = MyGem::VERSION
spec.authors = ["Author Name"]
spec.email = ["author@example.com"]
spec.summary = "Short description"
spec.description = "Longer description"
spec.homepage = "https://github.com/user/my_gem"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.files = Dir.glob("lib/**/*") + ["README.md", "LICENSE"]
spec.require_paths = ["lib"]
spec.executables = ["my_gem"] # files in bin/
spec.add_dependency "json", "~> 2.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rubocop", "~> 1.0"
end
The .gemspec is the metadata for gem build and gem push.
RubyGems CLI
gem search rails # search registry
gem install rails # install globally
gem install rails -v 8.0.0 # specific version
gem uninstall rails # remove
gem list # list installed
gem outdated
gem cleanup
gem environment # show paths and config
gem build my_gem.gemspec # build the .gem file
gem push my_gem-1.0.0.gem # publish to RubyGems.org
The conventional contemporary discipline uses Bundler for project gems; gem install is conventional for system-wide tools (e.g., bundler itself, rubocop, pry).
Version managers
Multiple Ruby versions admit isolation per project; the conventional managers:
- rbenv — minimal, uses shims.
- rvm — older, more features, more invasive.
- chruby — minimal, simpler than rbenv.
- asdf — language-agnostic version manager.
- mise — modern asdf alternative.
# rbenv:
rbenv install 3.4.0
rbenv local 3.4.0 # writes .ruby-version
# Then in the project:
echo "3.4.0" > .ruby-version
The .ruby-version file admits automatic version selection; conventional in modern projects.
Standard library vs gems
The Ruby standard library admits substantial functionality without gems:
require "json"
require "yaml"
require "csv"
require "uri"
require "net/http"
require "fileutils"
require "set"
require "date"
require "time"
require "logger"
require "ostruct"
require "pathname"
require "open3"
require "tempfile"
require "tmpdir"
The standard library admits substantial breadth; the conventional Ruby program leans on it for routine functionality and reaches for gems for substantial third-party functionality.
Common gems
The conventional contemporary gems:
Web frameworks
- Rails — full-featured web framework.
- Sinatra — minimal microframework.
- Hanami — modern alternative to Rails.
- Roda — routing tree DSL.
Testing
- RSpec — BDD-style testing.
- Minitest — built-in standard library testing.
- Capybara — browser-driver testing.
- FactoryBot — test data factories.
- VCR — record HTTP interactions.
Utility
- Active Support — substantial Ruby extensions (part of Rails).
- Pry — REPL replacement for
irb. - Faker — fake data generation.
- DotEnv — environment variable loading.
Database
- Active Record — ORM (part of Rails).
- Sequel — alternative ORM.
- Sqlite3, pg, mysql2 — database adapters.
- Redis — Redis client.
Background work
- Sidekiq — background job processor.
- Active Job — job queue abstraction (part of Rails).
HTTP
- Faraday — HTTP client with middleware.
- HTTParty — simple HTTP client.
Linting/formatting
- RuboCop — linting and formatting.
- Standard — alternative ruleset.
Documentation
- YARD — documentation generator.
- RDoc — older standard.
Common patterns
Project initialisation
mkdir my_app && cd my_app
bundle init
echo "3.4.0" > .ruby-version
echo "*.log" >> .gitignore
echo ".bundle/" >> .gitignore
echo "vendor/bundle/" >> .gitignore
git init
Then edit Gemfile:
source "https://rubygems.org"
ruby "3.4.0"
gem "puma", "~> 6.5"
gem "redis", "~> 5.0"
group :development, :test do
gem "rspec", "~> 3.13"
gem "rubocop", "~> 1.71"
end
Then:
bundle install
Module organisation
my_app/
├── Gemfile
├── Gemfile.lock
├── README.md
├── .ruby-version
├── .rubocop.yml
├── lib/
│ ├── my_app.rb # entry point
│ └── my_app/
│ ├── version.rb
│ ├── client.rb
│ └── helpers/
│ ├── string.rb
│ └── time.rb
├── bin/
├── spec/ # or test/
└── config/
# lib/my_app.rb
require "my_app/version"
require "my_app/client"
module MyApp
# ...
end
Loading from lib
For libraries with a lib/ directory, the lib_paths is conventionally added to $LOAD_PATH:
# in bin/my_app or test setup:
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "my_app"
In gemified projects, bundle exec and the .gemspec handle this automatically.
Constants in modules
module MyApp
VERSION = "1.0.0"
DEFAULT_TIMEOUT = 30
module Config
DEBUG = ENV["DEBUG"] == "1"
end
end
# Access:
MyApp::VERSION
MyApp::Config::DEBUG
Conditional gem loading
require "rails"
# Optional gem:
begin
require "redis"
REDIS_AVAILABLE = true
rescue LoadError
REDIS_AVAILABLE = false
end
The pattern admits optional dependencies in libraries.
Gem with executable
# bin/my_app
#!/usr/bin/env ruby
require "my_app"
MyApp::CLI.start(ARGV)
# In .gemspec:
spec.executables = ["my_app"]
After gem install, my_app is admitted on the command line.
Releasing a gem
# Build:
gem build my_gem.gemspec # produces my_gem-1.0.0.gem
# Test locally:
gem install ./my_gem-1.0.0.gem
# Publish:
gem push my_gem-1.0.0.gem # uploads to RubyGems.org
# Or via Rake (with bundler/gem_tasks):
rake build
rake release # tag, build, push
Bundler conventions
# Install:
bundle install
# Run with the bundle:
bundle exec ruby my_script.rb
bundle exec rspec
bundle exec rails server
# Or use binstubs:
bundle binstubs rspec-core
./bin/rspec # without bundle exec
The binstubs admit running commands without bundle exec prefix.
Version constants
module MyApp
VERSION = "1.0.0"
end
# .gemspec:
require_relative "lib/my_app/version"
spec.version = MyApp::VERSION
The pattern admits a single source of truth.
Local gem development
# Gemfile (in consuming project):
gem "my_other", path: "../my_other"
# Or for git development:
gem "my_other", git: "/Users/me/code/my_other"
The conventional discipline removes path:/git: references before publishing or pushing the consuming project.
A note on the conventional discipline
The contemporary Ruby gems-and-Bundler advice:
- Use Bundler for project dependency management.
- Commit
Gemfile.lockfor applications; do not commit for libraries. - Use
~>(twiddle-wakka) for version constraints. - Use
.ruby-version— admits version-manager auto-selection. - Use
bundle exec(or binstubs) for running. - Use the
lib/layout for gem-style libraries. - Use module-namespaced constants —
MyApp::VERSION, etc. - Use
require_relativefor project-internal files. - Use the standard library before reaching for gems.
- Group dev/test gems in
Gemfile. - Document the
Gemfilewith comments where conventions are non-obvious.
The combination — Gemfile and Gemfile.lock for declarative dependency management, the lib/ convention for gem libraries, the .gemspec metadata, the version managers for toolchain isolation, the substantial standard library plus the rich gem ecosystem — is the substance of Ruby’s package management. The discipline produces reproducible, version-pinned, well-organised projects with substantial automation.