Rake is a tool for controlling builds. In this part of the Rake
tutorial, we see how to organize the Rake actions to apply to many
similar tasks.
In the RakeTutorialIntroduction, we talked about the basics of
specifying dependencies and associating actions to build the files.
We ended up with a nice Rakefile that built our simple C program, but
with some duplication in the build rules.
But First, Some Extra Rake Targets
But before we get into all that, lets add some convience targets to
our Rakefile. First of all, it would be nice to have a default target
that is invoked when we don’t give any explicit task names to rake.
The default target looks like this:
task :default => ["hello"]
Until now, the only kind of task we have seen in Rake are file
tasks. File tasks are knowledgable about time stamps on files. A
file task will not execute its action unless the file it represents
doesn’t exist, or is older than any of its prerequisites.
A non-file task (or just plain “task”) does not represent the creation
of a file. Since there is no timestamp for comparison, non-file tasks
always execute their actions (if they have any). Since the
default task does not represent a file named “default”, we use a
regular non-file task for this purpose. Non-file tasks just use the
task keyword (instead of the file keyword).
Here are a couple of other really useful tasks that I almost always
include in a Rakefile.
clean:
Remove temporary files created during the build process.
clobber:
Remove all files generated during the build process.
clean tidies up the directories and removes any files that generated
as part of the build process, but are not the final goal of the build
process. For example, the .o files used to link up the final
executable hello program would fall in this category. After the
executable program is built, the .o files are no longer
needed and will be removed by saying “rake clean”.
clobber is like clean, but even more aggressive. “rake
clobber” will remove all files that are not part of the original
package. It should return a project to the “just checked out of CVS”
state. So it removes the final executable program as well as the
files removed by clean.
In fact, these tasks are so common, Rake comes with a predefined
library that implements clean and clobber.
But every project is different, how do we specify which files are to
be cleaned and clobbered on a per project basis?
The answer is File lists.
File Lists to the Rescue
A file list is simply a list of file names. Since a lot of what Rake
does involves files and lists of those files, a file list has some
special features to make manipulating file names rather easy.
Suppose you want a list of all the C files in your project. You could
add this to your rake file:
SRC = FileList['*.c']
This will collect all the files ending in ”.c” in the top level
directory of your project. File lists understand glob patterns
(i.e. things like "*.c") and will find all the matching files.
By the way, no matter where you invoke it, rake always executes in
the directory where the Rakefile is found. This keeps your path names
consistent without depending on the current directory the user
interactive shell.
The clean and clobber tasks use file lists to manage the files to
remove. So if we want to clean up all the .o files in a
project we could try …
CLEAN = FileList['*.o']
(CLEAN is the file list associated with the clean task. I’ll let
you guess the name of the file list associated with clobber).
The Rakefile So Far …
With the addtion of a few extra tasks, our Rakefile now looks like
this. Notice the require ‘rake/clean’ line used to enable
the clean and clobber tasks.
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('hello')
task :default => ["hello"]
file 'main.o' => ["main.c", "greet.h"] do
sh "cc -c -o main.o main.c"
end
file 'greet.o' => ['greet.c'] do
sh "cc -c -o greet.o greet.c"
end
file "hello" => ["main.o", "greet.o"] do
sh "cc -o hello main.o greet.o"
end
Ok, now its time to address the redundant compile commands.
Dynamically Building Tasks
The command to compile the main.c and greet.c files
is identical, except for the name of the files involved. The
simpliest and most direct way to address the problem is to define the
compile task in a loop. Perhaps something like this …
SRC = FileList['*.c']
SRC.each do |fn|
obj = fn.sub(/\.[^.]*$/, '.o')
file obj do
sh "cc -c -o #{obj} #{fn}"
end
end
Just a couple things to note about the above code.
- The dependencies are not specified. This is a common where we
specify the dependents at one place and the actions in another. Rake
is smart enough to combine the dependencies with the actions.
- Although the task was named after the
.o (which is, after
all, what we want to generate), the file list is defined in terms of
the .c files. Why?
The simple reason is that file lists search for file names that
exist in the file system. We have no guarantee that the .o files
even exist at this point (indeed, the will not after invoking the
clean task). The .c are source and will always be there.
Rake Can Automatically Generate Tasks
Defining tasks in a loop is pretty cool, but is really not needed in a
number of simple cases. Rake can automatically generate file based
tasks according to some simple pattern matching rules.
For example, we can capture the above logic in a single rule … no
need to find all the source files and iterate through them.
rule '.o' => '.c' do |t|
sh "cc -c -o #{t.name} #{t.source}"
end
The above rule says that if you want to generate a file ending in
.o, then you if you have a file with the same base name, but
ending in .c, then you can generate the .o from the
.c.
t.name is the name of the task, and in file based tasks will
be the name of the file we are trying to generate. t.source
is the name of the source file, i.e. the one that matches the second
have of the rule pattern. t.source is only valid in the body
of a rule.
Rules are actually much more flexible than you are led to believe
here. But that’s an advanced topic that we will save for another day.
Final Rakefile
Here is our final resule. Notice how we use the SRC and OBJ file
lists to manage our lists of scource files and object files.
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('hello')
task :default => ["hello"]
SRC = FileList['*.c']
OBJ = SRC.ext('o')
rule '.o' => '.c' do |t|
sh "cc -c -o #{t.name} #{t.source}"
end
file "hello" => OBJ do
sh "cc -o hello #{OBJ}"
end
# File dependencies go here ...
file 'main.o' => ['main.c', 'greet.h']
file 'greet.o' => ['greet.c']
Up Next
In our next tutorial, we will look at using Rake to handle some tasks
other than compiling C code.
comments
|