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=$(FOOEY)
foo.o: foo.c
$(call do,$$(COMPILE.C) -DFOO=$$(FOO)$$(@F) -o $$@ $$<)
bar.o: bar.c
$(call do,$$(COMPILE.C) -DBAR=$$(BAR) -o $$@ $$<)
-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 $@ $<,
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 $@: $F >> $S
endef
define do
$(eval S := $*.sig)$(eval F := $*.force)$(eval C := $1)
$(if $(call sne,$@,$(last_target)),$(call new_rule),$(eval
last_target := $@))
@echo "$(subst $$,\$$,$$(if $$(call sne,$1,$C),
$$(shell touch $F)))" >> $S
$C
endef
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)
Labels: gnu make