In this chapter, we will cover the installation of Rake, the definition of basic terms such as rake task and Rakefile, and how to use them for easy programming issues. The introduction will be given using straightforward examples to explain the terms as clearly as possible. You will see that Rake is a tool that is written in the Ruby programming language, and that's why any Ruby code can be written in a Rake application. Also, you have the choice of using any available Ruby library in a Rake project. This feature makes Rake the winner compared to many other build tools, which use their own limited languages. The chapter will serve as a base for introducing Rake's Domain Specific Language (DSL) and project file structuring.
In this chapter, we will cover the following topics:
Installing Rake
Introducing rake tasks
The command-line arguments
Using global Rakefiles to run tasks anywhere
Defining custom rake tasks
The structure of a Rake project
The code conventions of Rake
As Rake is a Ruby library, you should first install Ruby on the system if you don't have it installed already. The installation process is different for each operating system. However, we will see the installation example only for the Debian operating system family.
Just open the terminal and write the following installation command:
$ sudo apt-get install ruby
Note
If you have an operating system that doesn't contain the apt-get
utility and if you have problems with the Ruby installation, please refer to the official instructions at https://www.ruby-lang.org/en/installation. There are a lot of ways to install Ruby, so please choose your operating system from the list on this page and select your desired installation method.
Rake is included in the Ruby core as Ruby 1.9, so you don't have to install it as a separate gem. However, if you still use Ruby 1.8 or an older version, you will have to install Rake as a gem. Use the following command to install the gem:
$ gem install rake
Note
The Ruby release cycle is slower than that of Rake and sometimes, you need to install it as a gem to work around some special issues. So you can still install Rake as a gem and in some cases, this is a requirement even for Ruby Version 1.9 and higher.
To check if you have installed it correctly, open your terminal and type the following command:
$ rake --version
This should return the installed Rake version.
The next sign that Rake is installed and is working correctly is an error that you see after typing the rake
command in the terminal:
$ mkdir ~/test-rake $ cd ~/test-rake $ rake rake aborted! No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb) (See full trace by running task with --trace)
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
From the previous error message, it's clear that first you need to have Rakefile
. As you can see, there are four variants of its name: rakefile
, Rakefile
, rakefile.rb
, and Rakefile.rb
. The most popularly used variant is Rakefile
. Rails also uses it. However, you can choose any variant for your project. There is no convention that prohibits the user from using any of the four suggested variants.
Rakefile
is a file that is required for any Rake-based project. Apart from the fact that its content usually contains DSL, it's also a general Ruby file. Also, you can write any Ruby code in it. Perform the following steps to get started:
Let's create a
Rakefile
in the current folder, which will just sayHello Rake
, using the following commands:$ echo "puts 'Hello Rake'" > Rakefile $ cat Rakefile puts 'Hello Rake'
Here, the first line creates a
Rakefile
with the content,puts 'Hello Rake'
, and the second line just shows us its content to make sure that we've done everything correctly.Now, run
rake
as we tried it before, using the following command:$ rake Hello Rake rake aborted! Don't know how to build task 'default' (See full trace by running task with --trace)
The message has changed and it says
Hello Rake
. Then, it gets aborted because of another error message. At this moment, we have made the first step in learning Rake.Now, we have to define a default rake task that will be executed when you try to start Rake without any arguments. To do so, open your editor and change the created Rakefile with the following content:
task :default do puts 'Hello Rake' end
Now, run
rake
again:$ rake Hello, Rake
The output that says Hello, Rake
demonstrates that the task works correctly.
The most commonly used rake command-line argument is -T
. It shows us a list of available rake tasks that you have already defined.
We have defined the default
rake task, and if we try to show the list of all rake tasks, it should be there. However, take a look at what happens in real life using the following command:
$ rake -T
The list is empty. Why? The answer lies within Rake. Run the rake
command with the -h
option to get the whole list of arguments. Pay attention to the description of the -T
option, as shown in the following command-line output:
-T, --tasks [PATTERN] Display the tasks (matching optional PATTERN) with descriptions, then exit.
Note
You can get more information on Rake in the repository at the following GitHub link at https://github.com/jimweirich/rake.
The word description
is the cornerstone here. It's a new term that we should know. Additionally, there is also an optional description to name a rake task. However, it's recommended that you define it because you won't see the list of all the defined rake tasks that we've already seen. It will be inconvenient for you to read your Rakefile
every time you try to run some rake task. Just accept it as a rule: always leave a description for the defined rake tasks.
Now, add a description to your rake tasks with the desc
method call, as shown in the following lines of code:
desc "Says 'Hello, Rake'" task :default do puts 'Hello, Rake.' end
As you see, it's rather easy. Run the rake -T
command again and you will see an output as shown:
$ rake -T
rake default # Says 'Hello, Rake'
By default, Rake is looking for tasks that are placed in the current folder (that is, the folder where you run the rake
command) in the Rakefile
. Assume that we need to have a rake task that can be executed in any folder. For example, say that we have a rake task that cleans the Linux files ending with ~
. The following Rakefile defines the rake task to remove them:
desc 'Cleans backup files *~' task :default do files = Dir['*~'] rm(files) end
Here, we get temporary files in the current folder and remove them with the rm
method. This method is defined in the FileUtils
module, which is included in Rake as well. So, we will discuss it in the next chapters.
When you are in the current folder, check this rake task using the Rakefile
:
$ rake
rm
Here, we see that the rm
command was executed and Rake explicitly said this in the second line. If you don't want to see this verbose message, pass the -q
option to the command.
However, what would happen if we go to the folder one level up? When you try to type the rake
command, you will have an error message that says that no Rakefile
was found. We can get rid of this problem by passing the -f
option with the path to the Rakefile
as shown in the following lines of code:
$ rake -f ~/my-rake-task/Rakefile
rm
This works well, but you may agree with me that it's too unhandy. Rake produces one useful feature to make this situation work the way we want. It's based on the method of finding the Rakefile
. First, Rake tries to find the Rakefile
in the current folder. If Rake can't find it there, the search continues till it reaches the user's home folder. If there is no Rakefile
there, it finally raises an exception saying that the Rakefile
was not found. We can apply this behavior to our issue. Just move the Rakefile
to your home folder and mark the rake tasks defined in it as available for the current user everywhere. Open the terminal and type the following commands to achieve the expected output:
$ mv ~/my-rake-task/Rakefile ~/ $ cd ~/my-rake-task $ rake (in /Users/andrey) rm
As you can see, this works as expected, and there is one more new line, as follows:
(in /Users/andrey)
This command says that the Rakefile
was found at the user home folder. You can disable showing this information by passing the -s
option.
There is another way to define global Rakefiles
. You have an option to define them in the ~/.rake
folder, and they can be executed from any folder with the help of the -g
option. The following is the Rake output of the help
command:
-g, --system Using system wide (global) rakefiles (usually '~/.rake/*.rake').
So, let's define a global Rakefile
in this way and check it in action. The following is an example of how to do it through the terminal:
$ mkdir ~/.rake
$ touch ~/.rake/hello.rake
$ echo -e 'task "hello" do\n puts "Hello, Rake"\nend' > ~/.rake/hello.rake
$ rake -g hello
Hello, Rake
So far, we defined only one task named default
. Rake allows you to define your custom tasks with any name. The common form of the custom rake task definition is passing a task name to the task
method and a block as a second argument. The block defines some action and usually contains some Ruby code. The rake task might have an optional description, which is defined with the desc
method. This method accepts a text for the description of the task. The following code snippet is an example of defining a custom rake task:
desc 'Restart web server' task :restart do touch '~/restart.txt' end
This is an example of a possible rake task to restart Passenger (this is a module for the Nginx web server, which works with the Rails applications). We name the task restart
. To run this task, just pass its name as the second argument to the rake
command as shown in the following line of code:
$ rake restart
If you have a lot of tasks, it's handy to enclose them to the named spaces, as shown in the following code snippet:
namespace :server do desc 'Restart web server' task :restart do touch './tmp/restart.txt' end end
You can also run the task in the command line using the following command:
$ rake server:restart
Actually, the task method accepts more arguments. However, they are related to other topics that are explained further along in the book in Chapter 2, Working with Files, and Chapter 3, Working with Rules.
Sometimes, you have to write tasks that depend on other tasks. For example, when I'm going to seed data in my project, I want to clean all the persisting data that can break my code. In this case, we can say that our seed data task depends on the clean seed data task. The following code example shows us a Rakefile
for this case:
task :clean do puts 'Cleaning data...' end task :seed => :clean do puts 'Seeding data...' end
The preceding code executes the clean do
task before running the seed
task. The result of the execution of this task is shown below the following line of code:
$ rake seed Cleaning data... Seeding data...
It works as expected.
If you have to run the task from another namespace, pass its whole name as a string, as shown in the following code snippet:
namespace :db do task :clean do puts 'Cleaning data...' end end task :seed => 'db:clean' do puts 'Seeding data...' end
However, if the dependent task is in the same namespace, you don't have to pass it as a string, as shown in the following code snippet:
namespace :db do task :clean do puts 'Cleaning data...' end task :seed => :clean do puts 'Seeding data...' end end
Earlier in this chapter, we defined the default
rake task. To be honest, we did it just to understand what happens on running rake
without arguments and to introduce Rake in a few steps giving as less information as possible in an interactive way. However, in the practical word, nobody defines the default
rake task with an action. Setting dependencies is a convenient feature. It allows the default
task to refer to some other task as many times as you want without regression. For example, today, the default
task runs a doc:generate
task but tomorrow, we decide to run a test:run
task instead. In such a situation, we can just change the prerequisite and that's it. So, always define your default
rake task with the following template:
task :default => :some_task
It's also possible to pass many prerequisites for a task. The following line of code is an example of how to do this:
task :task1 => [:task2, :task3]
A task might be specified more than once. Each specification adds its dependencies and implementation to the existing definition. This allows one part of a Rakefile
to specify the actions and a different Rakefile
(perhaps a separately generated one) to specify the dependencies.
For example, take a look a Rakefile
that contains the following code:
task :name => [:prereq1, :prereq2] do # action end
It can be rewritten as the following code:
task :name task :name => [:prereq1] task :name => [:prereq2] task :name do # action end
Assume that you have a rake task that sets the title for our blog and you want to pass it from the command line; this should be optional. If you don't pass the title of the blog, the default title should be set.
We have two solutions to solve this problem. The first solution is to pass parameters through the environment variable that is passed into the ENV
variable in Ruby code (ENV
is a hash-like accessor for environment variables, and it is available in any Ruby program). The second solution is using the built-in Rake syntax—you just pass variables to each task through square braces. The first use case doesn't allow you to pass variables for each task in isolation. The variables are shared among all the tasks in the Rakefile
. So, the preferable style is the second choice. However, we are shown two alternatives, which will be discussed in the next sections.
The first alternative is a case where we pass variables using environment variables. The following code represents a Rakefile
:
task :set_title do title = ENV['TITLE'] || 'Blog' puts "Setting the title: #{title}" end
The following code is a usage example:
$ rake set_title TITLE='My Blog' Setting the title: My Blog $ rake set_title # default title should be set in this case Setting the title: Blog
In the preceding example, the ENV
variable approach can be used without any caution. The following code snippet represents the collision in sharing the variable between the tasks. Check the following Rakefile
:
task :task1 do puts "#{ENV['TITLE']} in task1" end task :task2 do puts "#{ENV['TITLE']} in task2" end
The following code is an example of usage:
$ rake task1 task2 TITLE='test' test in task1 test in task2
You can see that the TITLE
variable is accessible in both the tasks and is the same. Sometimes, you don't want to get this behavior and you need to pass the variables to each task individually.
A variable declared within a rake command will not persist in the environment. The following terminal output will confirm this statement:
$ export TITLE='Default Title' $ rake set_title TITLE='My Blog' Setting the title: My Blog $ echo $TITLE Default Title
The second variant has a built-in Rake feature. The following is the Rakefile
code:
task :set_title, [:title] do |t, args| args.with_defaults(:title => 'Blog') puts "Setting title: #{args.title}" end
Note
Please ignore the t
variable at this moment; you will see what it means and what its usages are in Chapter 2, Working with Files.
Look at args
, which is a hash-like object of the Rake::TasksArguments
class. It has a useful method that is used here, named with_defaults
, to merge the given arguments from the command line and the default values. If you don't pass the variables through the command line, the default variable for the title will be set.
The following code depicts how it may be used:
$ rake "set_title[My Blog]" Setting title: My Blog $ rake set_title Setting title: Blog
Here, to pass the argument as a string with space (My Blog
), I have enclosed the rake task with the argument within quotes. It's not the only case where I have to enclose the task name within double quotes. There are some terminals that don't understand the squared parentheses in the command line and should escape them with \
at the end of the code line of the rake task that is enclosed within the double quotes.
You are also able to pass multiple arguments to the rake task by separating them with a comma, as shown in the following line of command:
$ rake "name[Andrey,Koleshko]"
The task declaration for the preceding task is as follows:
task :name, [:first_name, :last_name] do |t, args| puts "First name is #{args.first_name}" puts "Last name is #{args.last_name}" end
Finally, you are able to pass variable-length parameters to the task with a comma, as we did in the previous example. In this case, you may use the extras
method on the given args
variable:
task :email, [:message] do |t, args| puts "Message: #{args.message}" puts "Recipients: #{args.extras}" puts "All variables: #{args.to_a}" end
In the following example, the first argument will be assigned to the message
variable on the args
variable and the remaining arguments will go to the extras
method. If you want to have an array that passes all the variables including the one associated with the message
variable, you can call the to_a
method on the args
variable, as demonstrated in the preceding Rakefile
.
$ rake "email[Hello Rake, ka8725@gmail.com, test@example.com]" Message: Hello Rake Recipients: ["ka8725@gmail.com", "test@example.com"] All variables: ["Hello Rake", "ka8725@gmail.com", "test@example.com"]
Apart from the necessary Rakefile, there is a technique that allows us to form a good structure of a Rake project. Say that you have a very complicated Rake project with a lot of tasks. It's a good idea to split them into separate files and include them in the Rakefile. Fortunately, Rake already has this feature and you shouldn't care about implementing this feature from the scratch. Just place your separated files to the rakelib
folder (it can be changed to custom by passing the -R
option), give these files a .rake
extension, and that's it. You don't have to do anything additional. Files with the *.rake
extensions are included in the Rakefile
automatically for you. Nonstandard extension such as .rake
for the files should not scare you. These are the usual Ruby files. There you can write any Ruby code, define their rake tasks, include the related libraries, and so on. So, take this feature as a good thing to refactor a Rake project.
To approve the things said in this section, please open the terminal and check the following example:
$ mkdir rakelib
$ cat > rakelib/clean.rake
task :clean do
puts 'Cleaning...'
end
^D
$ cat > Rakefile
task :default => :clean
^D
$ rake
Cleaning...
In this example, ^D
is a keyboard shortcut: Ctrl + D. The cat
utility writes the standard output to the files here.
It's possible to include other Ruby files or Rakefiles
to describe a current Rakefile
. It can be achieved by a standard require
Ruby statement. However, what do we do when the including files depend on some method or variable defined in the describing Rakefile
? To demonstrate the situation, create the following two files in a folder:
rakefile
dep.rb
The rakefile
has some tasks definition, a method definition, and a require
statement, as shown in the following code snippet:
require './dep.rb' def method_from_rakefile puts 'it is a rakefile method' end task :a do puts 'task a' end task :b do puts 'task b' end
The dep.rb
file just defines a new task that has both the prerequisites tasks, a
and b
. Also, it calls the defined method, method_from_rakefile()
, for some reason, as shown in the following code snippet:
method_from_rakefile() task :c => [:a, :b] do puts 'task c' end
Trying to run a rake task defined in Rakefile
will cause an exception that says that there is no defined method_from_rakefile
while the dep.rb
file is loading:
$ rake c rake aborted! undefined method `method_from_rakefile' for main:Object ~/dep.rb:1:in `<top (required)>' ~/rakefile:1:in `<top (required)>' (See full trace by running task with --trace)
The exception occurs when the dep.rb
file is required by the Rakefile
. The problem here is caused because the required file loaded even before the Rakefile
could load. One of the possible solutions here is just to move the require
statement to the last line of the Rakefile
. As a result, the method and tasks required for the dep.rb
file will be defined at the time of the dep.rb
file being included in the Rakefile
. To be honest, the solution seems like a hack; this is the Rake way.
Fortunately, Rake provides us with a tool to resolve this issue—the import
method. It does what we really want here; the import
statement may be used in any line of the Rakefile
, and this doesn't apply to the loading process at all. The imported files will be loaded after the whole Rakefile
is loaded. Its usage looks similar to the require
statement and is shown in the following line of code:
import(filenames)
Here, you are able to pass more than one file.
There is one more feature of the import
method. If you pass the filenames to the import
task, they are evaluated first, and this allows us to generate the dependent files on the fly. Look at the following Rakefile
:
task 'dep.rb' do sh %Q{echo "puts 'Hello, from the dep.rb'" > dep.rb} end task :hello => 'dep.rb' import 'dep.rb'
This example generates the dep.rb
file on the file
due to the import 'dep.rb'
call that evaluates the 'dep.rb'
task. The result of the hello
task execution is shown as follows:
$ rake hello echo "puts 'Hello, from the dep.rb'" > dep.rb Hello, from the dep.rb
It is a really helpful feature that can not only help you in writing the Rake project, but also in a simple Ruby project.
Sometimes, you will have to execute some defined task from your task manually. For this purpose, you have two methods of the Rake::Task
class: execute
and invoke
. The difference between the two methods is that execute
doesn't call dependent tasks, but the invoke
method does. Both of these methods also accept arguments that can be passed to the tasks if you need them. Their usage is the same and is shown as follows. The following is the first code:
Rake::Task['hello'].invoke
The following is the second code:
Rake::Task['hello'].execute
With the Rake::Task['hello']
code, we got the hello
rake task. It returns an instance of the Rake::Task
class and then, we are able to run any method on this. In the preceding examples, we called invoke
and execute
.
To get the namespaced task by name, like in the previous example, use a syntax or similar to the following line of code:
Rake::Task['my:hello']
One more difference between these methods is that the invoke
method can't be executed twice without some trick. If you need to run the task more than once with the invoke
method, use the reenable
method as shown in the following code snippet:
Rake::Task['hello'].invoke Rake::Task['hello'].reenable Rake::Task['hello'].invoke
These capabilities can be used when you need to run some other rake task after a current task has been executed. Look at the following example that depicts how to use it in task actions. It demonstrates the usage of the invoke
and reenable
methods:
task :clean do puts 'cleaning data...' end task :process do puts 'processing some data...' Rake::Task['clean'].invoke end task :process_with_double_clean do puts 'processing some data...' Rake::Task['clean'].invoke Rake::Task['clean'].invoke end task :process_with_double_clean_and_reenable do puts 'processing some data...' Rake::Task['clean'].invoke Rake::Task['clean'].reenable Rake::Task['clean'].invoke end
Try to paste this code in a Rakefile
and run the process
, process_with_double_clean
, and process_with_double_clean_and_reenable
tasks to find the difference between them. The following code is the output of the executions:
$ rake -f rakefile22 process processing some data... cleaning data... $ rake -f rakefile22 process_with_double_clean processing some data... cleaning data... $ rake -f rakefile22 process_with_double_clean_and_reenable processing some data... cleaning data... cleaning data...
The words namespace
, desc
, task
, touch
, and so on in the Rakefile
are general methods and, of course, you are able to pass parentheses when you pass the parameters there, as shown in the following code snippet:
namespace(:server) do desc('Restart web server') task(:restart) do touch('./tmp/restart.txt') end end
However, the code looks quite ugly now, so it's recommended that you avoid using styles such as the one used here. Rake has its own DSL, and if you follow it, the code will be more readable.
The namespace
and task
methods are the basic methods that accept blocks that make the Rake code very expressive. For the task
method, the block in the task definitions is optional, similar to what we saw in the Task dependencies – prerequisites section.
The blocks can be specified with either a do/end
pair or with curly braces in Ruby. To specify a Rakefile
, it's strongly recommended that you define rake tasks only with do/end
. Because the Rakefile
idiom tends to leave off parentheses on the tasks definitions, unusual ambiguities can arise when using curly braces. Take a look at the following proposed Rakefile
:
def dependent_tasks [:task2, :task3] end task :task2 do puts 'In task2...' end task :task3 do puts 'In task3...' end task :task1 => dependent_tasks { puts 'In task1...' # We are expecting this code to be run but it's not }
The following is the result of the execution of task1
:
$ rake task1 In task2... In task3...
The defined action in task1
is not evaluated. It leads to unexpected behavior. Because curly braces have a higher precedence than do/end
, the block is associated with the dependent_tasks
method rather than the task
method.
A variant of passing the block after the dependent task name is not valid Ruby code at all, as shown:
require 'rake' task :task1 => :task2 { }
It might seem strange but unfortunately, this code doesn't work and gives a syntax error as shown:
# => SyntaxError: syntax error, unexpected '{', expecting end-of-input
The conclusion of this is that if you just follow the Rakefile
convention, you won't have problems with Rake's unexpected behavior.
Finally, the last tip for Rakefiles
description: don't use the new style of a hash definition in the task prerequisites (in other words, don't describe tasks dependencies like this: task1: :task2
). Often, only one prerequisite, defined at the first instance, transforms to the list of prerequisites and then you will have to translate the hash definition to the old style (in other words, the task1: :task2
code transforms to :task1 => [:task2, task3]
). Usually, all the task definitions contain the hash rocket instead of the colon notation. The conclusion here is simple: use the old style of the creation of Ruby hashes in the rake tasks definitions.
In this chapter, you learned what Rake is, what you have to do to start using Rake, how to use the rake
command-line tool, how to write and run rake tasks, how to set their dependencies, and how to structure the code. There was some advice about Rake's DSL and an explanation on why should you follow it. This chapter doesn't demonstrate any real examples of how to use Rake because this knowledge is not enough to work with the files, but please be patient because you will see them in the upcoming chapters.
The next chapter will explain the basics to work with files using Rake. Also, you will see the first real example of put the knowledge of both these chapters to practice.