Metaprogramming Make III — Constructed Macro Names

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.

Leave a Reply

Your email address will not be published. Required fields are marked *