Makefile Assertions

[article]
Summary:

Unfortunately, GNU Make does not have any form of assertions built in, but they are easy to create using existing GNU Make functions.

Unfortunately, GNU Make does not have any form of assertions built in, but they are easy to create using existing GNU Make functions. There are even convenient assertion macros defined in the GNU Make Standard Library.

The GNU Make Standard Library (GMSL) project provides two assertion macros: assert and assert_exists. assert will output a fatal error if its first argument is false. In GMSL terms (and for GNU Make's $(if) function) true is any non-empty string, and false is an empty string. Thus if assert's argument is an empty string the assertion will cause a fatal error; the second argument to assert is printed as part of the error. For example, this Makefile breaks immediately with an assertions because $(FOO) and $(BAR) are the same:

include gmsl

FOO := foo
BAR := foo

$(call assert,$(call sne,$(FOO),$(BAR)),FOO and BAR should not be equal)

It prints the message:

Makefile:5: *** GNU Make Standard Library: Assertion failure: FOO and BAR should not be equal. Stop.

The macro uses another GMSL function, sne, which compares to strings and returns true if they are not equal, or false is they are the same. Because true is simply "not an empty string", it's easy to assert that a variable be defined:

include gmsl

$(call assert,$(FOO),FOO is not defined)

This can be used, for example, to check that a user has set all the necessary variables on the command-line; if FOO is set on the command-line of GNU Make then the assertion will not cause an error. The assertions could even be used to enforce that certain command-line flags are not used. Here's an example that prevents the user from setting -i (the ignore errors flag):

include gmsl

$(foreach o,$(MAKEFLAGS),$(call assert,$(call sne,-i,$o),You can't use the -i option))

ifneq ($(patsubst -%,-,$(firstword $(MAKEFLAGS))),-)
$(call assert,$(call sne,$(patsubst i%,i,$(patsubst %i,i,$(firstword $(MAKEFLAGS)))),i),You can't use the -i option)
endif

It's more complex than the previous two examples because GNU Make can store the -i flag in the variable MAKEFLAGS (which contains a record of the command-line options passed to GNU Make) in two ways: as a flag in the familiar form -i, or as a block of single characters in the first word of MAKEFLAGS: e.g. the command-line flags -i -k results in MAKEFLAGS having the value ki. The first assert in the loop looks for -i, the second assert searches for i in the first word of MAKEFLAGS.

Because the presence or absence of files is so vital to GNU Make, the GMSL provides an assertion specifically design to warn if a file is missing: assert_exists. assert_exists has a single argument, the name of the file that must exist. For example, it's possible to check that the file foo.txt exists before any commands are run by the Makefile by adding an assertion at the start:

include gmsl

$(call assert_exists,foo.txt)

If the file does not exist then an assertion stops the Make:

Makefile:3: *** GNU Make Standard Library: Assertion failure: file 'foo.txt' missing. Stop.

A common problem in building real-world Makefiles that directory hierarchies must be constructed during or before the build. Using assert_exists it's possible to ensure that every directory exists before each rule runs. Using a combination of GNU Make and GMSL functions and macros the special assert_target_directory macro can be created:

include gmsl

assert_target_directory = $(call assert,$(wildcard $(dir $@)),Target directory $(dir $@) missing)

foo/all: ; @$(call assert_target_directory)echo $@

By inserting $(call assert_target_directory) at the start of each rule's body (or the start of a pattern rule's body), GNU Make will automatically check that the directory in which the target is to be written exists. For example, if foo/ does not exist the Makefile above results in the following error:

Makefile:6: *** GNU Make Standard Library: Assertion failure: Target directory foo/ missing. Stop.

And the error itself gives the name of the Makefile and the line number at which the problem occurred, making it trivial to find the rule that has a problem.

For a final trick it's possible to make the Makefile check every single rule for a missing directory with a two line modification to the Makefile. Instead of adding $(call assert_target_directory) to every rule, just redefine the $(SHELL) variable to include $(call assert_target_directory). This does slow performance, but can be useful in tracking down a missing directory somewhere deep in nested Makefiles.

include gmsl

assert_target_directory = $(call assert,$(wildcard $(dir $@)),Target directory $(dir $@) missing)

OLD_SHELL := $(SHELL)
SHELL = $(call assert_target_directory)$(OLD_SHELL)

foo/all: ; @echo $@

GNU Make expands the value of $(SHELL) and hence performs a call to assert_target_directory for every rule that is run. That simple change means that every rule run is automatically checked.

The new value of $(SHELL) consists of a call to assert_target_directory (which always returned an empty string) followed by the old value of $(SHELL) stored in OLD_SHELL. Note how OLD_SHELL is defined using := so that SHELL doesn't refer to itself. OLD_SHELL contains the value of $(SHELL) at run time and can be safely used to redefine $(SHELL). If OLD_SHELL were defined using = then GNU Make would fail to run because of a circular reference SHELL, which would refer to OLD_SHELL which would refer to SHELL.

assert_target_directory works by calling $(wildcard) (a GNU Make function) with the name of the directory in which the current target being built is to be written. The target is defined by the GNU Make automatic variable $@ and the directory portion is extracted with $(dir). $(wildcard) simply checks to see if the directory exists or not and returns the name of the directory (if it exists), or the empty string if the directory is missing. Recall that the assert function interprets a non-empty string as true, and empty as false, to see that the assertion only fires if the directory is missing.

The trick of redefining SHELL so that some function is performed for every rule executed by the Makefile has uses beyond assertions. In a future article I'll show how redefining shell allows tracing of the execution path of a Makefile and even the generation of an XML document describing a run of Make.

About the author

CMCrossroads is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.