Metaprogramming is the ability of a program generate other programs, or even modify itself while running. This is the third in a series of posts exploring how to use metaprogramming in GNU make makefiles, discussing constructed macro names.
Constructed Macro Names
In previous posts we’ve discussed the concepts of evaluation and expansion, including recursive expansion. In this post we’ll examine constructed macro names.
This facility is the simplest to understand, and one of the most useful. It’s also mostly portable across a wide array of make
implementations, which is nice (although always keep in mind Paul’s First Rule 🙂 ). However, it still conforms to the rules of expansion and evaluation which means that it cannot fundamentally alter the behavior of make; in that sense it’s arguably not really “metaprogramming”, but we’ll start with it anyway.
A simple macro assignment and reference, as we know, looks like this:
MY_VAR = my value all: ; @echo '$(MY_VAR)'
The important idea for this post is that the text constituting the name of the macro itself is expanded, before the macro name is looked up!
Consider this makefile:
foo_TEXT = foo
bar_TEXT = bar
all: foo bar
foo bar: ; @echo "$@ text is $($@_TEXT)"
The $($@_TEXT)
is a constructed macro name. Make notices the first dollar sign/open parenthesis and realizes that it introduces a macro reference. It then collects all the text up to the matching close parenthesis: $@_TEXT
. Instead of immediately attempting to find a macro with that name, make will first expand that text. In this case, as we know $@
is an automatic variable which make will set to the name of the target (foo
or bar
). So $@_TEXT
will expand to foo_TEXT
when building the target foo
, and bar_TEXT
when building the target bar
:
foo text is foo bar text is bar
This can be an extremely powerful capability, allowing recipes to be customized on a per-target basis without having to write separate rules. If you wanted to allow extra compilation flags to be added to the recipe only for certain object files, you could write a generic pattern rule then define explicit rules for each target which needed a different set of flags. Obviously this is annoying from many perspectives. Using constructed macro names instead, we can use a single pattern rule for all objects; for example:
SRCS = foo.c bar.c biz.c baz.c
%.o : %.c
$(CC) $(CPPFLAGS) $(CFLAGS) $($*_FLAGS) -c -o $@ $<
all: $(SRCS:%.c=%.o)
bar_FLAGS = -Wno-error
baz_FLAGS = -I/opt/baz/include
When make invokes the recipe to create the target bar.o
it will add -Wno-error
to the compile line, and when it invokes the recipe to create baz.o
it will add -I/opt/baz/include
to the compile line. Constructs like this allow for a more "data-driven" makefile implementation, where the build is controlled more by variable settings and less by explicit rules.
Constructed macro names can also be useful in the context of looping constructs such as the foreach
function; given the above makefile we could imagine adding a debugging statement:
$(foreach S,$(SRCS),$(info extra flags for $S: $($(basename $S)_FLAGS)))
which would give the output:
extra flags for foo.c: extra flags for bar.c: -Wno-error extra flags for biz.c: extra flags for baz.c: -I/opt/baz/include
Target-Specific Variables
A popular alternative for some uses of constructed variable names are target-specific variables. These can be used to set macro values which are in effect only in the context of a specific target. The above example could be equivalently written, with target-specific variables, as:
SRCS = foo.c bar.c biz.c baz.c %.o : %.c $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< all: $(SRCS:%.c=%.o) bar.o : CFLAGS += -Wno-error baz.o : CPPFLAGS += -I/opt/baz/include
Target-specific variables have an additional feature which can be quite powerful: they are inherited by all prerequisites of that target. So for example you can create a top-level target debug
that adds debugging flags to all compilations:
all: prog prog: prog.o foo.o bar.o biz.o baz.o debug: all debug: CFLAGS += -g debug: CPPFLAGS += -DDEBUG
Now running make debug
will cause the all
target and its prerequisites to be built (since debug
depends on all
), and all these targets inherit the settings for CFLAGS
and CPPFLAGS
from debug
.
On the other hand, target-specific variable values are available only within the recipe of their target. This means they're not available for external debugging statements such as the foreach
loop example above. It also means they're not useful for more advanced metaprogramming techniques.
Limitations
The main limitation on constructed macro names is that they still must obey the normal rules for immediate and deferred expansion. For example, users often try to do something like this:
foo_OBJECTS = foo.o baz.o biz.o
bar_OBJECTS = bar.o baz.o boz.o
foo bar: $($@_OBJECTS)
However, this cannot work because the prerequisite list is expanded in an immediate context, and the automatic variables like $@
are not set until much later, when make wants to invoke the recipe. So $@
(most likely) expands to the empty string, and $($@_OBJECTS)
expands to the value of the macro _OBJECTS
, which is likely not set either.
Target-specific variables have this same limitation, since as noted above their values are available only within the context of a recipe, not when expanding and evaluating prerequisites in an immediate context.
Next...
In the next post we'll examine a feature used to overcome the limitation on using automatic variables in prerequisite lists: secondary expansion.