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.

flowchart LR scaffold["bundle gem shelf"] --> code["write lib/ exe/ sig/ test/"] code --> spec["describe it in shelf.gemspec"] spec --> check["bundle exec rake: tests and RuboCop"] check --> bump["bump version.rb and CHANGELOG.md"] bump --> build["gem build: shelf-0.1.0.gem"] build --> inspect["inspect the file list"] inspect --> push["gem push or a tag with trusted publishing"] push --> index["RubyGems.org index: gem install shelf"]

The tools divide the work as follows.

Tool Role in the pipeline

bundle gem

Generates the project skeleton (gemspec, Rakefile, lib/, tests, CI, license).

gem build

Reads a gemspec and writes the .gem archive. It also validates the gemspec.

gem push, gem yank, gem owner, gem signin

Talk to a gem server (RubyGems.org by default): upload a version, withdraw one, manage owners, store an API key.

bundle exec rake

Runs the project tasks: test, rubocop, and the tasks that Bundler adds with bundler/gem_tasks (build, install, release).

Rake

A task runner written in Ruby: tasks, prerequisites and file-based rebuilds, described in a Rakefile.

git

The generated gemspec takes its file list from git ls-files, and rake release tags the commit.

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

--exe, --bin, -b

Creates exe/NAME and lists it in the gemspec. --no-exe turns it off.

--coc, --no-coc

Adds or skips CODE_OF_CONDUCT.md.

--changelog, --no-changelog

Adds or skips CHANGELOG.md.

--mit, --no-mit

Adds or skips an MIT LICENSE.txt. The copyright holder comes from the global git configuration.

-t, --test=minitest, --test=rspec, --test=test-unit, --no-test

Chooses the test framework and generates a test/ or spec/ skeleton.

--ci=github, --ci=gitlab, --ci=circle, --no-ci

Generates a CI configuration file (.github/workflows/main.yml for GitHub).

--linter=rubocop, --linter=standard, --no-linter

Adds the linter to the Gemfile and writes its configuration file.

--ext=c, --ext=go, --ext=rust, --no-ext

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.

--git

Runs git init in the new directory.

--github-username=NAME

Fills the user name into the README and the repository URLs.

-e, --edit=EDITOR

Opens the new gemspec in an editor.

--bundle, --no-bundle

Runs or skips bundle install after generation.

Four things about the command are easy to miss.

  • Answers are remembered. The first time you run bundle gem without --coc, --mit, --changelog, --test, --ci or --linter, it asks and stores the answer in Bundler’s global configuration (the gem.coc, gem.mit, gem.changelog, gem.test, gem.ci, gem.linter and gem.github_username settings). Later runs reuse the stored value, and a conflicting flag is ignored with the message shown above. Change a stored answer with bundle config set --global gem.test rspec.

  • The skeleton does not build yet. The generated gemspec contains TODO placeholders in summary, description and allowed_push_host, and gem build rejects them. bundle exec rake build on 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.rb contains assert false, so bundle exec rake test on a new gem reports Expected 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 never git 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

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" loads lib/shelf.rb, and lib/shelf/book.rb is shelf/book. A gem that ships lib/set.rb or lib/erb.rb shadows or breaks the library of the same name for every program that loads the gem. Namespacing under lib/shelf/ avoids that.

  • Use require_relative inside the gem, never File.join(File.dirname(__FILE__), ...) and never $LOAD_PATH.unshift. RubyGems has already put lib on the load path, and a gem that edits $LOAD_PATH can change which file a later require finds. The entry point of shelf is a list of require_relative calls.

  • 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 in sig/; commands in exe/; native sources in ext/. Tests, CI files and bin/ scripts stay out of the packaged gem.

Names carry a convention that makes require guessable:

Gem name require Constant

shelf

require "shelf"

Shelf

shelf_import (several words)

require "shelf_import"

ShelfImport

shelf-sqlite (extends another gem)

require "shelf/sqlite"

Shelf::Sqlite

shelf-open_library (both)

require "shelf/open_library"

Shelf::OpenLibrary

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

The fields

RubyGems requires five attributes and warns when other important ones are missing.

Attribute Meaning Status

name

The gem name, unique on the server.

Required

version

A version string such as "0.1.0"; a letter in it makes a prerelease.

Required

authors (or author)

An array of names (or one name).

Required

summary

One line, shown by gem list -d and on the gem page. TODO and FIXME are rejected.

