08 · Working with Rake Tasks¶
Rake ("Ruby Make") is Ruby's build-tool DSL — the thing running behind
rails db:migrate, rake spec, and countless custom project chores. A
Rakefile is just Ruby: task is a plain method call that registers a
named block you can invoke from the command line as rake <name>.
A first Rakefile¶
rake -T lists every task that has a desc above it (undocumented tasks
run fine but are hidden from this summary — a convention for keeping
internal/helper tasks out of the public task list).
Task dependencies¶
A task can depend on other tasks, which run first, in declaration order, each exactly once even if depended on from multiple places:
:greet runs to completion before :build's own block starts. This is
how rake assets:precompile style multi-step pipelines are put
together — small tasks composed via dependencies instead of one giant
method.
Namespaces¶
namespace groups related tasks under a prefix, exactly like a module
groups classes:
namespace :db do
task :migrate do
puts "Running migrations..."
end
task :seed => :migrate do
puts "Seeding data..."
end
end
db:seed depends on db:migrate within the same namespace — you refer
to it as plain :migrate inside the namespace block, and Rake resolves
it to db:migrate automatically.
File tasks — only rebuild when needed¶
Regular task always runs its block. file tasks are Rake's answer to
Make's original purpose: only run if the target doesn't exist yet, or is
older than its prerequisite:
file "output.txt" => "input.txt" do
content = File.read("input.txt")
File.write("output.txt", content.upcase)
puts "Generated output.txt"
end
Running rake output.txt again immediately does nothing and prints
nothing, because output.txt now exists and is newer than
input.txt — Rake compares file modification times and skips work it
doesn't need to redo. Touch input.txt (or edit it) and output.txt
becomes stale again, triggering a rebuild on the next run.
desc — documenting tasks¶
Only :bye shows up because it's the only task with a desc line
directly above it in this Rakefile — :greet, :build, and the db
namespace tasks above were left undocumented on purpose to demonstrate
the difference.
Tasks that take arguments¶
The quotes around the whole invocation matter in most shells — [ and
] are glob-special characters that the shell would otherwise try to
expand.
Rake-specific traps¶
- Task blocks always run when invoked directly via
task, even if nothing actually changed — onlyfiletasks get the "skip if up-to-date" behavior. Using plaintaskfor something that's expensive to redo (like a full data import) wastes time on every invocation. - A dependency runs once per
rakeinvocation, not once ever.rake a bwhere bothaandbdepend oncrunscexactly once, but runningrake aand thenrake bseparately runsctwice — Rake doesn't remember completed work across separate process invocations. - Namespaced task dependencies need the full path from outside the
namespace.
task :other => "db:migrate"(as a string, with the namespace prefix) from outsidenamespace :db do ... end, versus the bare:migratesymbol used inside the namespace block. - Rakefiles execute top-to-bottom at load time, before any task
runs — a
putsat the top level of a Rakefile (outside any task block) prints on every singlerakeinvocation, includingrake -T, which surprises people expecting task-scoped output only. filetasks compare modification time, not content. Touching a file without changing its content (touch input.txt) still counts as "newer" and triggers a rebuild — useful to know when a build seems to rerun for no visible reason.
How It Actually Works¶
A Rakefile is plain Ruby, evaluated top to bottom exactly like any
required file — task :name do ... end is a method call that registers
a Rake::Task object (name, prerequisites, and the block) into a global
task registry; it does not run the block immediately. Running
rake name looks the task up in that registry, resolves its prerequisite
tasks recursively (building a dependency graph and running each
prerequisite's block first, but only once even if multiple tasks depend on
it — Rake tracks which tasks have already been "invoked" this run), and
only then executes the named task's own block. namespace blocks work by
prefixing whatever tasks are defined inside them with namespace:name
before registering — it's a naming convention layered onto the same
registry, not a separate mechanism. Because a Rakefile is just Ruby, you
can freely mix ordinary method calls, requires, and conditionals into
task bodies — there is no restricted "task language" underneath.
Cheat sheet¶
| Task | Rake code |
|---|---|
| Define a task | task :name do ... end |
| Document a task | desc "..." above the task line |
| Depend on another task | task :b => :a do ... end |
| Multiple dependencies | task :c => [:a, :b] |
| Group under a namespace | namespace :db do ... end |
| Reference inside the namespace | task :seed => :migrate |
| Reference from outside | task :other => "db:migrate" |
| Rebuild only when stale | file "out" => "in" do ... end |
| Accept CLI arguments | task :t, [:arg] do \|t, args\| ... end |
| List documented tasks | rake -T |
| Set a default task | task :default => :spec |
Exercise¶
Build a Rakefile for a small project:
- A
:cleantask that removes any*.logfile in the current directory (useFileList["*.log"].each { \|f\| File.delete(f) }). - A
namespace :report do ... endwith ageneratetask depending oncleanthat writes areport.logfile with today's date, and ashowtask that prints the contents ofreport.log(or a friendly message if it doesn't exist yet). - A
desc-documented default task (task :default => "report:generate") so plainrakewith no arguments runs the whole pipeline. - Run
rake -T, thenrake, thenrake report:show, and paste the output of each.