Friday, February 24, 2006

Shoehorning Keep State into GNU Make

Sun Make has a lovely feature called Keep State: if the commands used to build a target change from build to build the target is rebuilt, even if looking at file time stamps shows that the target is "up to date". Why is this a lovely feature? Because it means that make followed by make DEBUG=1 will do that right thing. In Make's that only check time stamps the make DEBUG=1 would probably report that there was no work to do.

Of course, you can get round these problems if you really try (e.g. for the DEBUG case you could encode the fact that the objects are debug objects in either the name or path and then Make would do the right thing).

A recent post on the GNU Make mailing list got me thinking about this problem again and I've come up with a very simple solution that shoehorns Keep State into GNU Make. There's no code change to GNU Make at all; it's all done with existing GNU Make functions.

Here's an example Makefile that I've modified to rebuild foo.o and bar.o if their commands change

include signature

all: foo.o bar.o


foo.o: foo.c
$(call do,$$(COMPILE.C) -DFOO=$$(FOO)$$(@F) -o [email protected] $$<)

bar.o: bar.c
$(call do,$$(COMPILE.C) -DBAR=$$(BAR) -o [email protected] $$<)

-include foo.sig bar.sig
There are three modifications from a standard Makefile: firstly there's 'include signature' at the start. (You'll see the definition of signature below), then the commands for each rule have been wrapped in $(call do,...) and any $'s in the commands have been quoted with an extra $. Lastly the Makefile includes a .sig file for each .o being created (if the .sig exists, hence the -include instead of include).

The .sig file is generated by code in signature when a rule is run and is used to perform the 'command has changed' checking that you need. Here, for example, is the contents of bar.sig after make has been run for the first time:

$(eval @ := bar.o)
$(eval % := )
$(eval < := bar.c)
$(eval ? := bar.c)
$(eval ^ := bar.c)
$(eval + := bar.c)
$(eval * := bar)

bar.o: bar.force

$(if $(call sne,$(COMPILE.C) -DBAR=$(BAR) -o [email protected] $<,
g++ -c -DBAR= -o bar.o bar.c),$(shell
touch bar.force))
The first set of lines captures the state of the automatic variables within the rule to make bar.o, the next line says that bar.o depends on a special file called bar.force and lastly there's a rather complex $(if ...) that uses the GMSL (see GNU Make Standard Library) string-not-equal (sne) function to check the current expansion of the commands to make bar.o against the previous expansion. It's this $(if ...) that can detect a change in the commands to run a rule. If such a change is detected bar.force is touched and hence bar.o will be rebuilt because bar.force is newer.

The signature include is where the work is done:

include gmsl

last_target :=

dump_var = \$$(eval $1 := $($1))

define new_rule
@echo "$(call map,dump_var,@ % < ? ^ + *)" > $S
@$(if $(wildcard $F),,touch $F)
@echo [email protected]: $F >> $S

define do
$(eval S := $*.sig)$(eval F := $*.force)$(eval C := $1)
$(if $(call sne,[email protected],$(last_target)),$(call new_rule),$(eval
last_target := [email protected]))
@echo "$(subst $$,\$$,$$(if $$(call sne,$1,$C),
$$(shell touch $F)))" >> $S
I won't go into all the details of how signature works, but essentially the do macro is responsible for updating the .sig files as needed. I'll write this up for my column on CM Crossroads in March, but you can play around with the code (you need the GMSL and GNU Make 3.80 for this to work) and you'll see that changing a parameter does work.

Here's an example of starting from scratch and then changing the values of FOO and BAR in the Makefile above:

$ make
g++ -c -DFOO=foo.o -o foo.o foo.c
g++ -c -DBAR= -o bar.o bar.c
$ make
make: Nothing to be done for `all'.
$ make BAR=bar
g++ -c -DBAR=bar -o bar.o bar.c
$ make BAR=bar
make: Nothing to be done for `all'.
$ make BAR=baz
g++ -c -DBAR=baz -o bar.o bar.c
$ make BAR=baz FOO=foo
g++ -c -DFOO=foofoo.o -o foo.o foo.c
$ make BAR=bar FOO=foo
g++ -c -DBAR=bar -o bar.o bar.c
$ make
g++ -c -DFOO=foo.o -o foo.o foo.c
g++ -c -DBAR= -o bar.o bar.c
The only limitation of this scheme is that if you change the commands in a rule by editing the Makefile you need to do a clean build or at least delete the corresponding .sig file so that it gets remade. (Of course, even that could be worked around by making foo.o and bar.o depend on Makefile)