skip to content

In Rake, how do file, directory and rule tasks decide whether to rebuild, and why does a file task depending on a plain task always rebuild?

level: middleimportance: should knowfreq 28%

answer

  1. needed? compares mtimes
  2. missing file means needed
  3. plain task timestamp is Time.now
  4. rake/phony, directory, rule
  5. file tasks ignore namespace

basics

~20 s

A Rake file task runs only if its file is missing or older than a prerequisite's timestamp. A plain task's timestamp is always the current time, so a file task depending on one always rebuilds; rake/phony's phony task avoids that.

solid answer

~40 s

`file "pkg/ledger_kit-1.4.0.gem" => SOURCES do ... end` names the task after the file it produces. Its `needed?` is true when the file does not exist or any prerequisite's timestamp is newer than the file's mtime, so an up-to-date gem is not rebuilt. A prerequisite that is itself a file task contributes its mtime, while a **plain** `task`'s timestamp is `Time.now` — always newer — which forces a rebuild every run; `require "rake/phony"` adds a `phony` task with a timestamp of `Time.at 0` for grouping without that effect. `directory "pkg"` creates the directory and its parents only when missing. `rule ".html" => ".md"` synthesises file tasks for any `*.html` requested but not defined, with `t.source` pointing at the matching `.md`. File task names are not namespaced, and `rake -B` forces a full rebuild.

code

ruby · 19 lines
ruby
require_relative "lib/ledger_kit/version"
require "rake/clean"

GEM_FILE = "pkg/ledger_kit-#{LedgerKit::VERSION}.gem"
SOURCES  = FileList["lib/**/*.rb", "ledger_kit.gemspec"]
CLOBBER.include("pkg")

directory "pkg"

file GEM_FILE => ["pkg", *SOURCES] do
  sh "gem", "build", "ledger_kit.gemspec", "-o", GEM_FILE
end

desc "Build the gem if sources changed"
task build: GEM_FILE

rule ".html" => ".md" do |t|
  sh "ruby", "script/render_md.rb", t.source, t.name
end

go deeper

for a junior

Know that file tasks are named after the file they build and are skipped when it is up to date.

for a middle

Explain needed? via mtimes, why plain-task prerequisites force rebuilds, and what directory, rule and phony provide.

for a senior

Model build artefacts as file tasks so repeated runs are cheap and correct, and keep side-effecting steps such as pushes as plain tasks.

for a principal

Decide where incremental builds matter enough to model precisely and where a simple always-run task is the cheaper design.

## Tasks that produce files Most Rake tasks run whenever invoked. **File tasks** are different: they are named after the file they create, and Rake skips them when that file is already up to date. This is the part of Rake inherited from `make`. ## How needed? decides A `Rake::FileTask` runs its actions when **`needed?`** is true, which means either: 1. the file does **not exist**, or 2. some prerequisite's **timestamp** is newer than the file's modification time. Timestamps come from each prerequisite: | Prerequisite kind | Its timestamp | |---|---| | another file task, or an existing file | the file's mtime | | a file task whose file is missing | a special "late" time, so dependents rebuild | | a plain `task` | **`Time.now`** — always newer | | the `phony` task from `rake/phony` | `Time.at 0` — never newer | The third row is the classic trap. `file GEM_FILE => [:check_version]` looks harmless, but `check_version` is a plain task whose timestamp is the current time, so the gem is rebuilt on every run. Either make the prerequisite a file, or depend on `:phony` when you only need ordering or grouping. ## A gem build as file tasks - `SOURCES = FileList["lib/**/*.rb", "ledger_kit.gemspec"]` collects inputs; a `FileList` behaves like an Array of paths resolved lazily from globs. - `directory "pkg"` declares a task that creates `pkg` (and any parent directories) when missing. - `file GEM_FILE => ["pkg", *SOURCES] do sh "gem", "build", "ledger_kit.gemspec", "-o", GEM_FILE end` rebuilds only when a source file changed. - `task build: GEM_FILE` gives people a friendly name to type. Running `rake build` twice in a row does real work only the first time. ## Rules A **`rule`** synthesises file tasks on demand. `rule ".html" => ".md" do |t| ... end` says: whenever a file ending in `.html` is needed and no task defines it, create one whose source is the file with the same base name ending in `.md`. Inside the action, **`t.name`** is the target and **`t.source`** the matched source. Patterns can be regular expressions and sources can be computed by a proc; a regexp pattern needs parentheses around `rule(...)` because of Ruby's parsing. If the source itself is missing, Rake tries other rules recursively; `--rules` traces that search. ## Other details - **Namespaces do not apply** to file or directory tasks: a file task's name is a path, so it stays the same inside `namespace :gem`. - **Implicit file tasks**: naming an existing file that no task defines makes Rake treat it as a satisfied prerequisite. - **`rake -B`** (`--build-all`) treats every file prerequisite as out of date. - **`require "rake/clean"`** adds `clean` and `clobber` tasks driven by the `CLEAN` and `CLOBBER` file lists. ## When not to use file tasks If the output is not a file whose freshness can be judged by mtime — a database migration, a network upload, a git tag — use a plain task. Pushing a gem is a plain task that depends on the gem file task, not a file task itself.

  • Why does file GEM_FILE => [:check_version] rebuild the gem on every run?
    A plain task's timestamp is the current time, so it is always newer than the gem file, and `needed?` is true on every run. Depend on real input files instead, or on the `phony` task from `rake/phony`, whose timestamp is the epoch and therefore never forces a rebuild.
  • Inside namespace :gem, what is the full name of file "pkg/ledger_kit.gem"?
    Still `pkg/ledger_kit.gem`. File and directory task names are file paths, so Rake does not prefix them with the namespace. Only ordinary tasks become `gem:name`.
  • What do t.name and t.source hold inside rule ".html" => ".md"?
    `t.name` is the file being built, such as `README.html`, and `t.source` is the prerequisite the rule matched, such as `README.md`. Using them keeps one rule correct for every file it synthesises.

saying these in an interview costs you the question

  • Rake file tasks compare content hashes to decide whether to rebuild.
  • A plain task prerequisite never forces a file task to rebuild.
  • directory "pkg" deletes and recreates pkg on every run.
  • File tasks inside namespace :gem get a gem: prefix.
  • A rule replaces the actions of an explicit file task for the same target.