Deferred Simple Variable Expansion

Most users of GNU make are familiar with its two types of variables: recursive variables, where the value is expanded every time the variable is referenced, and simple variables, where the value is expanded only once: when the variable is assigned. But what if you wanted a variable that was expanded only once, but not until the first time it was used? Such a thing is possible in GNU make… with a bit of trickery.

Let’s suppose that we have a command that generates output that we will need to store in a variable in our makefile. One way to obtain that output is using a recursive variable assignment:

OUTPUT = $(shell some-command)

This has the pleasant feature that some-command is not actually invoked until the variable reference $(OUTPUT) is expanded somewhere in the makefile. If $(OUTPUT) is never expanded, then some-command is never invoked. If the variable is expanded only one time (maybe you only need that variable in the recipe of one target) then some-command is expanded once. If these are true in your makefile, you’re done!

However, if $(OUTPUT) is expanded more than once, then some-command is re-run every time $(OUTPUT) is expanded. That’s most likely not what you want.

We know how we can avoid re-running the command multiple times: use a simple variable assignment instead:

OUTPUT := $(shell some-command)

This ensures that some-command is run only one time, which is probably better overall for us, but the downside is that it will be run every time we invoke make, even if we end up not actually needing this output (because we don’t happen to be invoking any recipes that rely on it, for example).

If running the command every time is not a burden, because the command is pretty fast and/or you expand $(OUTPUT) in enough parts of your makefile that it’s rare that you don’t need it at all, then you’re done!

However, let’s further suppose that this command is kind of slow and that its output is only needed for a subset of the targets in your makefile: enough that you don’t want to run it multiple times but not enough that you’re willing to eat the cost of running it every time you invoke make. How can we defer the invocation of some-command until the first time it’s used, while also ensuring that it’s only ever run once?

We can do this (at least if you have GNU make 3.80 or above), with a bit of eval magic:

OUTPUT = $(eval OUTPUT := $$(shell some-comand))$(OUTPUT)

Whoa! What is going on here?

Let’s break it down.

First, we can see that we’re using a recursive variable assignment, so we know that the right-hand side of the assignment will not be evaluated when this assignment is made. Thus after make parses this line, the value of the variable OUTPUT will be the string $(eval OUTPUT := $$(shell some-comand))$(OUTPUT). That’s good, because it means that we haven’t invoked the shell function or some-command yet. That fulfils the first part of our requirement: the command is not invoked every time we parse this makefile.

Next, suppose that we are going to expand $(OUTPUT) for the first time. Because the variable is recursive, make will expand its contents. As always, make will expand things from left to right, one expression at a time. So, it will first see this:

$(eval OUTPUT := $$(shell some-command))$(OUTPUT)

The eval function first will expand its argument, which yields this:

OUTPUT := $(shell some-command)

(remember that $$ expands to $). Then this string will be evaluated as a makefile construct. This is a simple variable assignment, which means that the right-hand side of the assignment is expanded immediately. This causes some-comand to be invoked and the output is assigned to the variable OUTPUT which is now converted into a simple variable because of the := assignment style.

You may worry about reassigning a variable while expanding it, but you don’t need to: make is working on a copy of the contents of that variable while it’s being expanded, so there’s no danger of stomping on the active value.

Wow, that’s cool! But there’s one problem here: the eval function expands to the empty string. That means the first time we expand $(OUTPUT), we’ll get the empty string. The second and subsequent times, since we have now redefined its value, we’ll get the right output. How can we fix this? That’s where the rest of the original recursively assigned value comes in. Remember we’ve finished the eval expression, so what’s left?

$(eval OUTPUT := $$(shell some-command))$(OUTPUT)

This expands the variable OUTPUT and since this variable now a simple variable assignment we won’t get an error about recursive variables referencing itself; instead we’ll get the new value.

Here’s something to consider: why do we need to escape the shell function invocation? Suppose instead we wrote this:

OUTPUT = $(eval OUTPUT := $(shell some-command))$(OUTPUT)

Here, some-command would be invoked when eval expanded the argument, before it started evaluating the results. Why does this matter? In most cases it won’t… but sometimes it will. Consider if some-command generated the output “foobar“; then eval will be expanding the makefile construct:

OUTPUT := foobar

Here it clearly doesn’t matter: you get a value of foobar. But, suppose that some-comand instead generated the output “foo$bar“? Now, eval wil be expanding the makefile construct:

OUTPUT := foo$bar

and it will treat $b as a reference to the makefile variable b which is most likely not set, and the avlue of OUTPUT will, confusingly, be fooar instead of foo$bar. By escaping the shell function we avoid the re-expansion of its output because the function itself is not expanded until eval is evaluting the results.

One last item on this subject to consider: what about target-specific variables? If you wanted to use this construct with target-specific variables you might try to use:

mytarget: OUTPUT = $(eval OUTPUT := $$(shell some-command))$(OUTPUT)

However, this won’t work. Why not? Because inside the eval you’re setting the global variable OUTPUT, not the target-specific variable OUTPUT. However this is easily solved; simply modify your eval argument to make this a target-specific assignment:

mytarget: OUTPUT = $(eval mytarget: OUTPUT := $$(shell some-command))$(OUTPUT)

I should mention that I tried this trick with pattern-specific variable assignments but couldn’t get it to work for various reasons. This may be a bug in GNU make.

Leave a Reply

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