I’ve been reading a lot of Rakefiles lately, and it’s obvious that the folks writing them think of Rake as An Engine For Encapsulating Tasks. That’s fine, but it’s only part of what Rake can do.

I Do Declare

Rake is all about resolving dependencies. It’s about being declarative, not imperative. Especially when dealing with files and directories, this can make a huge difference. Here’s an example of a imperative task:

desc "Make sure your database.yml is in place"
task :wheres_your_database_yml do
  unless File.exists? File.join("config", "database.yml")
    puts "Where's your database.yml, dude?"
    if File.exist? File.join("config", "database.yml.example")
      puts "(elided)... so I'll make a copy of it for you."

      cp File.join("config", "database.yml.example"),
        File.join("config", "database.yml")

        abort "(elided)...rerun the last command."
    else
      abort "(elided)...There's no config/database.yml.example..."
    end
  end
end

(This is from technicalpickles’ dude-wheres-your-database-yml, a nice little Rails plugin for automatically installing a database.yml file the first time a new contributor runs Rake. I’ve shortened a few of the messages, but that’s it.)

This says, “Hey, if there’s not a config/database.yml file, create it by copying config/database.yml.example. If THAT doesn’t exist, complain.”

Here’s how to write this in a more declarative way:

file "config/database.yml" => "config/database.yml.example" do |t|
  sh "cp #{t.prerequisites.first} #{t.name}"
end

(This doesn’t abort or talk to the user, but you get the idea.)

This says essentially the same thing. file declarations in Rake behave just like tasks – they can have prerequisites, be dependencies, and be invoked via rake – but they only run if the target doesn’t exist or the prerequisites are newer than the target.

There’s also a shortcut for creating directories:

directory "foo"

# ...is the same as:
file("foo") { |t| mkdir t.name  }

It’s also possible to declare a transformation for a set of files, not just a single file. Let’s say that you have a directory of Markdown files, and you want to be able to transform any of ‘em into HTML. Easy:

rule ".html" => ".markdown" do |t|
  sh "markdown #{t.source} > #{t.name}"
end

# in your shell
$ rake foo.html

Yay! Again, this will only run when the dependencies are unresolved, that is, if the target is missing or the source is newer.

Picking Nits

A grab-bag of other bad usage I’ve seen recently:

Prerequisites

task :mystuff do
  # ...
end

task :something => :mystuff

This isn’t necessary. Unless you want to be able to use your mystuff task independently, just define something again:

task :something do
  # my stuff...
end

“Redefining” an existing task in Rake appends the new actions (and prerequisites) to the original. Don’t add new tasks unless you actually need them.

Explicit Invocation

Sometimes it’s necessary to run Rake tasks imperatively. That’s fine. Run them with invoke, not with execute:

task :mine do
  # some setup stuff
  Rake::Task["theirstuff"].invoke
end

invoke uses the same rules as normal Rake execution: If the task has run already, it won’t run again. execute runs the task no matter what. Make sure you know which one you want.

Fin

For a more detailed explanation of directory, file, and rule, take a look at the Rake documentation.