to read: rake

  1. http://martinfowler.com/articles/rake.html
  2. http://erik.debill.org/2011/12/04/rake-for-rails-developers (finished reading)

Code Organization

Rake tasks go in the lib/tasks directory, in files with a .rake extension. These aren’t loaded during normal application boot, so whenever possible you should minimize the amount of code that goes into those .rake files and instead have them call code that’s in other parts of the codebase. Think of rake tasks as actions for a command line application. They should do some argument processing and then hand things off to business logic that lives in your models or other classes.

Try not to let your .rake files get too big. Code in them is hard to test and inaccessible from the rest of your app.

Basic Rake Task

Here’s a very basic .rake file, with a simple task.

namespace :demo do

  desc "a basic task"
  task :basic do |t, args|
    puts "I'm a basic task"
  end

end

You’d call this with ‘bundle exec rake demo:basic‘.

First off, notice the namespace :demo line. I’ve named this to match the filename (demo.rake). Tasks within this namespaces will be called with demo: before their names. This gives you a way to indicate that some tasks are related to each other. This gets important when you have a lot of them. The naming convention helps you find the right file.

Next, you see the desc line. This gives a description that will be shown when you run rake -T. Keep it short – rake will truncate it to fit it on a single line in the user’s terminal.

If you do not include a desc line, the task will not show up when you run rake -T. You can make use of that to create “private” tasks that aren’t advertised.

The task line is the start of the actual task declaration. It names the task, declares args and dependencies and starts up the block of code that is executed when the task is run. The |t,args| on the block are optional, but I’d recommend always including them to help remind you about how to pass args. Consider them boilerplate.

Try to keep your tasks short and sweet, just like methods. These are every bit as much application code as anything in a controller or model, and the same rules about clear naming, documentation and short methods that apply to other code apply equally to rake tasks.

Dependencies

Rake tasks can depend on other things. You do this by putting ‘= > [:list, : of, :dependencies]‘ between the task name and the do that starts the task block. You can list dependencies as either symbols or strings. Most people use symbols and fall back to strings when forced to by syntax. If a dependency is in the same namespace you don’t have to include the namespace part. For dependencies outside your current namespace you pretty much need to list them as strings.

Dependencies are a great way to break longer rake tasks down into shorter tasks and make it easier to re-use code. All dependencies will be run before the body of a task is executed, so any setup code will itself have to go in a dependency.

You’ll notice that I never accessed any of my application’s models in the basic demo task. That’s because by default your rails application isn’t loaded. The powers that be know you need this a lot, so they have a handy rake task called :environment that will do it for you. Just list it as a dependency. This is a great example of extracting some commonly used code into a separate task that’s brought in as a dependency.

Here are a couple rake tasks that make use of dependencies:

namespace :deps do
  desc "uses the environment"
  task :with_environment => [:environment] do |t, args|
    puts "User count: #{User.count}"
  end

  desc "run both one and two"
  task :both => [: one, :two] do |t, args|
    puts "both"
  end

  desc "print 'one'"
  task :one do |t, args|
    puts "one"
  end

  desc "print 'two'"
  task :two do |t, args|
    puts "two"
  end

end

Basically, anything on the task line in the array after the hash rocket will be run first. You can add any number of dependencies by adding them to array, but most of the time you’ll just want the environment.

Multiple dependencies will be satisfied in the order specified, but will only be called once for the entire invocation of rake. This means that each task will only run once, even if it’s a dependency of multiple other tasks.

rake_demo > bundle exec rake deps:with_environment deps:both deps:one
User count: 0
one
two
both
rake_demo >

Loading the rails environment will make your rake task take a lot longer to run. If possible, avoid loading rails. Your fellow developers will thank you for it.

Arguments

Sometimes, you want to pass in an argument or two to your task. The tasks that come with rails usually pass arguments via environment variables. This gives you named arguments, but you can’t differentiate arguments for different tasks and you can’t use the very handy argument handling code that comes with rake.

Rake’s built-in argument handling code gets arguments like this: ‘bundle exec rake db:dump[filename]‘.

Arguments are listed in an array on the task line, after the task is named, but before the declaration of dependencies (if any). task :name, [:arg1, :arg2] => [:dep1, :dep2] do |t, args|

Here’s a simple rake task that accepts some arguments.

namespace :args do
  desc "takes dimensions as arguments"
  task :dimensions, [:x,:y] do |t, args|
    args.with_defaults(:x => 50, :y => 100)

    puts "dimensions are #{args[:x]}x#{args[:y]}"
  end
end

Arguments are ordered, not named. However, rake automatically parses them into a hash for you based on the order you declare them on the task line. It also gives you a handy way to set defaults for those arguments. Check out the #with_defaults method. It will make your life easier.

rake_demo > bundle exec rake -T args
rake args:dimensions[x,y]  # takes dimensions as arguments
rake_demo > bundle exec rake args:dimensions
dimensions are 50x100
rake_demo > bundle exec rake args:dimensions[25]
dimensions are 25x100
rake_demo > bundle exec rake args:dimensions[25,75]
dimensions are 25x75
rake_demo >

You don’t get a lot of guarantees about what type the arguments will show up as (e.g. String vs Integer), so don’t forget to do appropriate conversions to keep yourself safe. Likewise, rake doesn’t consider arguments mandatory, so you’ll need to enforce that manually.

Calling Other Tasks

Sometimes, you want to call another rake task, but for some reason you don’t want to just list it as a dependency. Perhaps you need to calculate some values to pass to it as arguments, or you want to force it to run even if it was already invoked as a dependency by some other task.

You can do this by looking up the task using Rake::Task['task_name_as_string'] and calling #invoke on it.

Pass your arguments to the #invoke call just like any normal method call.

namespace :invoke do

  desc 'invoke bar with random argument'
  task :foo do |t, args|
    n = rand(5)

    Rake::Task['invoke:bar'].invoke n
  end

  desc 'default is 3'
  task :bar, [:n] do |t, args|
    args.with_defaults(:n => 3)
    puts "n is #{args['n']}"
  end

end

rake_demo > bundle exec rake invoke:foo
n is 0
rake_demo >

File Tasks

Rake is based on the traditional make command. Make is all about creating files based on dependencies – it’s a helper for compiling things.

Naturally, rake gives you a handy tool for building files. Here’s a quick example of using that capability as part of a larger task.

desc "set up a fresh git clone for dev work"
task :setup => ['config/database.yml', 'setup:bundle_install', 'db:migrate'] do |t,args|
end

namespace :setup do

  desc "set up database.yml"
  file "config/database.yml" => ['config/database.yml.example'] do

    cp "config/database.yml.example", "config/database.yml"

  end

  desc "install gems"
  task :bundle_install do |t, args|

    system('gem install bundler')
    system('bundle install')

  end

end

Rake is smart enough to not overwrite database.yml if one already exists. If a file depends on other files, it will take their timestamps into account when deciding whether or not they need to be updated. In this case, if I update database.yml.example, bundle exec rake setup will overwite my database.yml, picking up any new settings that were added. That means you can use it to conditionally build assets, etc.

If you don’t declare any file dependencies, rake will only build the file if it doesn’t already exist. Sometimes this is safer – maybe I don’t want to risk nuking my database.yml and it’s better to leave it alone if one already exists.