The structure of a Rake project
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.
Using the import method to load other Rakefiles
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.
Running rake tasks from other tasks
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...