Mark Probert on the Ruby-Talk mailing lists asks: “I am not sure
how to create a set of Rake rules to do the following. Can anyone
prove assistance?”
I had planned for the next Rake tutorial to go into using prepackaged
task libraries, but Mark’s question highlights an interesting problem
(and resolution) with rules. I posted an answer to Mark on the list,
but why waste a perfectly good explaination when I can recycle it
here.
The Problem
Mark has two separate source directories (he calls them src_a and
src_b, but I suspect they are more creatively named in real life.
Both directories contain .c files. However, the kicker is that all
the object files are to be placed in a single directory (named obj)
no matter which source directory contained the original C code.
The rule we introduced in the last tutorial isn’t powerful enough to
move the .o file into the obj directory. We need to tweek it just
a bit.
The .o File Rule
Here is the rule in question.
rule '.o' => '.c' do |t|
sh "cc -c -o #{t.name} #{t.source}"
end
To recap, the rule specifies how to create a .o file from a
similarly named .c file. But as noted above, the .c are in a
different location. We can fix this by giving the rule a general
purpose function that transforms the object file name into the correct
source file name.
Finding the Source File.
But how do we find the source file? Assuming we have a constant named
SRC that contains a list of all our source files, this simple find
command will do the trick (assume objfile is the name of the object
file):
SRC.find { |s| File.basename(s, '.c') == File.basename(objfile, '.o') }
Wrapping this in a method (named find_source) gives us a nice way to
find the source file.
Tweeking the Rule
We can now write the rule like this…
rule '.o' => lambda { |objfile| find_source(objfile) } do |t|
sh "cc -c -o #{t.name} #{t.source}"
end
The Whole Rakefile
Just a couple notes about the Rakefile
- Note that we invoke the OBJDIR task directly in the rule. Because
it is a rule, there is no opportunity to list OBJDIR as an explicit
dependency. By invoking it directly inside the rule, we will build
that directory if it is needed (but only if it is needed).
- If searching the SRC list has performance problems (because SRC is
very long), then an alternative is to create a mapping of object names
to source names at the top of the file. Then finding the source name
is a simple hash lookup.
Rakefile
require 'rake/clean'
PROG = "foo"
LIBNAME = PROG
LIBFILE = "lib#{LIBNAME}.a"
SRC = FileList['**/*.c']
OBJDIR = 'obj'
OBJ = SRC.collect { |fn| File.join(OBJDIR, File.basename(fn).ext('o')) }
CLEAN.include(OBJ, OBJDIR, LIBFILE)
CLOBBER.include(PROG)
task :default => [:build, :run]
task :build => [PROG]
task :run => [PROG] do
sh "./#{PROG}"
end
file PROG => [LIBFILE] do
sh "cc -o #{PROG} -L . -l#{LIBNAME}"
end
file LIBFILE => OBJ do
sh "ar cr #{LIBFILE} #{OBJ}"
sh "ranlib #{LIBFILE}"
end
directory OBJDIR
rule '.o' => lambda{ |objfile| find_source(objfile) } do |t|
Task[OBJDIR].invoke
sh "cc -c -o #{t.name} #{t.source}"
end
def find_source(objfile)
base = File.basename(objfile, '.o')
SRC.find { |s| File.basename(s, '.c') == base }
end
Alternatives
On possible alternative is to replace the rule with a loop that
explicitly creates tasks to compile each .c file. It might look
something like this:
SRC.each do |srcfile|
objfile = File.join(OBJDIR, File.basename(srcfile).ext('o'))
file objfile => [srcfile, OBJDIR] do
sh "cc -c -o #{objfile} #{srcfile}"
end
end
What I like about this solution is the ability to put the OBJDIR
dependency directly in the task definition.
|