A guide to switching from Make to Rake.
If you're just after a Rakefile for building C code, use my Rake Template.
1. Syntax
1.1 Variables
Immediate evaluation assignment.
PROJ := MyDoc
DOCS := $(PROJ).html $(PROJ).odt
PROJ = "MyDoc"
DOCS = [ "#{PROJ}.html", "#{PROJ}.odt" ]
Lazy evaluation assignment.
TODO
1.2 Targets
images: $(IMAGES)
desc "Build images"
task :images => IMAGES
1.3 Rules
Create .html files by combining .rst and .svg:
%.html : %.rst %.svg
rst2html.py $< $@
rule '.html' => ['.rst','.svg'] do |t|
sh "rst2html.py #{t.source} #{t.name}"
end
1.4 Globs and Patsubst
SRC := $(wildcard *.c)
OBJ := $(patsubst %.c,%.o,$(SRC))
SRC = FileList['*.c']
OBJ = SRC.ext('o')
1.5 Output Paths
Generate output files with names based on the input files.
Support output to a separate build directory.
Using pathmap
To create an OBJ list with an output path in Rake we can use the pathmap syntax
%X and substitute known paths %{obj,src}.
Pathmap is limited though.
Substituting a variable like OBJDIR in the pathmap syntax for FileLists? doesn't work.
It should work with the '*' symbol allowing one to specify a block to do the
required substitution, but FileList? breaks this, substituting a literal '*'.
The mapping of SRC to OBJ needs to be reversed in the '.o' generation rule.
Without using a proc
this example needs to repeat the output path in
multiple places. (Not very DRY.)
Note also that the substitution depends on SRC paths starting with './'.
DRYer?, more flexible mapping is achieved with a map
and a proc
. See below.
OBJDIR := obj
OBJS := ${OBJDIR}/foo.o
$(OBJDIR)/%.o : src/%.c
@mkdir -p $(dir $@)
$(CC) -c -o $@ $<
OBJDIR = "obj"
OBJS = C_SRCS.pathmap('%{.,path/to/obj/dir}X.o')
rule '.o' => ['%{path/to/obj/dir,.}X.c', '%d'] do |t|
mkdir_p t.name.pathmap('%d')
sh "#{CC} #{t.source} -o #{t.name} #{STDLIB_CFLAGS} #{STDLIB_LFLAGS}"
end
Using map and proc
Use map
and proc
to map source trees to output trees in a flexible way.
# Map sources to objects
OBJS = C_SRCS.map { |f|
f.sub(/^#{SRC_DIR}/, OBJDIR).ext('.o')
}
# Map objects to sources
rule '.o' => [
proc { |tn| tn.sub(/#{OBJ_DIR}/, SRC_DIR).ext('.c') },
'%d'
] do |t|
sh %Q{#{CC} -c "#{t.source}" -o "#{t.name}"}
end
An example showing how to use procs to map outputs to a build directory.
RakeExampleBuildDirectory
Creating directories
In GNUMake?, use a silent mkdir -p
and the dir
built-in.
$(OBJDIR)/%.o : src/%.c
@mkdir -p $(dir $@)
$(CC) -c -o $@ $<
In Rake to create output directories to mimic the source tree layout we could
use the same method as Make with the mkdir_p
method but this still calls
mkdir every time the rule is applied.
A more efficient way is to:
- Create a set of
directory
rules based on the OBJS paths
- Make the generation rule depend on both the object file and its directory (using pathmap syntax).
# declare directory tasks for each object file path
OBJS.each do |d|
directory d.pathmap('%d')
end
# Depend on the source file and the output directory
rule '.o' => ['%{path/to/obj/dir,.}X.c', '%d'] do |t|
sh "#{CC} #{C_FLAGS} #{C_DEFINE} #{C_INCLUDE} #{t.source} -o#{t.name}"
end
The source file must come first for 't.source' to work correctly.
Here the pathmap syntax '%d' does the work of extracting the path from the filepath.
1.6 Include Paths and Define lists
Mapping an array of include paths to a string passed to the compiler.
INCLUDE_DIRS = [
".",
"inc",
"/usr/include/blah",
]
# Map to the compiler's command line format
C_INCLUDE = INCLUDE_DIRS.map {|s| "-I "+s }.join(" ")
1.7 Spaces in Filenames
The Easy Way with Shellwords
Require shellwords
require 'shellwords'
rule '.out' => [*C_OBJS, '%d'] do |t|
sh %Q{#{CC} -o #{t.name.shellescape} #{C_OBJS.shelljoin}}
end
The Manual Way with Monkey-patching
If you're not using Bash as your shell you may have to do some manual substitution.
In rake, we insert ("monkey patch" or "duck punch") a method in FileList? and Array to convert each to a list of quoted strings.
# Extension to quote lists (of object files, for example)
class Array
def to_quoted_s(q='"')
"#{q}#{self.join("#{q} #{q}")}#{q}"
end
end
class FileList
def to_quoted_s(q='"')
self.to_a.to_quoted_s(q)
end
end
Then a rule looks like:
rule '.out' => [*C_OBJS, '%d'] do |t|
sh %Q{#{CC} #{LD_FLAGS} #{C_FLAGS} #{C_DEFINE} #{C_INCLUDE} -o "#{t.name}" #{C_OBJS.to_quoted_s}}
end
This could be extended (with a map-join or inject) to escape cases where the quote character appears in the filenames themselves.
1.8 OS Detection
Make
UNAME := $(shell uname)
ifeq ($(UNAME), Linux)
# do something Linux-y
endif
ifeq ($(UNAME), Solaris)
# do something Solaris-y
endif
ifdef SystemRoot
RM = del /Q
FixPath = $(subst /,\,$1)
else
ifeq ($(shell uname), Linux)
RM = rm -f
FixPath = $1
endif
endif
Rake
def os
@os ||= (
host_os = RbConfig::CONFIG['host_os']
case host_os
when /mswin|bccwin|wince|emc/
:windows
when /cygwin/
:cygwin
when /mingw|msys/
:mingw
when /darwin|mac os/
:macosx
when /linux/
:linux
when /solaris|bsd/
:unix
else
:unknown
end
)
end
1.9 Invocation and Arguments
Make pulls all environment variables into the same namespace as Make variables.
Rake accesses arguments as if they were environment variables.
Make
CONFIG ?= Release
Invocation:
$ make CONFIG=Debug
Rake
CONFIG = ENV["CONFIG"] || "Release"
$ rake CONFIG=Debug
1.10 PHONY
Make
.PHONY: clean
rake
As of 0.9.3 rake has phony built in:
require 'rake/phony'
task :clean => :phony
But it can be simulated with:
def (task(:phony)).timestamp
Time.at 0
end
task :clean => :phony
2. Examples
2.1 Single build tree
RakeExampleBuildDirectory
2.2 Make and Rake
An example project which takes a ReStructuredText? document and graphviz/dot files and generates a single html or odt document.
The html document has SVG images, the odt document has png images.
#
# GNUmakefile for dot and reStructuredText
#
##### Inputs ######
PROJ := MyDoc
DOCS := $(PROJ).html $(PROJ).odt
IMAGES := $(PROJ).svg $(PROJ).png
##### Targets ######
.PHONY: all clean images
all: docs
docs: $(DOCS)
images: $(IMAGES)
clean:
@-$(RM) $(DOCS) $(IMAGES)
##### Rules ######
%.svg : %.dot
dot -Tsvg $< -o $@
%.png : %.dot
dot -Tpng $< -o $@
%.html : %.rst %.svg
rst2html.py $< $@
%.odt : %.rst %.png
sed 's/\.svg/\.png/g' $< | rst2odt.py > $@
Rake
#
# Rakefile for dot and reStructuredText
#
require 'rake/clean'
##### Inputs ######
PROJ = "MyDoc"
DOCS = [ "#{PROJ}.html", "#{PROJ}.odt" ]
IMAGES = [ "#{PROJ}.svg", "#{PROJ}.png" ]
##### Targets ######
CLEAN.include DOCS, IMAGES
desc "Build documents"
task :docs => DOCS
desc "Build images"
task :images => IMAGES
desc "Build all"
task :default => 'docs'
##### Rules ######
rule '.svg' => ['.dot'] do |t|
sh "dot -Tsvg #{t.source} -o #{t.name}"
end
rule '.png' => ['.dot'] do |t|
sh "dot -Tpng #{t.source} -o #{t.name}"
end
rule '.html' => ['.rst','.svg'] do |t|
sh "rst2html.py #{t.source} #{t.name}"
end
rule '.odt' => ['.rst','.png'] do |t|
sh "sed 's/\.svg/\.png/g' #{t.source} | rst2odt.py > #{t.name}"
end
2.3 Putting it all together
My rakefile template for building C projects.
3. Extras
3.1 Task iteration
From stackoverflow #1290119.
Ruby tasks, when invoked, are marked as complete. Repeated calls (e.g. in a loop) will invoke only once.
The solution is either to:
- convert the task to a method
- reenable the task after each call
As A Method
task :build => [:some_other_tasks] do
build
end
task :build_all do
[:debug, :release].each { |t| build t }
end
def build(type = :debug)
# ...
end
Reenabling tasks
Rake::Task[':build'].reenable
Rake::Task[':build'].invoke
4. Links
---