Struggling to get variable substitution to affect targets and dependencies in GNU Make? Here's why, and how to fix it (if you don't mind a good hack).


GNU Make is powerful and, although it's been superceded in some circles by wrappers like automake or all-out replacements like CMake and scons, for simple distributions it's still got teeth.

It's not without its pitfalls, though. In the below Makefile, not only do I have to create my own string equality function eq, but the outcome is not what I'd have expected:

# Comparison function
eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1)))

# Variables
suf = $(if $(call eq, $(NATIVE), 1),-native,)
build_dir = build$(suf)

# Targets
.PHONY: a b

# (should yield "build")
a: $(build_dir)

# (should yield "build-native")
b: NATIVE=1
b: $(build_dir)

$(build_dir):
     @echo $@ ($(build_dir))

Outcome:

# Flag behaving properly:
~$ make a NATIVE=1
build-NATIVE build-NATIVE
~$ make b NATIVE=1
build-NATIVE build-NATIVE

# Flag not behaving properly:
~$ make a
build (build)
~$ make b
build (build-NATIVE)

In target b with no flag initially given, the sub-target build is being invoked rather than the build-NATIVE target that I'm really after, as indicated by the output of $@. Notice how $(build_dir) is correct within the rule itself.

In real life, my Makefile will accept a flag describing the target platform for the build; the default case is an ARMv5 cross-compilation, but a native build can be performed for local debugging. In addition, unit tests must be built natively as they are executed locally as part of the build process. So the idea is to override the target flag for the unit test target; then, even when mixing targets as in make check all, for the check target only a flag NATIVE is automatically set to "1" even though it was missing from the commandline invocation.

Alas, as shown above, although this works for instructions inside the encapsulated rule, the same is not true for the target itself (or its dependencies, of which we haven't written any here). This is because the Makefiles run in two phases: targets and pre-requisites are expanded in the "read-in" phase; my variable isn't reset until the "target-update" phase, in which only variables inside rules are expanded.

To work around this, we can make use of "secondary expansion". Applying it to the example above, using the details provided in the GNU Make documentation:

# Comparison function
eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1)))

# Variables
suf = $(if $(call eq, $(NATIVE), 1),-native,)
build_dir = build$(suf)

# Targets
.PHONY: a b
.SECONDEXPANSION:

# (should yield "build")
a: $$(build_dir)

# (should yield "build-native")
b: NATIVE=1
b: $$(build_dir)

$(build_dir):
     @echo $@ ($(build_dir))

Outcome:

# Flag behaving properly:
~$ make a NATIVE=1
build-NATIVE build-NATIVE
~$ make b NATIVE=1
build-NATIVE build-NATIVE

# Flag not behaving properly:
~$ make a
build (build)
~$ make b
make: *** No rule to make target `build-foo', needed by `b'. Stop.

We're closer: target b is now at least attempting to invoke the correct sub-target build-foo. Unfortunately, secondary expansion only works for the prerequisite lists and not for target names, so in the case above the target build-foo doesn't actually exist.

The sensible thing to do at this point is to get that explicit value out of all targets and use patching matching instead or, better yet, employ AutoMake. But if the targets don't have obvious patterns and a build system switch doesn't whet your appetite today, and if you fancy a good hack, there is a complete workaround:

# create comparison function
eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1)))

suf = $(if $(call eq, $(MYVAR), 1),-foo,)
build_dir = build$(suf)

.PHONY: a b
.SECONDEXPANSION:

# Should yield `build`
a: $$(build_dir)

# Should yield `build-foo`
b: $$(if ($$call eq,$$(MYVAR),1),$$(build_dir),)
        $(if $(call eq,$(MYVAR),1),,@make b MYVAR=1)

$(build_dir):
        @echo $@ ($(build_dir)) $(INHERITED)

Outcome:

[~]$ make a INHERITED=6
build (build) 6
[~]$ make a MYVAR=1 INHERITED=6
build-foo (build-foo) 6
[~]$ make b INHERITED=6
make[1]: Entering directory `~`
build-foo (build-foo) 6
make[1]: Entering directory `~`

By invoking make from scratch for the target tree under b, we can have our targets any way we want. Any other "local" variables are even automatically inherited by the sub-make (as demonstrated above by the INHERITED variable).