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.
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.
.o File RuleHere 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.
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.
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
Just a couple notes about the 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
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.