Required

files

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

description

A longer text, a few paragraphs at most, without formatting. TODO and FIXME are rejected.

Recommended

email

One address or an array.

Recommended

homepage

The project URL.

Recommended (gem build warns without it)

license, licenses

An SPDX identifier such as MIT or Apache-2.0, at most 64 characters. Use licenses for more than one.

Recommended (gem build warns without it)

metadata

A hash of strings (keys up to 128 bytes, values up to 1024 bytes, UTF-8) for links and flags.

Recommended

required_ruby_version

A requirement on the Ruby version, for example ">= 4.0".

Recommended (gem build warns without it)

required_rubygems_version

A requirement on RubyGems itself.

Optional

add_dependency (alias add_runtime_dependency)

A runtime dependency with optional version requirements.

Optional

add_development_dependency

A dependency needed only to work on the gem; not installed with it.

Optional

bindir, executables

The directory of the commands and their file names.

Optional

require_paths

Directories added to $LOAD_PATH when the gem is activated. The default is ["lib"].

Optional

extensions

Paths of extconf.rb files that build native extensions at install time.

Optional

platform

Defaults to ruby (pure Ruby). Set only for gems that contain precompiled code.

Optional

post_install_message

Text printed after installation. Use it rarely.

Optional

extra_rdoc_files, rdoc_options

Extra files and options for RDoc when documentation is generated.

Optional

requirements

Free text about requirements outside RubyGems (a system library, for example).

Optional

cert_chain, signing_key

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

homepage_uri

Project home page.

source_code_uri

The source repository.

changelog_uri

The changelog; a link that gets users to "what changed" quickly.

bug_tracker_uri

Where to file issues.

documentation_uri

Hosted API documentation.

mailing_list_uri, wiki_uri, funding_uri

Other optional links.

rubygems_mfa_required

The string "true" makes RubyGems.org refuse to publish new versions unless every owner has multi-factor authentication enabled. The setting takes effect from the version that carries it.

allowed_push_host

A URL; gem push and rake release refuse to push to any other server. The generated skeleton contains a TODO here so that a private gem is not published to RubyGems.org by accident. Delete the line for a public gem or set it to your own server.

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

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. ~> 2 stops 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 Gemfile thin. The generated Gemfile contains the line gemspec, which adds the gem itself and its runtime dependencies to the bundle, followed by development tools such as rake, minitest and rubocop that are not part of the shipped library. The shelf Gemfile has the same shape.

  • Declare every library you require that is not default. After Ruby 3.4 moved csv, base64, bigdecimal and others out of the default gems, and Ruby 4.0 did the same for benchmark, logger, ostruct, pstore, fiddle and more, a gem that requires them without declaring them works on the author’s machine and fails under Bundler. The remedy is a line like spec.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 (beta10 sorts after beta9), and a string segment sorts before any number, so every prerelease sorts before the release it leads up to. Stick to alpha, beta, pre, rc, which also happen to be in alphabetical order.

  • Trailing zeros are ignored: 1.0 equals 1.0.0.

  • A hyphen is read as .pre., so the SemVer-style 1.0.0-rc1 is accepted and normalised to 1.0.0.pre.rc1.

  • SemVer build metadata (1.0.0+001) is not a valid gem version.

  • Gem::Version#bump drops the prerelease part and the last segment and increments the one before, so 1.2.3.bump is 1.3. approximate_recommendation is 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

The rules for executables:

  • The gemspec names them, the file lives in bindir. spec.executables = ["shelf"] plus spec.bindir = "exe" means exe/shelf; do not write the directory in executables. The file must be in spec.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 env as EXECUTABLE DIRECTORY) that activates the gem and runs the script, so the command works whichever Ruby the wrapper belongs to.

  • bundle exec shelf works inside the project because Bundler adds the exe directories of the gems in the bundle to the path. bundle binstubs shelf writes bin/shelf, which sets BUNDLE_GEMFILE, requires bundler/setup and loads the real executable, so bin/shelf runs without typing bundle exec. The binstub is for your development environment and belongs in bin/, not in exe/.

  • 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

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

-o, --output FILE

Writes the archive to another file name.

--platform PLATFORM

Builds for a platform (used for precompiled gems).

--strict

Treats validation warnings as errors.

