Monday, August 14, 2006

Recursive Make Considered Harmful -- Building Multiple Programs

I have a number of test programs in a testsuite for an embedded system. They may share source files among them in addition to some other common files. I do not feel like managing a number of full-blown makefiles in building these programs. Besides, Peter Miller has written a very excellent paper on "Recursive Make Considered Harmful". He argued that multiple makefiles with common dependencies simply do not work.

The paper above shows how to handle building a program with dependencies that come from multiple directories. How would we then build multiple programs using a single makefile without too much boilerplate cut-and-paste? The latest GNU make 3.81 with (correct) eval support makes this very easy. Just like in the paper, I depend on GCC to generate source code dependencies and also on sed, the stream editor, to transform the dependency output to what I want. But first, below is the structure of my build directory:



The build directory contains the makefile and the other directories are "module" directories.

Similar to the paper, I put makefile include file module.mk in each module directory. The source files other modules may depend on are exported through the SRC variable. For my case, however, a module may also have one or more source files that contribute to the implementation of the main function for the module (let's call this module program module.) For this purpose, I define PROG_SRC variable for declaring these source files. An example from module.mk from my dhrystone module is given below.
SRC +=
PROG_SRC := dhrystone/dhry_1.c dhrystone/dhry_2.c

Below is another example of module.mk from my common module, which do not have a main function.
SRC += common/vec.S common/swi.S common/crt0.S \ 
common/int-handler.c common/swi-handler.c \
common/intr-write.c common/swi-write.c \
common/swi-clock.c common/console.c

In my singular makefile, I have variable PROGS to declare all program modules and COMMS for other "common" modules. For example:
PROGS := simple unmapped rap fiq dhrystone large
COMMS := common

To declare the modules a program module depends on, I declare makefile variable with name like <program-module>_MK. Basically, this variable lists the module.mk files from the modules the program module depends on. An example is given below for my simple program module:
simple_MK := ../common/module.mk ../simple/module.mk

The above simply says that the simple module depends on the common module and its own. Note that the "self" module.mk must be last in the list so that its PROG_SRC variable will take effect.

Given that we have defined all of the program modules and the module.mk files each depends on, how do we go about creating the rules for building the program modules? Copy-and-paste is one option but where is the fun in that? What we need is rule building template which can be "instantiated" through eval magic. The template is given below:
define PROG_template
SRC :=
include $$($(1)_MK)
$(1)_OBJ := $$(call get_objs,$$(patsubst %, ../%,$$(PROG_SRC) $$(SRC)))

$(1): $$($(1)_OBJ)
$$(CC) $$(LDFLAGS) -o $$@ $$^
endef

So if you were to call PROG_template with argument simple, make would have emitted this text:
SRC :=
include $(simple_MK)
simple_OBJ := ../simple/simple.o ../common/vec.S # ...and other common files

simple: $(simple_OBJ)
$(CC) $(LDFLAGS) -o $@ $^

All module.mk files simple depends on are first included, thus filling in the SRC and PROG_SRC variables. Next, the object files it depends on are listed followed by the rule to build simple. By the way, function get_objs called above converts all .c and .S filenames into .o filenames:
define get_objs
$(patsubst %.S,%.o, $(filter %.S,$(1))) $(patsubst %.c,%.o, $(filter %.c,$(1)))
endef

Now, the trick is to "evaluate" the emitted text for all of the program modules:
$(foreach t,$(PROGS),$(eval $(call PROG_template,$(t))))

And finally, one rule to build all program modules:

progs: $(PROGS)

That is all there is to it. Below is the complete makefile for your reference.
.SUFFIXES:
.SUFFIXES: .h .c .S .o .lst .sym .d

# list the test program to build. There should be a directory for each
# test program with the same name. COMMS are for directories that contains
# common source files but do not contain main function.

PROGS := simple unmapped rap fiq dhrystone large
COMMS := common

# define the module.mk files to include for building each test. Each
# module.mk defines the source files it exports in variable SRC and,
# if the module defines a program, the program source file(s) in
# PROG_SRC. Put the module.mk for the program last.

simple_MK := ../common/module.mk ../simple/module.mk
unmapped_MK := ../common/module.mk ../unmapped/module.mk
rap_MK := ../common/module.mk ../rap/module.mk
fiq_MK := ../common/module.mk ../fiq/module.mk
dhrystone_MK := ../common/module.mk ../dhrystone/module.mk
large_MK := ../common/module.mk ../large/module.mk

# !!!DO NOT CHANGE ANYTHING BELOW THIS LINE!!!

TARGET:=arm-elf
CC:=$(TARGET)-gcc
AS:=$(TARGET)-as
LD:=$(TARGET)-ld
OBJDUMP:=$(TARGET)-objdump
NM:=$(TARGET)-nm
OBJCOPY:=$(TARGET)-objcopy
STRIP:=$(TARGET)-strip
RM:=rm

SED:=sed
LDSCRIPT:=zero.ld

XDEFINES:=-DHZ=100
XINCLUDES:=$(COMMS:%=-I../%) $(PROGS:%=-I../%)
DFLAGS:=-g
OFLAGS:=-O2 -fomit-frame-pointer
WFLAGS:=-ansi -Wall -Wstrict-prototypes -Wno-trigraphs
CFLAGS:=-mcpu=arm7tdmi $(DFLAGS) $(OFLAGS) $(WFLAGS) \
$(XDEFINES) $(DEFINES) $(XINCLUDES) $(INCLUDES)
ASFLAGS := $(SDDEFINES) $(XDEFINES) $(DEFINES) $(XINCLUDES) $(INCLUDES)
LDFLAGS := $(CFLAGS) -Wl,--script=$(LDSCRIPT) -nostartfiles

#objdump flags for generating listing files
ODFLAGS :=

.PHONY: all progs listings clean clean-dep clean-all

all: progs listings

progs: $(PROGS)

listings: $(PROGS:%=%.lst) $(PROGS:%=%.sym)

# deduce object files from .S and .c files

define get_objs
$(patsubst %.S,%.o, $(filter %.S,$(1))) $(patsubst %.c,%.o, $(filter %.c,$(1)))
endef

# an eval template for deducing object files and rule for a program.
# $(1) is the test program name.

define PROG_template
SRC :=
include $$($(1)_MK)
$(1)_OBJ := $$(call get_objs,$$(patsubst %, ../%,$$(PROG_SRC) $$(SRC)))

$(1): $$($(1)_OBJ)
$$(CC) $$(LDFLAGS) -o $$@ $$^
endef

# generate the rule for building each of the programs

$(foreach t,$(PROGS),$(eval $(call PROG_template,$(t))))

# generate all of the object files from all test test programs
# and use it to include all of the dependency files.

ALL_OBJ :=
$(foreach t,$(PROGS),$(eval ALL_OBJ += $$($(t)_OBJ)))
ALL_OBJ := $(sort $(ALL_OBJ))

include $(ALL_OBJ:.o=.d)

clean:
-$(RM) $(PROGS) $(PROGS:%=%.lst) $(PROGS:%=%.sym) \
$(ALL_OBJ)

clean-dep:
-$(RM) $(ALL_OBJ:.o=.d)

clean-all: clean clean-dep

%.lst: %
$(OBJDUMP) $(ODFLAGS) -d $< > $@

%.sym: %
$(NM) -n $< > $@

%.d: %.c
@echo generating dependencies from $<
@$(CC) $(CFLAGS) -MM -MG $< | \
$(SED) 's%^\(.*\)\.o%$(dir $@)\1.d $(dir $@)\1.o%' > $@

%.d: %.S
@echo generating dependencies from $<
@$(CC) $(ASFLAGS) -MM -MG $< | \
$(SED) 's%^\(.*\)\.o%$(dir $@)\1.d $(dir $@)\1.o%' > $@

(Note: You need to change the space characters before each command in a rule with a tab!)

No comments: