RakeForMakeUsers

A guide to switching from Make to Rake.

If you're just after a Rakefile for building C code, use my Rake Template.

On this page... (hide)

  1. 1. Syntax
    1. 1.1 Variables
    2. 1.2 Targets
    3. 1.3 Rules
    4. 1.4 Globs and Patsubst
    5. 1.5 Output Paths
    6. 1.6 Include Paths and Define lists
    7. 1.7 Spaces in Filenames
    8. 1.8 OS Detection
    9. 1.9 Invocation and Arguments
    10. 1.10 PHONY
  2. 2. Examples
    1. 2.1 Single build tree
    2. 2.2 Make and Rake
    3. 2.3 Putting it all together
  3. 3. Extras
    1. 3.1 Task iteration
  4. 4. Links

1.  Syntax

1.1  Variables

Immediate evaluation assignment.

PROJ := MyDoc
DOCS := $(PROJ).html $(PROJ).odt

PROJ = "MyDoc"
DOCS = [ "#{PROJ}.html", "#{PROJ}.odt" ]

$[Get Code]21

Lazy evaluation assignment.

TODO

1.2  Targets

images: $(IMAGES)

desc "Build images"
task :images => IMAGES

$[Get Code]22

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

$[Get Code]23

1.4  Globs and Patsubst

SRC := $(wildcard *.c)
OBJ := $(patsubst %.c,%.o,$(SRC))

SRC = FileList['*.c']
OBJ = SRC.ext('o')

$[Get Code]24

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

$[Get Code]28

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

$[Get Code]29

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

$[Get Code]32

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(" ")

$[Get Code]33

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

$[Get Code]35

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

$[Get Code]36

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

$[Get Code]37

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

$[Get Code]38

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"

$[Get Code]39

$ 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

$[Get Code]40

But it can be simulated with:

def (task(:phony)).timestamp
Time.at
end

task :clean => :phony

$[Get Code]41


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.

GNUmake?

#
# 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

$[Get Code]44

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

$[Get Code]46

Reenabling tasks

Rake::Task[':build'].reenable
Rake::Task[':build'].invoke

$[Get Code]47

4.  Links

---|