by Adam Brett

Makefile Variables

Following on from my previous post on getting started with make, I wanted to cover some conventions and best practices I've picked up over the last few years. At least, that's how it started. Before I did that, I felt I needed to give some background on the various types of variables and assignment available in Make for some of the best practices to make sense. It turns out, there's a lot to write about variables in Make, so it's become a post all of its own. Here it goes.

Recursive Assignment

The first is the simplest:

foo = bar

This is a straightforward type of assignment. A simple name = value assignment has a slight quirk in Make, in that if value is an expression, or another variable, it's evaluated when it's used, and not when it's defined. For example:

NOW = $(shell date +%s.%N)

test:
    @echo ${NOW}
    @echo ${NOW}

Here, we're creating a variable called NOW, and using it to go out to the current shell and grab the date. %s is the unix timestamp, and %N is the current nanoseconds.

If you save this in a Makefile, and run make test, you should now see two different values for nanoseconds. This is because NOW is being interpreted when it's used, and not at the time it was defined.

$ make test
1479219496.158755384
1479219496.159360531

Similarly, if we were to assign a variable value to this, the value of the original variable would be used, like a pointer:

FOO = bar
BAR = ${FOO}
FOO = baz

test:
    @echo ${BAR}

Which would give us:

$ make test
baz

Simple (Static) Assignment

Building on this, if you want the value to be interpreted at define time, and not use, you need a static assignment, which looks like this:

foo := bar

If we update our example to use this new type of assignment:

NOW := $(shell date +%s.%N)

test:
    @echo ${NOW}
    @echo ${NOW}

Then run make test again, this time, we should see that nanoseconds contains the same value both times, which is the value of nanoseconds at the time NOW was defined.

$ make test
1479219766.828914100
1479219766.828914100

If we take a look at the variable assignment again, this time we get the value of FOO at the time BAR was defined:

FOO = bar
BAR := ${FOO}
FOO = baz

test:
    @echo ${BAR}

Like so:

$ make test
bar

Meaning any subsequent changes to FOO will no longer have an effect on the value of BAR.

Conditional (Default) Assignment

The final type of assignment is my favourite, I call it default assignment, but it's real name is the conditional variable assignment operator. What it allows us to do is set variables in our Makefile which we can override from our shell in the form of environment variables, either persistently via exported variables, or at runtime, allowing us to set the equivilent of parameters on our Makefile targets.

It looks like this:

NOW ?= $(shell date +%s.%N)

test:
    @echo ${NOW}
    @echo ${NOW}

Now if we run it via make test:

$ make test
1479220417.291186372
1479220417.291900324

You'll see here that it's behaved the same as the simple assignment operator by default, but what happens if we set the value of NOW for the command in our shell:

$ NOW=foo make test
foo
foo

Cool! We can set the value after our command too, and pretend we're passing in parameters:

$ make test NOW=foo
foo
foo

We can also export the value in our shell, and make it persistent:

$ export NOW=foo
$ make test
foo
foo
$ make test
foo
foo

This has a really nice use case which we'll get to in the next post.

Target Specific Variables

Having looked at the main ways we can assign variables in Make, what we haven't looked at is the scope of those variables. Simply put, all variables, no matter where they're defined, are global, and are always available.

That is, unless you make them target specific. To do that, you duplicate the target, and write the variable assignments on the same line, like so:

foo: BAZ=qix
foo:
    @echo ${BAZ}

bar: BAZ=qux
bar:
    @echo ${BAZ}

Now if we run make foo and make bar, we should see two different values for $BAZ

The local variables are defined like dependencies, but on a different target line. It doesn't matter if the line is above or below, it can be anywhere:

foo:
    @echo ${BAZ}

bar: BAZ=qux
foo: BAZ=qix
bar:
    @echo ${BAZ}

The result is the same:

$ make foo
qix
$ make bar
qux

Obviously, this is messy and hard to follow. My best practices dictate that they go on the line immediately above the target they're for.

We can declare multiple local variables by adding more lines, like so:

foo: BAR=baz
foo: QIX=qux
foo: QUUX=corge
foo:
    @echo ${BAR}
    @echo ${QIX}
    @echo ${QUUX}

We can use all of the different types of assignment here, just like with global variables, so we can set default variables that are target specific, too.

foo: BAR =  baz
foo: BAZ := qix
foo: QIX ?= qux
foo:
    @echo ...

The only caveat is that if you set a default variable for a variable that already exists, the existing value will be used as the default:

FOO := bar

foo: FOO ?= baz
foo:
    @echo ${FOO}

Which produces:

$ make foo
bar

But if we try setting the value of FOO:

$ make foo FOO=test
test

We can still set the value, and using this method convert a simple or static variable into a variable we can set from our shell, however our target specific default is being completely ignored.

Finally, it's also possible to set local variables that overrider global variables for a specific target, like so:

FOO := bar

test: FOO := baz
test:
    @echo ${FOO}

Which gives us:

$ make test
baz

When using target specific variables, there is an additional quirk - that value will be used for all dependencies too:

BAR=foo

foo: BAR=baz
foo: bar
    @echo foo target:
    @echo ${BAR}

bar:
    @echo bar target:
    @echo ${BAR}

Which for make foo give us:

$ make foo
bar target:
baz
foo target:
baz

And for make bar:

$ make bar
bar target:
foo

Appending Assignment

Whilst we've looked at the main types of assignment in Make, and the scope of those variables, there is one we've missed, and I've deliberately saved it until the end, because it is quite useful as a target specific assignment:

FOO = foo

foo: FOO += bar
foo:
    @echo ${FOO}

Now you may expect running make foo to produce foobar, but it doesn't:

$ make foo
foo bar

I would assume this is because Make's primary purpose is as a build tool, and this is most useful when building up a list of files, or a list of parameters for an argument. I don't know of a way to make it not add the space. If you wanted to do that, you probably need to use something like this:

FOO = foo

foo: FOO := ${FOO}bar
foo:
    @echo ${FOO}

You have to use the static assignment here, rather than the recursive, because otherwise FOO would hold a reference to itself, which it could never resolve.

The assignment operator works in the opposite way, so if you use it, it will be effected by the changes to any variables after it's used:

FOO = foo
BAR = bar

FOO += ${BAR}

BAR = baz

foo:
    @echo ${FOO}

Which results in:

$ make foo
foo baz

As a final note, appending assignment doesn't have to be used as a target specific variable, and can be used anywhere, just like any other type of assignment.

Conclusion

Hopefully this has given you a good grounding in the types of variables available to you in Make. Maybe you can already see some usages for them, or some patterns you've seen in Makefiles in the past make a little more sense.

In the next post, I'll jump straight in to some best practices, and I'm already planning posts 4 and 5 after that too, so keep an eye out for those.

For exclusive content, including screen-casts, videos, and early beta access to my projects, subscribe to my email list below.


I love discussion, but not blog comments. If you want to comment on what's written above, head over to twitter.