Creating and Publishing Gems, and Rake
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18), which ships RubyGems 4.0.20, Bundler 4.0.20 and Rake 13.3.1; features newer than Ruby 3.0 name the version that introduced them.
A gem is the unit in which Ruby code is packaged, versioned and shared: an archive that holds a library, optional executables and a metadata file called the gemspec.
This page follows one gem from an empty directory to a published release: scaffolding it with bundle gem, laying out the files, describing it in the gemspec, versioning it, shipping an executable, building the archive, and the steps that publish it to RubyGems.org (described here, never run).
The second half covers Rake, the build tool that every gem uses for its test, build and release tasks, and that is just as useful for the chores of any Ruby project.
Installing and resolving other people’s gems (gem install, the Gemfile, Gemfile.lock, bundle exec) is the subject of RubyGems and Bundler; how require finds the files of a gem is in Code Loading: require and autoload.
Tests for the gem are in Testing with Minitest, linting with RuboCop in Style, Linting and Static Analysis, and the API documentation published with the gem in Documentation with RDoc and YARD.
The executable that the shelf gem ships is the subject of Command-Line Programs, and the supply-chain side of publishing continues in Security.
For the same ideas in other languages see the Python, Rust, Go, Java and PHP sections of this guide (packaging, crates, modules, Maven artifacts and Composer packages respectively).
Every Ruby example and every Rakefile on this page was syntax-checked with ruby -wc, run on Ruby 4.0.7 with warnings enabled, and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings).
The output shown in # => comments and in the output blocks is the real output.
Commands that talk to RubyGems.org (gem push, gem yank, gem signin, gem owner) are described and never executed.
Paths that belong to the machine that produced an output are abbreviated.
The author@example.com addresses, the example.com URLs and the Example Author name are placeholders.
From idea to published gem
Publishing a gem is a short pipeline, and each stage has a tool.
The tools divide the work as follows.
| Tool | Role in the pipeline |
|---|---|
|
Generates the project skeleton (gemspec, |
|
Reads a gemspec and writes the |
|
Talk to a gem server (RubyGems.org by default): upload a version, withdraw one, manage owners, store an API key. |
|
Runs the project tasks: |
Rake |
A task runner written in Ruby: tasks, prerequisites and file-based rebuilds, described in a |
|
The generated gemspec takes its file list from |
The tool versions differ by Ruby release. The releases checked for this page shipped these versions:
| Ruby | RubyGems | Bundler | Rake |
|---|---|---|---|
3.2.11 |
3.4.19 |
2.4.19 |
13.0.6 |
3.3.12 |
3.5.22 |
2.5.22 |
13.1.0 |
3.4.11 |
3.6.9 |
2.6.9 |
13.2.1 |
4.0.7 |
4.0.20 |
4.0.20 |
13.3.1 |
Rake is a bundled gem in every one of these releases, not a default gem.
A bundled gem is installed with Ruby but is not loaded by require under Bundler unless the Gemfile or the gemspec lists it, which is why the generated Gemfile contains gem "rake".
See RubyGems and Bundler for the default-gem and bundled-gem distinction.
Scaffolding with bundle gem
bundle gem NAME creates a directory named NAME with a working gem skeleton.
The name must be unique on RubyGems.org if you intend to publish, so search the index before you commit to one, and follow the naming conventions in the section on layout below.
bundle gem pocket --exe --mit --changelog --no-coc \
--test=minitest --ci=github --linter=rubocop --git --github-username=example
Running that command in a scratch directory printed this (the lines that say "already configured" appear when Bundler’s global configuration already holds that answer; see the notes after the table):
Creating gem 'pocket'...
minitest is already configured, ignoring --test flag.
github is already configured, ignoring --ci flag.
MIT License enabled in config
Changelog enabled in config
rubocop is already configured, ignoring --linter flag.
RuboCop enabled in config
Initializing git repo in .../exp1/pocket
create pocket/Gemfile
create pocket/lib/pocket.rb
create pocket/lib/pocket/version.rb
create pocket/sig/pocket.rbs
create pocket/pocket.gemspec
create pocket/Rakefile
create pocket/README.md
create pocket/bin/console
create pocket/bin/setup
create pocket/.gitignore
create pocket/test/test_helper.rb
create pocket/test/test_pocket.rb
create pocket/.github/workflows/main.yml
create pocket/LICENSE.txt
create pocket/CHANGELOG.md
create pocket/.rubocop.yml
create pocket/exe/pocket
Gem 'pocket' was successfully created. For more information on making a RubyGem visit https://guides.rubygems.org/make-your-own-gem/
| Option | Effect |
|---|---|
|
Creates |
|
Adds or skips |
|
Adds or skips |
|
Adds or skips an MIT |
|
Chooses the test framework and generates a |
|
Generates a CI configuration file ( |
|
Adds the linter to the |
|
Adds boilerplate for a native extension. The C case is covered later on this page; Go and Rust extensions are generated from templates that depend on external projects. |
|
Runs |
|
Fills the user name into the README and the repository URLs. |
|
Opens the new gemspec in an editor. |
|
Runs or skips |
Four things about the command are easy to miss.
-
Answers are remembered. The first time you run
bundle gemwithout--coc,--mit,--changelog,--test,--cior--linter, it asks and stores the answer in Bundler’s global configuration (thegem.coc,gem.mit,gem.changelog,gem.test,gem.ci,gem.linterandgem.github_usernamesettings). Later runs reuse the stored value, and a conflicting flag is ignored with the message shown above. Change a stored answer withbundle config set --global gem.test rspec. -
The skeleton does not build yet. The generated gemspec contains
TODOplaceholders insummary,descriptionandallowed_push_host, andgem buildrejects them.bundle exec rake buildon the untouched skeleton stopped with"FIXME" or "TODO" is not a description. Replace each placeholder first. -
The generated test fails on purpose.
test/test_pocket.rbcontainsassert false, sobundle exec rake teston a new gem reportsExpected false to be truthy.until you replace it. -
The file list comes from git. The generated gemspec runs
git ls-files, so a new file that was nevergit add-ed is silently left out of the gem. The section on the gemspec shows this.
The template is also conservative about versions. The skeleton generated by Bundler 4.0.20 sets required_ruby_version to >= 3.2.0 and the rubocop TargetRubyVersion to 3.2, pins minitest to ~> 5.16 in the Gemfile although Ruby 4.0 ships Minitest 6, and starts at version 0.1.0.
Edit all of these to match what you actually support and test.
The generated exe/pocket also lacks the frozen_string_literal comment that the rest of the skeleton has.
What the skeleton contains
pocket/
├── .github/workflows/main.yml CI: bundle exec rake on a Ruby matrix
├── .gitignore /.bundle/, /pkg/, /coverage/, /doc/, /tmp/ ...
├── .rubocop.yml TargetRubyVersion and double-quoted strings
├── CHANGELOG.md "## [Unreleased]" and "## [0.1.0]"
├── Gemfile source, "gemspec", irb, rake, minitest, rubocop
├── LICENSE.txt
├── README.md
├── Rakefile bundler/gem_tasks, test and rubocop tasks
├── bin/console IRB with the gem loaded
├── bin/setup bundle install and other setup
├── exe/pocket the executable (with --exe)
├── lib/pocket.rb the entry point: module Pocket, class Error
├── lib/pocket/version.rb VERSION = "0.1.0"
├── pocket.gemspec
├── sig/pocket.rbs RBS signatures
└── test/ test_helper.rb and test_pocket.rb
Source: bundle gem manual
Two directories look alike and are not.
bin/ holds scripts for the people who work on the gem (bin/setup, bin/console) and is excluded from the packaged gem by the generated gemspec.
exe/ holds the commands that the gem installs for its users.
Older gems and older books keep executables in bin/; the gemspec attribute bindir says which directory a gem uses, and exe is the convention for new gems.
The generated Rakefile is three lines of substance, and it is worth reading because it is the glue of the whole workflow:
# frozen_string_literal: true
require "bundler/gem_tasks"
require "minitest/test_task"
Minitest::TestTask.create
require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[test rubocop]
bundler/gem_tasks defines the build, install and release tasks (shown later), Minitest::TestTask.create defines test, RuboCop::RakeTask.new defines rubocop, and the default task chains the two checks so that a bare rake is the whole local CI.
The generated CI workflow runs bundle exec rake, so a green rake locally is what the CI job checks.
Layout and naming
The shelf gem has this layout. It is the skeleton above with the additions the scenario needs (a Store namespace, a refinement and RBS signatures), and the full source of lib/, the gemspec and the Gemfile is in the shelf gem source:
shelf/
├── Gemfile
├── Rakefile
├── shelf.gemspec
├── exe/
│ └── shelf
├── lib/
│ ├── shelf.rb the entry point: requires the rest
│ └── shelf/
│ ├── version.rb
│ ├── isbn.rb
│ ├── author.rb
│ ├── book.rb
│ ├── loan.rb
│ ├── catalog.rb
│ ├── store/memory.rb
│ └── refinements/title_case.rb
├── sig/shelf.rbs
└── test/
├── test_helper.rb
└── shelf_test.rb
The rules that make a layout work come from how RubyGems and Bundler load a gem.
When a gem is activated, RubyGems adds its require_paths (by default lib) to $LOAD_PATH, so every file directly inside lib/ becomes requirable by a short name for every program that uses the gem.
-
One top-level file named after the gem, everything else underneath a directory of the same name.
require "shelf"loadslib/shelf.rb, andlib/shelf/book.rbisshelf/book. A gem that shipslib/set.rborlib/erb.rbshadows or breaks the library of the same name for every program that loads the gem. Namespacing underlib/shelf/avoids that. -
Use
require_relativeinside the gem, neverFile.join(File.dirname(__FILE__), ...)and never$LOAD_PATH.unshift. RubyGems has already putlibon the load path, and a gem that edits$LOAD_PATHcan change which file a laterrequirefinds. The entry point of shelf is a list ofrequire_relativecalls. -
Never call
gem "other", ">= 1.0"from library code. Declare the requirement in the gemspec so that the resolver can weigh it together with the requirements of every other gem. Applications control versions with Bundler. -
Keep the version in one constant in
lib/shelf/version.rb, so that the gemspec, the CLI and the tests read the same value. -
Code that the gem ships goes in
lib/; signatures insig/; commands inexe/; native sources inext/. Tests, CI files andbin/scripts stay out of the packaged gem.
Names carry a convention that makes require guessable:
| Gem name | require |
Constant |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Use underscores between the words of one name, a dash where the gem adds to another gem, and no capital letters (a capitalised name is a portability trap on case-insensitive file systems such as the macOS and Windows defaults). Following the convention also lets Bundler require the gem automatically with no extra configuration.
The gemspec
The gemspec is a Ruby file, by convention NAME.gemspec in the root of the project, that builds a Gem::Specification.
Because it is Ruby, it can compute values, but it is evaluated by gem build on your machine and by Bundler on every bundle install for a gem referenced with gemspec, path: or git:, so keep it fast, side-effect free and independent of your own library code.
The one thing it should load from the gem is the version file, with require_relative, so that Shelf::VERSION is defined without loading the rest of shelf (which may need dependencies that are not installed yet).
This is the gemspec of the shelf gem:
# frozen_string_literal: true
require_relative "lib/shelf/version"
Gem::Specification.new do |spec|
spec.name = "shelf"
spec.version = Shelf::VERSION
spec.authors = ["Example Author"]
spec.email = ["author@example.com"]
spec.summary = "A small library catalogue."
spec.description = "Shelf models books, authors and loans with plain Ruby objects " \
"and ships a small command-line catalogue browser."
spec.homepage = "https://example.com/shelf"
spec.license = "MIT"
spec.required_ruby_version = ">= 4.0"
spec.metadata = {
"homepage_uri" => spec.homepage,
"source_code_uri" => "https://example.com/shelf/source",
"changelog_uri" => "https://example.com/shelf/changelog",
"bug_tracker_uri" => "https://example.com/shelf/issues",
"rubygems_mfa_required" => "true"
}
spec.files = Dir["lib/**/*.rb", "sig/**/*.rbs", "exe/*", "README.md", "CHANGELOG.md", "LICENSE.txt"]
spec.bindir = "exe"
spec.executables = ["shelf"]
spec.require_paths = ["lib"]
end
Loading it from Ruby shows what RubyGems sees:
# frozen_string_literal: true
spec = Gem::Specification.load("shelf.gemspec")
puts spec.full_name
puts spec.runtime_dependencies.inspect
puts spec.required_ruby_version
puts spec.executables.inspect
puts spec.require_paths.inspect
puts spec.files.size
puts spec.metadata.keys.inspect
puts spec.license
puts spec.bindir
puts spec.platform
shelf-0.1.0
[]
>= 4.0
["shelf"]
["lib"]
14
["homepage_uri", "source_code_uri", "changelog_uri", "bug_tracker_uri", "rubygems_mfa_required"]
MIT
exe
ruby
Source: Gem::Specification.load
The fields
RubyGems requires five attributes and warns when other important ones are missing.
| Attribute | Meaning | Status |
|---|---|---|
|
The gem name, unique on the server. |
Required |
|
A version string such as |
Required |
|
An array of names (or one name). |
Required |
|
One line, shown by |
Required |
|
The files packed into the gem. Assign it; do not append. Directories are dropped and anything that is not a file is an error. |
Required |
|
A longer text, a few paragraphs at most, without formatting. |
Recommended |
|
One address or an array. |
Recommended |
|
The project URL. |
Recommended ( |
|
An SPDX identifier such as |
Recommended ( |
|
A hash of strings (keys up to 128 bytes, values up to 1024 bytes, UTF-8) for links and flags. |
Recommended |
|
A requirement on the Ruby version, for example |
Recommended ( |
|
A requirement on RubyGems itself. |
Optional |
|
A runtime dependency with optional version requirements. |
Optional |
|
A dependency needed only to work on the gem; not installed with it. |
Optional |
|
The directory of the commands and their file names. |
Optional |
|
Directories added to |
Optional |
|
Paths of |
Optional |
|
Defaults to |
Optional |
|
Text printed after installation. Use it rarely. |
Optional |
|
Extra files and options for RDoc when documentation is generated. |
Optional |
|
Free text about requirements outside RubyGems (a system library, for example). |
Optional |
|
Certificate chain and key for signed gems. |
Optional |
The read-only attributes rubygems_version and date are filled in by RubyGems at build time.
The date is not the wall clock: it is taken from the SOURCE_DATE_EPOCH environment variable when that is set, and is 1980-01-02 otherwise, which is one reason that two builds of the same sources are byte-identical (shown in the section on building).
Choosing the file list
spec.files is the allowlist of what ships.
Two idioms are common, and the choice matters for security as well as for correctness.
The first is an explicit list of globs, which is what shelf uses:
# frozen_string_literal: true
Gem::Specification.new do |spec|
spec.name = "files-glob"
spec.version = "0.1.0"
spec.authors = ["Example Author"]
spec.summary = "An explicit allowlist of globs."
spec.required_ruby_version = ">= 4.0"
spec.metadata["rubygems_mfa_required"] = "true"
# Predictable and independent of git, but a new kind of top-level file
# has to be added to the list by hand.
spec.files = Dir["lib/**/*.rb", "exe/*", "README.md", "LICENSE.txt"]
end
The second asks git, which is what the bundle gem template does:
# frozen_string_literal: true
Gem::Specification.new do |spec|
spec.name = "files-git"
spec.version = "0.1.0"
spec.authors = ["Example Author"]
spec.summary = "Whatever git tracks, minus development files."
spec.required_ruby_version = ">= 4.0"
spec.metadata["rubygems_mfa_required"] = "true"
# The bundle gem template: files that are not added to git are not packaged.
gemspec = File.basename(__FILE__)
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
ls.readlines("\x0", chomp: true).reject do |f|
(f == gemspec) || f.start_with?(*%w[bin/ test/ .github/ .rubocop.yml])
end
end
end
The git-based list has a trap that is easy to hit on the first release: a file that exists on disk but was never added to git is not packaged.
In a scratch copy of the generated gem, a new lib/pocket/extra.rb was missing from spec.files until it was staged:
$ ruby -e 'puts Gem::Specification.load("pocket.gemspec").files'
CHANGELOG.md
LICENSE.txt
README.md
Rakefile
exe/pocket
lib/pocket.rb
lib/pocket/version.rb
sig/pocket.rbs
$ git add lib/pocket/extra.rb
$ ruby -e 'puts Gem::Specification.load("pocket.gemspec").files.grep(/extra/)'
lib/pocket/extra.rb
The opposite trap is a glob that is too broad.
A gem is public and mirrored within minutes of publication, so a .env file, a local credentials file or a .git directory that a wide glob picks up cannot be recalled by yanking the version.
Prefer an allowlist, and read the packed file list before pushing (see the section on inspecting the archive).
Metadata keys
spec.metadata is a hash of string keys and string values.
The URI keys appear as links on the RubyGems.org page of the gem and must be valid http or https URLs:
| Key | Use |
|---|---|
|
Project home page. |
|
The source repository. |
|
The changelog; a link that gets users to "what changed" quickly. |
|
Where to file issues. |
|
Hosted API documentation. |
|
Other optional links. |
|
The string |
|
A URL; |
Licenses
Set license to a single SPDX identifier from https://spdx.org/licenses/.
Compound expressions such as MIT OR Apache-2.0 are not supported as one identifier; a gem under several licenses lists each identifier separately with licenses, and includes the full text of each in the gem.
For a license without an SPDX identifier use Nonstandard or LicenseRef-NAME.
A gem without a license means all rights are reserved: other people have no right to use the code.
Put the license text in a top-level LICENSE.txt and include it in spec.files.
Dependencies
A runtime dependency is declared in the gemspec and installed with the gem; a development dependency (a test framework, a linter) belongs in the Gemfile.
The shelf extension gem that adds the SQLite store from the scenario shows the three usual kinds of runtime dependency:
# frozen_string_literal: true
require_relative "lib/shelf/version"
Gem::Specification.new do |spec|
spec.name = "shelf-sqlite"
spec.version = Shelf::VERSION
spec.authors = ["Example Author"]
spec.summary = "SQLite store for the shelf catalogue."
spec.homepage = "https://example.com/shelf-sqlite"
spec.license = "MIT"
spec.required_ruby_version = ">= 4.0"
spec.metadata["rubygems_mfa_required"] = "true"
spec.files = Dir["lib/**/*.rb"]
# The gem this one extends: a lower bound only.
spec.add_dependency "shelf", ">= 0.1"
# A native gem that provides the database driver.
spec.add_dependency "sqlite3", ">= 2.0"
# csv became a bundled gem in Ruby 3.4 and must now be declared.
spec.add_dependency "csv"
end
gem build stores the dependencies, with their types, in the archive.
Reading them back from the loaded gemspec:
# frozen_string_literal: true
spec = Gem::Specification.load("shelf-sqlite.gemspec")
spec.dependencies.each do |dep|
puts format("%<name>-9s %<req>-7s %<type>s", name: dep.name, req: dep.requirement, type: dep.type)
end
shelf >= 0.1 runtime
sqlite3 >= 2.0 runtime
csv >= 0 runtime
Source: Gem::Specification#dependencies
A requirement left out (csv) means >= 0.
add_development_dependency "minitest", "~> 6.0" still works and is recorded with the type development (it is not installed with the gem), but RuboCop’s Gemspec/DevelopmentDependencies cop asks for these lines to live in the Gemfile instead, which is where bundle gem puts them.
Three rules govern the constraints:
-
Prefer lower bounds in a gemspec. A pessimistic constraint such as
~> 2.2(at least 2.2, below 3.0) locks every application that uses your gem to that range, even when the next major version of the dependency would have worked. Use~>only when you know of an incompatible change that will break your gem.~> 2stops before 3.0; it does not mean "2 or newer". Requirements combine as a list:">= 2.2", "< 4.0"or">= 2.0", "!= 2.2.1". -
Keep the
Gemfilethin. The generatedGemfilecontains the linegemspec, which adds the gem itself and its runtime dependencies to the bundle, followed by development tools such asrake,minitestandrubocopthat are not part of the shipped library. The shelfGemfilehas the same shape. -
Declare every library you
requirethat is not default. After Ruby 3.4 movedcsv,base64,bigdecimaland others out of the default gems, and Ruby 4.0 did the same forbenchmark,logger,ostruct,pstore,fiddleand more, a gem that requires them without declaring them works on the author’s machine and fails under Bundler. The remedy is a line likespec.add_dependency "csv".
What gem build checks
gem build validates the gemspec before it writes anything.
The problems fall into errors, which stop the build, and warnings, which do not.
These are the real messages for a deliberately poor gemspec.
A gemspec with a placeholder summary stops the build:
WARNING: See https://guides.rubygems.org/specification-reference/ for help
ERROR: While executing gem ... (Gem::InvalidSpecificationException)
"FIXME" or "TODO" is not a summary
A missing summary is reported as missing value for attribute summary, and a file list that names a file that does not exist as ["lib/missing.rb"] are not files.
A requirement RubyGems cannot parse (for instance "*") fails when the gemspec is loaded:
Invalid gemspec in [dep.gemspec]: Illformed requirement ["*"]
ERROR: Error loading gemspec. Aborting.
With a valid summary but no license, homepage or Ruby version, the build succeeds with warnings:
WARNING: licenses is empty, but is recommended. Use an license identifier from
https://spdx.org/licenses or 'Nonstandard' for a nonstandard license,
or set it to nil if you don't want to specify a license.
WARNING: no homepage specified
WARNING: make sure you specify the oldest ruby version constraint (like ">= 3.0") that you want your gem to support by setting the `required_ruby_version` gemspec attribute
WARNING: See https://guides.rubygems.org/specification-reference/ for help
Successfully built RubyGem
Name: bare
Version: 0.0.1
File: bare-0.0.1.gem
gem build --strict turns warnings into a failing exit status (it exited with status 1 for that gemspec), which is the right setting for a release job, and --force skips validation entirely, which is almost never what you want.
Versioning
RubyGems accepts any version string and attaches no meaning to its parts.
The convention is three numbers, MAJOR.MINOR.PATCH, and semantic versioning is the vocabulary most users assume: patch for fixes, minor for backwards-compatible additions, major for changes that can break callers.
It is a communication convention, not a rule enforced by anything.
Ruby itself, Rails and many other projects document their own schemes, so the best practice is to say what you do (a policy in the README, a changelog entry for every release, a deprecation warning one release before a removal) rather than to lean on the number.
Compatibility is wider than the method list: changing a return value or a default, raising required_ruby_version and tightening a dependency constraint can all break an application that resolved and ran before, and deserve the same visibility as a removed method.
A new gem starts at 0.1.0, which is what bundle gem writes, and 1.0.0 is widely read as "the API has settled".
The version lives in one constant, Shelf::VERSION in lib/shelf/version.rb, and the gemspec reads it.
Changing that constant and releasing is the whole mechanism.
A version number on RubyGems.org can never be reused, and yanking a release does not free the number. A broken release is fixed by pushing a new version.
Prerelease versions
A version that contains a letter is a prerelease: 1.0.0.rc1, 2.0.0.beta.3, 1.0.0.pre.
Installers ignore prereleases unless asked (gem install shelf --pre), and Bundler considers one only when a requirement names it, such as gem "shelf", ">= 1.0.0.rc1".
Gem::Version and Gem::Requirement let you probe the rules:
# frozen_string_literal: true
versions = %w[2.0.0 1.0.0 1.0.1 1.0.0.rc1 1.0.0.beta2 1.1.0.beta 1.0.0.alpha 1.0.0.beta10 1.0.0.beta9]
puts versions.map { |v| Gem::Version.new(v) }.sort.join(" < ")
v = Gem::Version.new("1.2.3")
p v.segments
p v.bump
p v.release
p Gem::Version.new("1.0.0.rc1").prerelease?
p Gem::Version.new("1.0") == Gem::Version.new("1.0.0")
p Gem::Version.new("1.0.0-rc1").to_s
p Gem::Version.new("1.4.2").approximate_recommendation
begin
Gem::Version.new("1.0.0+001")
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
req = Gem::Requirement.new("~> 2.2")
puts req
p req.satisfied_by?(Gem::Version.new("2.9.1"))
p req.satisfied_by?(Gem::Version.new("3.0.0"))
p Gem::Requirement.new(">= 2.2", "< 4.0").satisfied_by?(Gem::Version.new("3.5"))
p Gem::Requirement.new("~> 2").satisfied_by?(Gem::Version.new("2.9"))
p Gem::Requirement.new("~> 2.2.0").satisfied_by?(Gem::Version.new("2.3.0"))
begin
Gem::Requirement.new("*")
rescue Gem::Requirement::BadRequirementError => e
puts "#{e.class}: #{e.message}"
end
1.0.0.alpha < 1.0.0.beta2 < 1.0.0.beta9 < 1.0.0.beta10 < 1.0.0.rc1 < 1.0.0 < 1.0.1 < 1.1.0.beta < 2.0.0
[1, 2, 3]
#<Gem::Version "1.3">
#<Gem::Version "1.2.3">
true
true
"1.0.0.pre.rc1"
"~> 1.4"
ArgumentError: Malformed version number string 1.0.0+001
~> 2.2
true
false
true
true
false
Gem::Requirement::BadRequirementError: Illformed requirement ["*"]
Reading the output:
-
A version string is split into segments at dots and at letter/digit boundaries. Numeric segments compare as numbers (
beta10sorts afterbeta9), and a string segment sorts before any number, so every prerelease sorts before the release it leads up to. Stick toalpha,beta,pre,rc, which also happen to be in alphabetical order. -
Trailing zeros are ignored:
1.0equals1.0.0. -
A hyphen is read as
.pre., so the SemVer-style1.0.0-rc1is accepted and normalised to1.0.0.pre.rc1. -
SemVer build metadata (
1.0.0+001) is not a valid gem version. -
Gem::Version#bumpdrops the prerelease part and the last segment and increments the one before, so1.2.3.bumpis1.3.approximate_recommendationis the string for the usual pessimistic constraint,"~> 1.4". -
The pessimistic operator
~>raises the last given segment:"~> 2.2"allows 2.2 up to but excluding 3.0,"~> 2.2.0"allows 2.2.x only, and"~> 2"allows 2.x.Gem::Requirement.new("*")is not a valid requirement; write">= 0"for "any".
Releasing a version is two edits and a tag: change VERSION and move the ## [Unreleased] entries of CHANGELOG.md under the new number.
rake release then tags the commit as v plus the version (v0.1.0).
A repository that holds several gems can give each tag a prefix with Bundler::GemHelper.tag_prefix = "shelf-" in its Rakefile, which produces tags such as shelf-v0.1.0.
Executables
A gem can install commands.
Put a Ruby script without an extension in exe/ (the name of the command), start it with a #!/usr/bin/env ruby line, make it executable, and list it in the gemspec with bindir and executables.
The executable of shelf is the one used in Command-Line Programs:
#!/usr/bin/env ruby
# frozen_string_literal: true
require "optparse"
require "shelf"
options = { search: nil }
OptionParser.new do |opts|
opts.banner = "Usage: shelf [options]"
opts.on("-sTERM", "--search=TERM", "Search titles") { |t| options[:search] = t }
opts.on("-v", "--version", "Show version") do
puts Shelf::VERSION
exit
end
end.parse!
catalog = Shelf::Catalog.new
catalog.add(Shelf::Book.new(Shelf::Isbn.new("978-0-306-40615-7"), "ruby basics", Shelf::Author.new("A. Writer")))
term = options[:search]
catalog.select { |b| term.nil? || b.title.include?(term) }.each { |b| puts b }
Run from the project directory with the library on the load path, with bundle exec, and with a binstub:
ruby -w -Ilib exe/shelf --version
bundle exec shelf --version
bundle binstubs shelf
bin/shelf --version
0.1.0
0.1.0
Resolving dependencies...
0.1.0
Source: bundle binstubs; OptionParser
The rules for executables:
-
The gemspec names them, the file lives in
bindir.spec.executables = ["shelf"]plusspec.bindir = "exe"meansexe/shelf; do not write the directory inexecutables. The file must be inspec.files, and the packed copy keeps its executable bit. -
Executables are Ruby scripts. RubyGems documents that a gem can only ship Ruby scripts as executables, not a Bash script or a compiled binary. When the gem is installed, RubyGems generates a small wrapper in the Ruby executable directory (shown by
gem envasEXECUTABLE DIRECTORY) that activates the gem and runs the script, so the command works whichever Ruby the wrapper belongs to. -
bundle exec shelfworks inside the project because Bundler adds theexedirectories of the gems in the bundle to the path.bundle binstubs shelfwritesbin/shelf, which setsBUNDLE_GEMFILE, requiresbundler/setupand loads the real executable, sobin/shelfruns without typingbundle exec. The binstub is for your development environment and belongs inbin/, not inexe/. -
Keep the executable thin. Parse the command line in
exe/and delegate everything else to the library, so the behaviour can be tested without spawning a process. Exit codes, signals and standard streams are the subject of Command-Line Programs. -
Name collisions are real. Two gems that ship a command with the same name conflict when the second is installed, and RubyGems asks before overwriting the first. Check the name before you choose it.
Developing the gem locally
A typical working loop uses four commands:
bin/setup # bundle install and any other setup
bundle exec rake # the default task: tests and RuboCop
bin/console # IRB with the gem loaded
bundle exec shelf --search ruby
Source: Bundler: Developing a RubyGem
bin/console is generated by bundle gem; it requires bundler/setup and the gem, then starts IRB (see IRB and Debugging).
To use a gem you are developing from another project, point that project’s Gemfile at the checkout with gem "shelf", path: "../shelf" (or at a git repository with git:), and Bundler reads the gemspec in place.
The install tasks from bundler/gem_tasks build the gem and install it into the system gems of the current Ruby; rake install:local does so without network access.
They were not run for this page because they would install into the Ruby used here.
Running the default task of the shelf gem with the Rakefile shown at the end of this page, and printing the generated task list, gave:
$ bundle exec rake -T
rake build # Build shelf-0.1.0.gem into the pkg directory
rake build:checksum # Generate SHA512 checksum of shelf-0.1.0.gem into the checksums directory
rake clean # Remove any temporary products
rake clobber # Remove any generated files
rake install # Build and install shelf-0.1.0.gem into system gems
rake install:local # Build and install shelf-0.1.0.gem into system gems without network access
rake release[remote] # Create tag v0.1.0 and build and push shelf-0.1.0.gem to rubygems.org
rake rubocop # Run RuboCop
rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
rake test # Run tests
$ bundle exec rake
Run options: --seed 37829
# Running:
....
Finished in 0.000315s, 12698.4146 runs/s, 15873.0182 assertions/s.
4 runs, 5 assertions, 0 failures, 0 errors, 0 skips
Running RuboCop...
Inspecting 15 files
...............
15 files inspected, no offenses detected
Building the gem
gem build turns the gemspec into an archive named NAME-VERSION.gem.
gem build shelf.gemspec
Successfully built RubyGem
Name: shelf
Version: 0.1.0
File: shelf-0.1.0.gem
Options of gem build:
| Option | Effect |
|---|---|
|
Writes the archive to another file name. |
|
Builds for a platform (used for precompiled gems). |
|
Treats validation warnings as errors. |
|
Skips validation. Avoid it. |
bundle exec rake build does the same through Bundler: it runs gem build -V on the gemspec and moves the result into pkg/, printing shelf 0.1.0 built to pkg/shelf-0.1.0.gem., and rake build:checksum also writes a SHA-512 digest to checksums/shelf-0.1.0.gem.sha512.
Keep pkg/ and checksums/ out of version control (the generated .gitignore already lists /pkg/).
Inspect before you publish
A .gem file is a plain tar archive of three members: the compressed gemspec (metadata.gz), the compressed tarball of the files (data.tar.gz), and the checksums of both (checksums.yaml.gz).
That makes it easy to see exactly what would be published:
tar -tf shelf-0.1.0.gem
tar -xOf shelf-0.1.0.gem data.tar.gz | tar -tzf -
gem spec shelf-0.1.0.gem files
gem unpack shelf-0.1.0.gem --target tmp/unpacked
metadata.gz
data.tar.gz
checksums.yaml.gz
---
CHANGELOG.md
LICENSE.txt
README.md
exe/shelf
lib/shelf.rb
lib/shelf/author.rb
lib/shelf/book.rb
lib/shelf/catalog.rb
lib/shelf/isbn.rb
lib/shelf/loan.rb
lib/shelf/refinements/title_case.rb
lib/shelf/store/memory.rb
lib/shelf/version.rb
sig/shelf.rbs
(The --- line in the output separates the two commands. The files under lib/ are listed in full in the shelf gem source.)
gem spec shelf-0.1.0.gem prints the embedded gemspec as YAML, and gem spec FILE --ruby prints it as Ruby, which is the form to compare against what you meant to write.
Check three things every time: that nothing private is in the list (no .env, keys, local configuration or .git), that the executable keeps its executable bit after gem unpack, and that everything the library `require`s is in the list.
Builds are reproducible. Because the gemspec date comes from SOURCE_DATE_EPOCH (or the fixed 1980 default) and the archive members use fixed timestamps, building the same sources twice produced archives with the same SHA-256, even after touching a source file between the two builds.
That lets a third party rebuild a release and compare it with what was published.
Publishing to RubyGems.org
The commands in this section talk to a real server and publish code that cannot be recalled. They are described here and were deliberately not run for this page.
Before the first push
-
An account with MFA. Create an account on RubyGems.org with an e-mail address you control, then enable multi-factor authentication, preferably WebAuthn (a security key or passkey), which resists phishing; a one-time-password app also works. RubyGems.org requires MFA from the owners of the most downloaded gems, and you can require it for your own gem with
spec.metadata["rubygems_mfa_required"] = "true". -
A unique name. Search the index: every published gem needs a name that no other gem uses. A gem whose name is objectionable or infringes someone’s rights can be removed by the service.
-
A clean gemspec.
gem build --strictpasses,summaryanddescriptionare real text,allowed_push_hostis removed or set deliberately, and the license is declared. -
A green build.
bundle exec rakepasses, the version is bumped, the changelog is updated, and the working tree is committed. -
An inspected archive. The file list contains what you expect and nothing else.
Authentication
gem signin stores an API key for the host in the credentials file (gem env prints its location as CREDENTIALS FILE; on this machine it is under ~/.local/share/gem/, and older setups use ~/.gem/credentials).
RubyGems refuses to use a credentials file whose permissions are wider than 0600 and tells you to chmod 0600 it.
For a CI job, the key can instead be supplied in the GEM_HOST_API_KEY environment variable, and a one-time password in GEM_HOST_OTP_CODE (or with --otp).
API keys carry scopes: index_rubygems, push_rubygem, yank_rubygem, add_owner, remove_owner, access_webhooks and show_dashboard (the last one cannot be combined with another).
Create a key with only the scope it needs (a push-only key for a release job), give it an expiry date, and never reuse the key that came from the old single-key era of the service (legacy-key).
A key never belongs in a repository, in the gem, or in a CI log.
The push
gem push shelf-0.1.0.gem # not run: uploads the archive and registers the version
gem push shelf-0.1.0.gem --otp 123456
gem push shelf-0.1.0.gem --key ci-push-key
gem push shelf-0.1.0.gem --host https://gems.example.com
gem push shelf-0.1.0.gem --attestation shelf-0.1.0.sigstore.json
Usage: gem push GEM [options]
Options:
-k, --key KEYNAME Use the given API key
--otp CODE Digit code for multifactor authentication
--host HOST Push to another gemcutter-compatible host
--attestation FILE Push with sigstore attestations
(The usage text above is condensed from gem help push of RubyGems 4.0.20; the push commands themselves were not run.)
Without --host, RubyGems pushes to the server named by the gemspec metadata key default_gem_server, then by allowed_push_host, and otherwise to https://rubygems.org.
When allowed_push_host is set, a push to any other server fails with a message that the host "is not allowed by the gemspec".
The publishing guide lists two failures worth recognising: Access Denied. Please sign up for an account at https://rubygems.org usually means a stale credential (run gem signout and gem signin), and Rubygem requires owners to enable MFA. You must enable MFA before pushing new version. means the gem set rubygems_mfa_required and your account has no MFA.
rake release
bundler/gem_tasks wraps the whole sequence in one task.
Its description (from rake -D release) reads:
rake release[remote]
Create tag v0.1.0 and build and push shelf-0.1.0.gem to rubygems.org
To prevent publishing in RubyGems use `gem_push=no rake release`
Source: Bundler: Developing a RubyGem
The task depends on, in order: build, release:guard_clean, release:source_control_push and release:rubygem_push.
guard_clean aborts when the working tree has uncommitted changes (in the scratch copy, with a modified tracked file, it printed There are files that need to be committed first.); source_control_push creates the v0.1.0 tag if it does not exist yet and pushes the branch and the tag to the remote (the optional remote argument chooses which, by default the branch’s tracking remote); rubygem_push runs gem push on the built file, passing --host when allowed_push_host is set.
Setting gem_push=no (also n, false, off, 0) in the environment skips only the final push, which is a safe way to rehearse everything up to the upload.
It still pushes the branch and the tag, so it needs a remote.
Trusted publishing from GitHub Actions
An API key in CI is a long-lived secret. Trusted publishing removes it: a CI platform that acts as an OpenID Connect identity provider (GitHub Actions is one) issues a short-lived identity token for the running workflow, and RubyGems.org exchanges that token for a short-lived API key scoped to the gems that trust the workflow. Nothing is stored in the repository settings, and nothing remains valid after the job.
The one-time setup is on RubyGems.org, in the gem’s settings, under trusted publishers: enter the GitHub owner and repository, the workflow file name and, if you use one, the GitHub environment name. For a gem that has never been published, create a pending trusted publisher in your profile instead; it becomes a normal one, and you become the owner, after its first successful push. If the workflow is a reusable workflow from another repository, the optional workflow-repository fields must name the repository that holds it.
The workflow itself needs the id-token: write permission on the job (not the whole workflow) and contents: write so that rake release can push the tag:
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
push:
name: Push gem to RubyGems.org
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true
- name: Run the tests before releasing
run: bundle exec rake
- uses: rubygems/release-gem@v1
The rubygems/release-gem action configures git, obtains the short-lived credentials and then runs bundle exec rake release, so it requires that the project uses Bundler and has the bundler/gem_tasks tasks.
The GitHub environment (release) lets you add protection rules, such as required reviewers, so that not everyone with push access can publish.
The release is then triggered by pushing a tag, with no gem push, no API key and no OTP code.
After the push
-
Yanking.
gem yank shelf -v 0.1.0removes a version from the index and the gem file from the server. It does not free the version number, and it does not undo a leak: a pushed archive is mirrored by many services within minutes, so a published secret must be revoked first and the version yanked second. Fix a bad release with a new version. -
Owners.
gem owner shelf --add NAME-OR-EMAILand--removechange who may push, yank and edit the gem. Every owner has your full powers, so add only people you trust, and prefer an organisation with roles for a team. -
Security reports. If you suspect that your account or a version of your gem was compromised, yank the affected versions and report it to the RubyGems security contact.
-
Signing and attestations.
gem certcan create a certificate to sign gems and users can opt into verification with a trust policy, but there is no established chain of trust for the certificates and signing is rarely used. Sigstore attestations (gem push --attestation; the release action enables them by default, as an experimental feature) link a published gem to the workflow that built it, and are the direction the ecosystem is taking.
Private gems
If the gem must not reach RubyGems.org, set spec.metadata["allowed_push_host"] = "https://gems.example.com"; gem push and rake release then refuse any other server.
Serve the gems from a private server or a commercial gem host, and in consumers assign every private gem to its server with a source block in the Gemfile, so that a public gem with the same name cannot be substituted (a dependency-confusion attack).
A repository can also be used directly through Bundler with git: or path:, with no server at all.
Gems with C extensions
A gem can contain code that is compiled when the gem is installed. This page covers the packaging side; writing the C code, the Ruby C API, garbage-collection safety, FFI and WebAssembly are in C Extensions, FFI and WebAssembly.
The conventions for a gem named NAME:
my_malloc/
├── Rakefile
├── my_malloc.gemspec
├── ext/my_malloc/extconf.rb configures a Makefile
├── ext/my_malloc/my_malloc.c the C source, with Init_my_malloc
└── lib/my_malloc.rb requires "my_malloc/my_malloc"
-
extconf.rbusesmkmfto check that the headers, functions and libraries exist and ends withcreate_makefile "NAME/NAME"; it should abort when something is missing. -
The gemspec lists it in
extensionsand includes the sources infiles:# frozen_string_literal: true Gem::Specification.new "my_malloc", "1.0" do |spec| spec.summary = "Wraps malloc and free." spec.authors = ["Example Author"] spec.required_ruby_version = ">= 4.0" spec.metadata["rubygems_mfa_required"] = "true" spec.files = Dir["lib/**/*.rb", "ext/**/*.{c,h,rb}"] spec.extensions = ["ext/my_malloc/extconf.rb"] end -
On
gem install, RubyGems runsextconf.rb, thenmake, and the compiled library lands next to the Ruby files. The user therefore needs a compiler and the Ruby headers (theruby-devorruby-develpackages on many Linux distributions), which is the main cost of an extension. -
rake-compiler(a separate gem, added to theGemfileby the generator) adds acompiletask, so that the extension is built during development and tests. This is theRakefilethatbundle gem native --ext=cgenerated in a scratch directory:# frozen_string_literal: true require "bundler/gem_tasks" require "minitest/test_task" Minitest::TestTask.create require "rubocop/rake_task" RuboCop::RakeTask.new require "rake/extensiontask" task build: :compile GEMSPEC = Gem::Specification.load("native.gemspec") Rake::ExtensionTask.new("native", GEMSPEC) do |ext| ext.lib_dir = "lib/native" end task default: %i[clobber compile test rubocop]Source: rake-compiler; bundle gem manual
-
A gem that ships precompiled binaries sets
spec.platform(or builds withgem build --platform), and one archive is published per platform next to the source gem (nokogiri-1.6.0-x86-mingw32.gemis the guide’s example of the naming). Installers pick the matching platform and fall back to compiling the plainrubygem. -
bundle gem NAME --ext=cgenerates this layout:ext/NAME/extconf.rb,NAME.candNAME.h,spec.extensions = ["ext/NAME/extconf.rb"]in the gemspec, and aRakefilethat makesbuilddepend oncompile, as shown above. The generatedextconf.rbalso passes-fvisibility=hiddenso that the extension’s symbols do not clash with other gems.--ext=goand--ext=rustgenerate the Go and Rust equivalents (Rust through themagnusandrb-sysprojects). -
The RubyGems documentation also describes content-addressable builds that publish one gem per Ruby ABI for a platform. That is announced for RubyGems 4.1, which is newer than the 4.0.20 that ships with Ruby 4.0.7, so it is mentioned here only as upcoming.
Rake
Rake is a task runner written in Ruby, in the lineage of make: you describe tasks, say which tasks must run first, and run them by name.
Unlike make, the description is a Ruby program, so a Rakefile has loops, methods, string interpolation and every library you can require.
It is the standard tool for the chores of a Ruby project: tests, linting, building the gem, generating documentation, and anything else you would otherwise keep in a shell script.
Rake 13.3.1 is a bundled gem of Ruby 4.0.
Run it as bundle exec rake in a project that uses Bundler, so that the version in Gemfile.lock is the one that runs.
Finding the Rakefile
rake looks for a file called rakefile, Rakefile, rakefile.rb or Rakefile.rb in the current directory and then in each parent directory, and runs from the directory where it found it, announcing the directory when it differs from where you stand:
$ cd data
$ rake -T
(in /path/to/project)
rake books:count # Count the title files
rake books:list # List the title files
$ rake -N -T
rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
Source: Rake: Command line usage
-N (--no-search) disables the upward search, -f FILE names the file explicitly, and -C DIR changes directory first.
-s (--silent) suppresses the (in ...) line along with the other log messages.
Tasks
A task has a name, an optional description, optional prerequisites, and a block of Ruby.
desc documents the next task, and only tasks with a description appear in rake -T.
This Rakefile has four tasks that depend on each other in different ways:
# frozen_string_literal: true
desc "Say hello"
task :hello do
puts "hello"
end
desc "Greet someone (default: world)"
task :greet, [:name] do |_task, args|
args.with_defaults(name: "world")
puts "hello, #{args[:name]}"
end
task :prepare do
puts "prepare"
end
desc "Build after preparing"
task build: :prepare do
puts "build"
end
task :undocumented do
puts "nobody lists me"
end
namespace :db do
desc "Create the schema"
task :create do
puts "db:create"
end
desc "Load fixtures"
task seed: :create do
puts "db:seed"
end
end
task default: %i[build db:seed]
Source: Rake: Rakefile format; Rake::DSL
Running it, listing the tasks and dumping the dependency tree:
$ rake
prepare
build
db:create
db:seed
$ rake -T
rake build # Build after preparing
rake db:create # Create the schema
rake db:seed # Load fixtures
rake greet[name] # Greet someone (default: world)
rake hello # Say hello
$ rake -AT
rake build # Build after preparing
rake db:create # Create the schema
rake db:seed # Load fixtures
rake default #
rake greet[name] # Greet someone (default: world)
rake hello # Say hello
rake prepare #
rake undocumented #
$ rake -P
rake build
prepare
rake db:create
rake db:seed
create
rake default
build
db:seed
rake greet
rake hello
rake prepare
rake undocumented
$ rake -D db
rake db:create
Create the schema
rake db:seed
Load fixtures
$ rake nope
rake aborted!
Don't know how to build task 'nope' (See the list of available tasks with `rake --tasks`)
(See full trace by running task with --trace)
Source: Rake: Rakefile format
What the examples show:
-
task default: ...is the task that runs when no name is given. A project’sdefaulttask is by convention its whole local check. -
The two definition styles are equivalent:
task :build => :prepare(hash rocket) andtask build: :prepare(the form above). With arguments the prerequisite follows the argument list:task :greet, [:name] => :prepare. A prerequisite may be a symbol, a string or an array of them. -
Prerequisites run first, in the order listed, and each runs at most once per
rakeinvocation.defaultdepends onbuildanddb:seed;prepareran once even though several tasks could have requested it. -
-Tlists described tasks,-Aadds the undescribed ones (with-T),-Dprints the full description,-Pprints the prerequisites, and-Wprints where each task is defined (Rakefile:19:in '<top (required)>'forbuildabove, preceded by the absolute path). -
greet[name]in the list is the task’s argument signature; arguments are covered below. -
An unknown name aborts with the message above. Rake exits with status 1 whenever a task fails.
Namespaces
namespace :db do ... end prefixes the names of the tasks defined inside it, so seed becomes db:seed and cannot collide with a seed task elsewhere.
Inside a namespace, a prerequisite is resolved in the same namespace first (seed: :create means db:create).
To point at a task in another namespace give the full name ("db:seed"), and to point at the enclosing level start the name with ^ ("^build" inside namespace :a is the top-level build, while a plain :build there is a:build).
Namespaces nest, and a file task (below) is never namespaced, because its name is a path.
Arguments
A task may take positional arguments, declared as an array after the name and passed in square brackets on the command line.
The block receives the task and a Rake::TaskArguments object.
# frozen_string_literal: true
task :announce, [:isbn] do |_t, args|
puts "announce isbn=#{args[:isbn].inspect}"
end
desc "Lend a book to a member"
task :lend, %i[isbn member] => :announce do |_t, args|
isbn = args.fetch(:isbn)
member = args.fetch(:member, "anonymous")
puts "lending #{isbn} to #{member}"
puts "extras: #{args.extras.inspect}"
end
desc "Read a setting from the environment"
task :env do
puts "LEVEL=#{ENV.fetch('LEVEL', 'info')}"
end
$ rake "lend[9780306406157,Ada,spare]"
announce isbn="9780306406157"
lending 9780306406157 to Ada
extras: ["spare"]
$ rake "lend[9780306406157]"
announce isbn="9780306406157"
lending 9780306406157 to anonymous
extras: []
$ rake lend
announce isbn=nil
rake aborted!
KeyError: key not found: :isbn (KeyError)
.../Rakefile:9:in 'block in <top (required)>'
Tasks: TOP => lend
(See full trace by running task with --trace)
$ rake env LEVEL=debug
LEVEL=debug
$ rake -T
rake env # Read a setting from the environment
rake lend[isbn,member] # Lend a book to a member
Source: Rake::TaskArguments; Rake: Rakefile format
-
Quote the whole task name when it has brackets. In zsh (the default macOS shell) an unquoted
rake lend[1]is a glob pattern and fails withzsh: no matches found: lend[1]before Rake starts. Userake "lend[1]",rake lend\[1\]ornoglob rake lend[1]. -
Arguments are separated by commas without spaces; a space would split the shell word. Extra arguments are available as
args.extras. -
Read them with
args[:name]orargs.name(nil when missing),args.fetch(:name)andargs.fetch(:name, default)(aKeyErrorwhen missing and no default),args.to_h, andargs.with_defaults(name: value)to fill the missing ones. -
A prerequisite receives arguments by name.
announcedeclared:isbn, so it received theisbnthatlendwas given; a prerequisite that declares a name the caller does not have receives nil. -
NAME=valuewords on the command line are copied intoENVbefore any task runs, sorake env LEVEL=debugworks and is the usual way to pass options to tasks that take no arguments. That is the formrake releaseuses forgem_push=no.
Invoke, execute and enhance
A task is an object, Rake::Task[:name], and its block (the action) is separate from its prerequisites.
Defining a task twice does not replace it; the second task call adds an action:
# frozen_string_literal: true
task :setup do
puts "setup"
end
task :compile do
puts "compile"
end
# A second definition adds an action; it does not replace the first one.
task :compile do
puts "compile (second action)"
end
# enhance adds a prerequisite and an action to an existing task.
Rake::Task[:compile].enhance([:setup]) { puts "compile (enhanced)" }
desc "Show invoke, reenable and execute"
task :demo do
Rake::Task[:compile].invoke
puts "-- invoke again: nothing runs"
Rake::Task[:compile].invoke
puts "-- reenable, then invoke"
Rake::Task[:compile].reenable
Rake::Task[:compile].invoke
puts "-- execute skips prerequisites"
Rake::Task[:compile].execute
end
$ rake demo
setup
compile
compile (second action)
compile (enhanced)
-- invoke again: nothing runs
-- reenable, then invoke
compile
compile (second action)
compile (enhanced)
-- execute skips prerequisites
compile
compile (second action)
compile (enhanced)
Source: Rake::Task
-
Rake::Task[:name].invokeruns the prerequisites and then the actions, once. A secondinvokein the same process does nothing, which is what makes shared prerequisites safe and what surprises people who call a task twice. -
reenableclears that "already run" mark so the nextinvokeruns the task again (its prerequisites keep their own marks, which is whysetupdid not repeat above). -
executeruns the actions only, skipping the prerequisites and the once-only check. -
enhance(prerequisites) { ... }appends prerequisites and an action to an existing task. Gems use it to hook into tasks they did not define, for example to run something afterrake build. -
Tasks are found by name with
Rake::Task[name]; for a namespaced task use the full string,Rake::Task["db:seed"].
File tasks, directories and rules
A file task is about a file on disk: it runs when the target does not exist or is older than any of its prerequisites.
This is how Rake avoids rebuilding what has not changed, and it is the part of Rake that make users recognise.
The example builds a combined report from a directory of title files.
# frozen_string_literal: true
require "rake/clean"
SOURCES = FileList["data/*.txt"]
UPPER = SOURCES.pathmap("build/%n.up")
CLEAN.include("build/*.up")
CLOBBER.include("build")
directory "build"
rule ".up" => [->(name) { name.pathmap("data/%n.txt") }, "build"] do |t|
puts "convert #{t.source} -> #{t.name}"
File.write(t.name, File.read(t.source).upcase)
end
file "build/all.txt" => UPPER do |t|
puts "concatenate #{t.prerequisites.join(', ')}"
File.write(t.name, t.prerequisites.map { |f| File.read(f) }.join)
end
desc "Build the combined report"
task report: "build/all.txt"
task default: :report
The task graph that rake -P printed for it:
Running it, running it again, touching one input, forcing a full rebuild, and cleaning up:
$ rake
mkdir -p build
convert data/a.txt -> build/a.up
convert data/b.txt -> build/b.up
concatenate build/a.up, build/b.up
$ rake
$ touch data/a.txt; rake
convert data/a.txt -> build/a.up
concatenate build/a.up, build/b.up
$ rake -B
convert data/a.txt -> build/a.up
convert data/b.txt -> build/b.up
concatenate build/a.up, build/b.up
$ rake clean
$ ls build
all.txt
$ rake clobber
$ ls
Rakefile
data
$ rake -T
rake clean # Remove any temporary products
rake clobber # Remove any generated files
rake report # Build the combined report
Source: Rake: Rakefile format
Reading the output:
-
The second
rakedid nothing: every target was newer than its inputs. Aftertouch data/a.txtonlya.upand the report were rebuilt;b.upwas still current.-B(--build-all) rebuilds everything regardless. -
directory "build"defines a task that creates the directory and its parents (mkdir -p build). Rake logs the file operations it performs throughFileUtils. -
rule ".up" => [source, ...]says how to make any*.uptarget that does not have an explicit task. The source can be a suffix (".txt"), a regular expression or a lambda from the target name to the source name, as here; inside the blockt.sourceis the first prerequisite andt.nameis the target.--rulestraces the resolution when a rule does not fire. -
require "rake/clean"definescleanandclobber. Add patterns toCLEANfor intermediate files and toCLOBBERfor everything generated;clobberdepends onclean. Above,cleanremovedbuild/*.upand leftall.txt, andclobberremoved thebuilddirectory. -
The
-nflag (--dry-run) shows which tasks would run, without running them; the real dry-run output for thisRakefilebegins** Invoke default (first_time)and shows each action as** Execute (dry run) NAME, and tasks whose file prerequisites exist as(first_time, not_needed).
FileList and pathmap
Rake::FileList is an array of file names built from globs, lazily (the pattern is expanded when the list is first used) and with a handful of conveniences: exclude, ext and pathmap.
String#pathmap is the Rake extension that rewrites paths with %-codes: %d the directory, %f the file name, %n the name without extension, %x the extension, %X the path without extension, and %{old,new}d a substitution.
# frozen_string_literal: true
require "rake"
files = Rake::FileList["data/*.txt"]
p files
p files.ext(".bak")
p files.pathmap("%n")
p files.pathmap("%{data,build}d/%n.up")
p "lib/shelf/book.rb".pathmap("%d | %f | %n | %x | %X")
# exclude changes the list in place; ask for a copy first.
p files.dup.exclude("data/b.txt").to_a
p files.to_a
p Rake::FileList["data/*"].exclude(/\.md\z/).to_a
files.exclude("data/b.txt")
p files.to_a
["data/a.txt", "data/b.txt"]
["data/a.bak", "data/b.bak"]
["a", "b"]
["build/a.up", "build/b.up"]
"lib/shelf | book.rb | book | .rb | lib/shelf/book"
["data/a.txt"]
["data/a.txt", "data/b.txt"]
["data/a.txt", "data/b.txt"]
["data/a.txt"]
Source: Rake::FileList; Rake: Rakefile format
exclude mutates the list it is called on, so exclude from a copy (dup) when the original is still needed.
The directory of "lib/shelf/book.rb" is lib/shelf, and %X is lib/shelf/book.
Use FileList for file names in a Rakefile; for plain Ruby code prefer Dir.glob and Pathname, covered in IO, Files and Directories.
Running commands
Rake adds the FileUtils methods (cp, mv, rm_rf, mkdir_p, touch, …) to the DSL, logging each call, plus sh for external commands and ruby for running the current Ruby on a script or -e code.
# frozen_string_literal: true
desc "Run shell commands from a task"
task :shell do
sh "echo", "echoed by sh"
sh "echo quiet", verbose: false
ruby "-e", "puts RUBY_VERSION", verbose: false
sh "false" do |ok, status|
puts "ok=#{ok} exit=#{status.exitstatus}"
end
end
desc "A failing command stops the task"
task :broken do
sh "false"
puts "never printed"
end
$ rake shell
echo echoed by sh
echoed by sh
quiet
4.0.7
false
ok=false exit=1
$ rake broken
false
rake aborted!
Command failed with status (1): [false]
.../Rakefile:15:in 'block in <top (required)>'
Tasks: TOP => broken
(See full trace by running task with --trace)
Source: Rake::FileUtilsExt; Rake: Rakefile format
-
shechoes the command before running it (theverbosedefault) and raises when the command exits non-zero, which aborts the task and the wholerakerun. With a block, the block receives a success flag and aProcess::Statusand decides what to do. -
Prefer the multi-argument form,
sh "echo", user_input, which does not go through a shell, over a single interpolated string, which does and is a command-injection risk. See Processes, Signals and Shell. -
verbose: false(orverbose(false)for a wholeRakefile) silences the echo, and-q/--quietand-s/--silentdo the same from the command line.-nmakes theFileUtilshelpers print what they would do.
Parallel tasks
multitask runs its prerequisites in parallel threads, and rake -m makes every task a multitask; -j N limits the number of tasks that run at once (the default is the number of CPU cores plus four).
# frozen_string_literal: true
SLOW = %i[slow_a slow_b slow_c].freeze
SLOW.each do |name|
task name do
sleep 0.3
end
end
task serial: SLOW
multitask concurrent: SLOW
def elapsed
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
end
desc "Compare ordinary and parallel prerequisites"
task :compare do
%i[serial concurrent].each do |name|
SLOW.each { |slow| Rake::Task[slow].reenable }
seconds = elapsed { Rake::Task[name].invoke }
puts format("%<name>-10s %<seconds>.1fs", name:, seconds:)
end
end
$ rake compare
serial 0.9s
concurrent 0.3s
Source: Rake::MultiTask; Process.clock_gettime
The timings depend on the machine, but the ratio does not: three 0.3-second prerequisites took about 0.9 seconds one after another and about 0.3 seconds as a multitask.
Because they are threads, a multitask helps with waiting (processes, network, sleeping) and not with CPU-bound Ruby code; see Threads and the GVL.
Tasks that run in parallel must not depend on each other’s side effects or write to the same file.
Organising a large Rakefile
Three mechanisms split tasks across files.
rakelib/. Every *.rake file in a directory called rakelib next to the Rakefile is loaded automatically (-R DIR/--rakelib DIR names another directory).
A file rakelib/greetings.rake containing a namespace :greetings block is picked up with no require:
# frozen_string_literal: true
namespace :greetings do
desc "Defined in rakelib/greetings.rake"
task :hello do
puts "hello from rakelib"
end
end
$ rake -T
rake default # Run everything
rake greetings:hello # Defined in rakelib/greetings.rake
$ rake
hello from rakelib
Source: Rake: Rakefile format
import. import "tasks/docs.rake" in the Rakefile loads a file explicitly, after the Rakefile has finished, which is useful for generated or conditional task files. The Rakefile and the imported file are:
# frozen_string_literal: true
import "tasks/docs.rake"
task default: "docs:build"
# frozen_string_literal: true
namespace :docs do
desc "Build the documentation"
task :build do
puts "docs built"
end
end
$ rake
docs built
$ rake -T
rake docs:build # Build the documentation
Source: Rake::DSL#import
System-wide tasks. rake -g (--system) also loads *.rake files from ~/.rake (or the directory in RAKE_SYSTEM), and -G ignores them.
A reusable task library. To package a group of related tasks with options, subclass Rake::TaskLib (which includes the DSL) and define the tasks in the constructor.
This is how Rake::TestTask, Gem::PackageTask and RuboCop::RakeTask are built, and how a gem exposes tasks to its users:
# frozen_string_literal: true
require "rake/tasklib"
module Shelf
# Defines `<name>:count` and `<name>:list` tasks for a set of title files.
class TitlesTask < Rake::TaskLib
attr_accessor :name, :pattern
def initialize(name = :titles)
super()
@name = name
@pattern = "data/*.txt"
yield self if block_given?
define
end
private
def define
files = Rake::FileList[pattern]
namespace(name) do
define_count(files)
define_list(files)
end
end
def define_count(files)
desc "Count the title files"
task(:count) { puts "#{files.size} title files" }
end
def define_list(files)
desc "List the title files"
task(:list) { files.each { |file| puts file } }
end
end
end
Shelf::TitlesTask.new(:books) do |t|
t.pattern = "data/*.txt"
end
$ rake -T
rake books:count # Count the title files
rake books:list # List the title files
$ rake books:count
2 title files
Source: Rake::TaskLib; Rake::FileList
Tasks the standard library already provides
| Task library | Use |
|---|---|
|
Defines a |
|
Builds tar.gz, zip and similar archives of a source tree. |
|
Defines |
|
The |
|
|
|
A |
|
The |
Command-line options
| Option | Effect |
|---|---|
|
List tasks that have a description, optionally filtered by a pattern; |
|
Describe tasks in full; show where they are defined. |
|
Print every task and its prerequisites. |
|
Show what would run without running the actions. |
|
Log each task’s invoke and execute, and print full backtraces. |
|
Full backtrace without tracing, and a filter for noisy frames. |
|
Rebuild all prerequisites, including up-to-date file tasks. |
|
Treat all tasks as multitasks; limit parallelism. |
|
Choose the Rakefile, do not search parents, change directory. |
|
rakelib directory; use or ignore the system-wide rakefiles. |
|
|
|
Run Ruby code and exit; run and print the result; run and then continue. |
|
Quiet, silent, verbose. |
|
Trace rule resolution; show job statistics for parallel runs. |
|
Disable deprecation warnings. |
|
Print the version ( |
The RAKEOPT environment variable supplies default options, as if they were typed on every command line: RAKEOPT=-T rake printed the task list.
Tracing a single task with --trace=stdout printed the order in which Rake handled it:
$ rake --trace=stdout build
** Invoke build (first_time)
** Invoke prepare (first_time)
** Execute prepare
prepare
** Execute build
build
Source: Rake: Command line usage
The Rakefile of the shelf gem
Putting the pieces together, the Rakefile of shelf uses the Bundler gem tasks, a test task and the RuboCop task, and makes the default task run both checks:
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rake/testtask"
require "rubocop/rake_task"
Rake::TestTask.new(:test) do |t|
t.libs << "test" << "lib"
t.pattern = "test/**/*_test.rb"
t.warning = true
end
RuboCop::RakeTask.new
task default: %i[test rubocop]
Its dependency tree (rake -P, abridged) shows how release is composed:
rake build
rake build:checksum
build
rake default
test
rubocop
rake install
build
rake install:local
build
rake release
build
release:guard_clean
release:source_control_push
release:rubygem_push
rake release:rubygem_push
build
Source: Rake: Command line usage
The same file doubles as the CI entry point (bundle exec rake) and as the release entry point (bundle exec rake release, run by the trusted-publishing workflow).
Pitfalls
| Pitfall | What happens and what to do |
|---|---|
Placeholder |
|
A new file that is not in git |
With the generated |
A glob that is too wide |
Private files ship to a public index and cannot be recalled. Use an allowlist. |
|
They shadow libraries for every program that loads the gem. Put everything under |
Library code that edits |
Breaks resolution and surprises Bundler. Use |
Requiring a gem that is no longer default |
Works locally, fails under Bundler. Declare |
Pessimistic constraints in a gemspec |
Lock users out of newer versions of the dependency. Prefer a lower bound, and use |
Reusing or "fixing" a published version |
Not possible. Push a new version. |
A secret in the gem or in a log |
Yanking does not recall it. Revoke the secret first. |
API key with full scope in CI |
Use a push-only, expiring key, or trusted publishing. |
|
|
Calling |
The second call does nothing. Use |
|
|
|
A method defined at the top level of a |
|
It changes the list in place. Call it on a |
Parallel tasks that share state |
|
Version notes
| Version | Change |
|---|---|
RubyGems 3.2 |
|
Ruby 3.4 |
|
Ruby 4.0 |
RubyGems 4.0 and Bundler 4.0 ship with Ruby. |
RubyGems 4.1 (not part of Ruby 4.0.7) |
Content-addressable gems for one Ruby ABI ( |
References
-
RubyGems Guides: Specification Reference and Gem::Specification
-
RubyGems Guides: Trusted Publishing, rubygems/release-gem and ruby/setup-ruby
-
Setting up multi-factor authentication, Using MFA in the command line and API key scopes
-
gem command reference, bundle gem and Bundler: Developing a RubyGem
-
Rake documentation, Rakefile format, command line usage and ruby/rake
-
Rake::FileList, Rake::TaskLib, Rake::TestTask and Rake::Task