Home Understanding Rake, the task runner for Ruby on Rails
Post
Cancel

Understanding Rake, the task runner for Ruby on Rails

TLDR

  • To use rake think in terms of tasks, not files, classes, etc. Tasks can have prerequisites(other tasks) and arguments, and they can be grouped in namespaces.
  • If the task’s work is complicated, don’t break it out into methods in the rake files. When any rake task is run, all the rake files are loaded and if multiple files define the same methods, they’ll get overwritten. Creating a service object and using it in the task keeps things manageable.
  • You can create new tasks using
    1
    
    bin/rails generate task <namespace> <task1_name> <task2_name>...
    
  • A very simple rake task looks like
    1
    2
    3
    4
    5
    6
    
    namespace :playground do
      desc "TODO"
      task dummy: :environment do
      end
    
    end
    

    The :environment prerequisite loads the entire rails env for you so you can use rails models, helpers, etc.

Also, note that this article is written in the context of Ruby on Rails which autoloads all the rake tasks and provides us with the environment task. For non-rails projects we may need to load files other than the one named Rakefile manually. Please check out the Rakefile documentation on how to do that.

What is rake?

Rake is the task runner for ruby. It is kinda like make and it has a few methods and capabilities which allow it to convert files and use it as a build system like make does, but in this article we’ll mainly focus on using rake as a general task runner.

How to write a task?

The easiest way to think about rake is in terms of tasks that need to be done. Let’s take an example to understand this. Suppose you want to generate a bunch of reports. The task would look like

1
2
3
task :generate_report do
  # code that generates the reports
end

It has come to your attention that you are working in rails and can’t use any rails methods, classes, etc. To fix that you add the environment prerequisite task

1
2
3
task generate_report: :environment do
  # code that generates the reports
end

You are a nice developer and want to add a description to your task. You can use desc for that, which has the added benefit of showing this description from the command line as well

1
2
3
4
desc "Generate the most important report of all time"
task generate_report: :environment do
  # code that generates the reports
end

You find out that you need to generate another report, now you either need to rename this task and remember to name all the tasks differently, or you can use namespace

1
2
3
4
5
6
namespace :yearly_earnings do
  desc "Generate the most important report of all time"
  task generate_report: :environment do
    # code that generates the reports
  end
end

Now, if you’re at all lazy like me, you’ll try to find a shorter way to write that. Luckily rails provides a generator for us to use

1
bin/rails generate task yearly_earnings generate_report

And this will create the same code that you did without you needing to worry about much.

Task with parameters

Suppose you want to provide arguments to the task.

1
2
3
4
5
6
7
namespace :yearly_earnings do
  desc "Generate the most important report of all time"
  task :generate_report, [:year] => :environment do |t, args|
    year = args.year
    # code that generates the reports
  end
end

And call it like

1
rake yearly_earnings:generate_report[2022]

Non-trivial task

Suppose your report generation code has now become very large and you want to move all those lines of code out of this task into somewhere a little more manageable.

First a note, Never create simple methods in any rake file. Rake will load all the rakefiles(and not just the one with the task in it). This means that there is a high chance of name collision and method overriding.

We have 2 options, which we can use together. We can split this large task into multiple steps and call those tasks as prerequisites(or dynamically) and you can also just write that code somewhere else and call it in the tasks.

What I usually do is if there are independent steps to the task, e.g. a setup part and an execution part, I split them into separate tasks and make the setup the prerequisite. Then I would create services which we can call in either or both tasks and keep the large complicated logic in that service. It has the added benefit that we can use the same service from within our normal rails code, if we ever desire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# app/services/yearly_earnings/report_generator.rb
module YearlyEarnings
  class ReportGenerator
    def initialize(year)
      @year = year
    end

    def generate
      #...
    end

    private

    attr_reader :year
  end
end

# lib/tasks/yearly_earnings.rake
namespace :yearly_earnings do
  desc "Refresh report views"
  task refresh_reports: :environment do
    #...
  end

  desc "Generate the most important report of all time"
  task :generate_report, [:year] => [:environment, :refresh_reports] do |t, args|
    YearlyEarnings::ReportGenerator.new(args.year).generate
  end
end

Summary

There you have it, the basics of using rake tasks effectively. Here’s a few references I’d highly recommend you check out.

This post is licensed under CC BY 4.0 by the author.