--force

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

  1. 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".

  2. 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.

  3. A clean gemspec. gem build --strict passes, summary and description are real text, allowed_push_host is removed or set deliberately, and the license is declared.

  4. A green build. bundle exec rake passes, the version is bumped, the changelog is updated, and the working tree is committed.

  5. 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`

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.0 removes 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-EMAIL and --remove change 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 cert can 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.rb uses mkmf to check that the headers, functions and libraries exist and ends with create_makefile "NAME/NAME"; it should abort when something is missing.

  • The gemspec lists it in extensions and includes the sources in files:

    # 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 runs extconf.rb, then make, and the compiled library lands next to the Ruby files. The user therefore needs a compiler and the Ruby headers (the ruby-dev or ruby-devel packages on many Linux distributions), which is the main cost of an extension.

  • rake-compiler (a separate gem, added to the Gemfile by the generator) adds a compile task, so that the extension is built during development and tests. This is the Rakefile that bundle gem native --ext=c generated 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]
  • A gem that ships precompiled binaries sets spec.platform (or builds with gem build --platform), and one archive is published per platform next to the source gem (nokogiri-1.6.0-x86-mingw32.gem is the guide’s example of the naming). Installers pick the matching platform and fall back to compiling the plain ruby gem.

  • bundle gem NAME --ext=c generates this layout: ext/NAME/extconf.rb, NAME.c and NAME.h, spec.extensions = ["ext/NAME/extconf.rb"] in the gemspec, and a Rakefile that makes build depend on compile, as shown above. The generated extconf.rb also passes -fvisibility=hidden so that the extension’s symbols do not clash with other gems. --ext=go and --ext=rust generate the Go and Rust equivalents (Rust through the magnus and rb-sys projects).

  • 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)

-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]

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)

What the examples show:

  • task default: ... is the task that runs when no name is given. A project’s default task is by convention its whole local check.

  • The two definition styles are equivalent: task :build => :prepare (hash rocket) and task 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 rake invocation. default depends on build and db:seed; prepare ran once even though several tasks could have requested it.

  • -T lists described tasks, -A adds the undescribed ones (with -T), -D prints the full description, -P prints the prerequisites, and -W prints where each task is defined (Rakefile:19:in '<top (required)>' for build above, 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
  • 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 with zsh: no matches found: lend[1] before Rake starts. Use rake "lend[1]", rake lend\[1\] or noglob 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] or args.name (nil when missing), args.fetch(:name) and args.fetch(:name, default) (a KeyError when missing and no default), args.to_h, and args.with_defaults(name: value) to fill the missing ones.

  • A prerequisite receives arguments by name. announce declared :isbn, so it received the isbn that lend was given; a prerequisite that declares a name the caller does not have receives nil.

  • NAME=value words on the command line are copied into ENV before any task runs, so rake env LEVEL=debug works and is the usual way to pass options to tasks that take no arguments. That is the form rake release uses for gem_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].invoke runs the prerequisites and then the actions, once. A second invoke in the same process does nothing, which is what makes shared prerequisites safe and what surprises people who call a task twice.

  • reenable clears that "already run" mark so the next invoke runs the task again (its prerequisites keep their own marks, which is why setup did not repeat above).

  • execute runs 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 after rake 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:

flowchart TD report["report (task)"] --> all["build/all.txt (file)"] all --> a["build/a.up (rule)"] all --> b["build/b.up (rule)"] a --> da["data/a.txt"] b --> db["data/b.txt"] a --> dir["build (directory)"] b --> dir

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

Reading the output:

  • The second rake did nothing: every target was newer than its inputs. After touch data/a.txt only a.up and the report were rebuilt; b.up was 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 through FileUtils.

  • rule ".up" => [source, ...] says how to make any *.up target 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 block t.source is the first prerequisite and t.name is the target. --rules traces the resolution when a rule does not fire.

  • require "rake/clean" defines clean and clobber. Add patterns to CLEAN for intermediate files and to CLOBBER for everything generated; clobber depends on clean. Above, clean removed build/*.up and left all.txt, and clobber removed the build directory.

  • The -n flag (--dry-run) shows which tasks would run, without running them; the real dry-run output for this Rakefile begins ** 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"]

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)
  • sh echoes the command before running it (the verbose default) and raises when the command exits non-zero, which aborts the task and the whole rake run. With a block, the block receives a success flag and a Process::Status and 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 (or verbose(false) for a whole Rakefile) silences the echo, and -q/--quiet and -s/--silent do the same from the command line. -n makes the FileUtils helpers 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

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

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

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

Tasks the standard library already provides

Task library Use

Rake::TestTask

Defines a test task that runs a set of test files in a child Ruby. Options: libs, pattern, test_files, warning (default true, which runs Ruby with -w), verbose, options, ruby_opts, loader, deps, description.

Rake::PackageTask

Builds tar.gz, zip and similar archives of a source tree.

Gem::PackageTask

Defines gem, package and repackage tasks from a Gem::Specification, for projects that do not use bundler/gem_tasks.

Bundler::GemHelper (bundler/gem_tasks)

The build, install and release tasks used above.

Rake::ExtensionTask (rake-compiler)

compile and cross-compilation tasks for C extensions.

RuboCop::RakeTask

A rubocop task, with the autocorrect variants.

Minitest::TestTask

The test task generated by bundle gem.

Command-line options

Option Effect

-T [PATTERN], -A

List tasks that have a description, optionally filtered by a pattern; -A includes the undescribed ones.

-D [PATTERN], -W [PATTERN]

Describe tasks in full; show where they are defined.

-P

Print every task and its prerequisites.

-n, --dry-run

Show what would run without running the actions.

-t, --trace[=OUT]

Log each task’s invoke and execute, and print full backtraces. OUT is stderr (default) or stdout.

--backtrace[=OUT], --suppress-backtrace PATTERN

Full backtrace without tracing, and a filter for noisy frames.

-B, --build-all

Rebuild all prerequisites, including up-to-date file tasks.

-m, -j [N]

Treat all tasks as multitasks; limit parallelism.

-f FILE, -N, -C DIR

Choose the Rakefile, do not search parents, change directory.

-R DIR, -g, -G

rakelib directory; use or ignore the system-wide rakefiles.

-r MODULE, -I DIR

require a library before the Rakefile loads; add a directory to the load path.

-e CODE, -p CODE, -E CODE

Run Ruby code and exit; run and print the result; run and then continue.

-q, -s, -v

Quiet, silent, verbose.

--rules, --job-stats

Trace rule resolution; show job statistics for parallel runs.

-X

Disable deprecation warnings.

-V

Print the version (rake, version 13.3.1).

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

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

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 TODO text in the generated gemspec

gem build rejects it. Fill in summary and description, and remove or set allowed_push_host.

A new file that is not in git

With the generated git ls-files file list the file is not packaged. git add it, or use an explicit glob list. Check the archive before pushing.

A glob that is too wide

Private files ship to a public index and cannot be recalled. Use an allowlist.

lib/ files with generic names (lib/set.rb, lib/json.rb)

They shadow libraries for every program that loads the gem. Put everything under lib/NAME/.

Library code that edits $LOAD_PATH or calls gem

Breaks resolution and surprises Bundler. Use require_relative and gemspec dependencies.

Requiring a gem that is no longer default

Works locally, fails under Bundler. Declare csv, logger, ostruct and the other bundled gems you use.

Pessimistic constraints in a gemspec

Lock users out of newer versions of the dependency. Prefer a lower bound, and use ~> only with a reason.

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.

rake release from a dirty tree or the wrong branch

guard_clean stops the first; for the second, check git branch before you tag.

Calling Rake::Task[:x].invoke twice

The second call does nothing. Use reenable or execute deliberately.

rake task[arg] in zsh

no matches found. Quote the name: rake "task[arg]".

def in a Rakefile

A method defined at the top level of a Rakefile becomes a private method of Object and is visible everywhere in the process. Name helpers carefully, or keep them in a module under lib/.

FileList#exclude on a list you still need

It changes the list in place. Call it on a dup.

Parallel tasks that share state

multitask runs in threads. Make the prerequisites independent.

Version notes

Version Change

RubyGems 3.2

gem signin can create API keys with chosen scopes.

Ruby 3.4

csv, base64, bigdecimal and observer become bundled gems (they were default gems in 3.3) and must be declared in a gemspec or Gemfile.

Ruby 4.0

RubyGems 4.0 and Bundler 4.0 ship with Ruby. benchmark, logger, ostruct, pstore, fiddle, irb and rdoc are bundled gems (they were default gems in 3.4). Rake 13.3.1 is the bundled Rake.

RubyGems 4.1 (not part of Ruby 4.0.7)

Content-addressable gems for one Ruby ABI (gem build --content-addressable). Not available in the RubyGems that ships with Ruby 4.0.7.