-
- Received via EMail:
-
- I have just started using the excellent Rake tool (thanks, Jim!) and I
am at a bit of a loss on how to proceed. I am attempting to create unit
test for some C++ code I am creating, […]
Several people recently have made similar comments, they really like
rake, but have had trouble getting started. Although the Rake
documentation is fairly complete, it really does assume you are familiar
with other build tools such as ant and make. It is not
really material for the newbie.
To adderess this lack, I’m going to post several Rake tutorial
articles that will take you through some of the basics. Eventually,
I’ll organize the articles into a document somewhere.
Here’s the first one!
The Problem
We will start with a very simple build problem, the type of problem that
make (and now rake) were desiged to deal with.
Suppose I am a C programmer and I have a simple C program consisting of the
following files.
main.c
#include "greet.h"
int main() {
greet ("World");
return 0;
}
greet.h
extern void greet(const char * who);
greet.c
#include <stdio.h>
void greet (const char * who) {
printf ("Hello, %s\n", who);
}
(Yes, it really is the old standard "Hello, World" program. I did
say we were starting with the basics!)
To compile and run this collection of files, a simple shell script like the
following is adequate.
build.sh
cc -c -o main.o main.c
cc -c -o greet.o greet.c
cc -o hello main.o greet.o
-
- For those not familiar with compiling C code, the cc command
is the C compiler. It generates an output file (specified by the
-o flag) from the source files listed on the command line.
Running it gives us the following results …
$ build.sh
$ ./hello
Hello, World
Building C Programs
Compiling C programs is really a two step process. First you compile all
the source code file into object files. Then you take all the object files
and link them together to make the executable.
The following figure illustrates the progression from source files to
object files to executable program.
Our program is so small that there is little benefit in doing more than the
three line build script above. However, as projects grow, there are more
and more source files and object files to manage. Recompiling everything
for a simple one line change in a single source file gets old quickly. It
is much more efficient to just recompile the few files that change and then
relink.
But how do we know what to recompile? Keeping track of that would be quite
error prone if we tried to do that by hand. Here is where Rake become
useful.
File Dependencies
First, lets take a look at when files need to be recompiled. Consider the
main.o. Obviously if the main.c file changes, then we
need to rebuild main.o. But are the other files that can trigger a
recompile of main.o?
Actually, yes. Looking at the source of main.c, we see that it
includes the header file greet.h. That means any changes in
greet.h could possibly effect the main.o file as well.
We say that main.o has a dependency on the files main.c
and greet.h. We can capture this dependency in Rake with the
following line:
file "main.o" => ["main.c", "greet.h"]
The rake dependency declaration is just regular Ruby code. We take
advantage of the fact that we can construct hash arguments on the fly, and
that Ruby doesn’t require parenthesis around the method arguement to
create a file task declaration that reads very naturally to the humans
reading the rake file. But its still just Ruby code.
Likewise, we can declare the dependencies for creating the
"greet.o" file as well.
file "greet.o" => ["greet.c"]
greet.c does include stdio.h, but since that is a system
header file and not subject to change (often), we can leave it out of the
dependency list.
Finally we can declare the dependencies for the executable program
hello. It just depends on the two object files.
file "hello" => ["main.o", "greet.o"]
Notice that we only have to declare the direct dependencies of
hello. Yes, hello depends on main.o which in
turn depends on main.c. But the .c files are not directly
used in building hello, so they can safely be omitted from the
list.
Building the Files
We have carefully specified how the files are related. Now we need to say
what Rake would have to do to build the files when needed.
This part is pretty simple. The three line build script that we started
with contains all the commands needed to build the program. We just need to
put those actions with the right set of dependencies. Use a Ruby do
/ end block to capture actions …
The result looks like this:
Rakefile
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
Trying it out
So, let’s see if it works!
$ rake hello
(in /home/jim/pgm/rake/intro)
cc -c -o main.o main.c
cc -c -o greet.o greet.c
cc -o hello main.o greet.o
The command line rake hello instructs rake to look through its
list of tasks and find one called "hello". It then checks
hello’s dependencies and builds them if required. Finally, when
everything is ready it builds hello by executing the C compiler
command.
Rake dutifully reports what it is doing as it goes along. We can see that
each compiler invocation is done in the correct order, building the main
program at the end. So, does the program work? Let’s find out.
$ ./hello
Hello, World
Success!
But what happens when we change a file. Lets change the greet function in
greet.c to print "Hi" instead of hello.
$ xemacs greet.c
$ rake hello
(in /home/jim/pgm/rake/intro)
cc -c -o greet.o greet.c
cc -o hello main.o greet.o
$
$ ./hello
Hi, World
Notice that it recompiles greet.c making a new greet.o. And then it needs
to relink hello with the new greet.o. Then it is done. There is no need to
recompile main.c since it never changed.
What do you think will happend if we run Rake again?
$ rake hello
(in /home/jim/pgm/rake/intro)
$
That’s right … nothing. Everything is up to date with its
dependencies, so there is no work for Rake to do.
Ok, sure. Rake is a bit of overkill for only two source files and a header.
But imagine a large project with hundreds of files and dependencies. All of
a sudden, a tool like Rake becomes very attractive.
Summary
What have we learned? Building a Rakefile involves identifying dependencies
and the actions required to create the target files. Then declaring the
dependencies and actions are as simple as writing them down in standard
Ruby code. Rake then handles the details of building
What’s Up Next
We notice that even our small example has a bit of duplication in it. We
have specify how to compile both C file separately, even though the only
difference is the files that are used. The next installment will look at
fixing that problem as well as introduce non-file based tasks, rules and
file lists.
Until then … Code Red, Code Ruby.
comments
|