The Basics: Getting environment variables into GNU Make

[article]

The most basic rule of GNU Make and the environment is that any variable set in the environment when GNU Make is started will be available as a GNU Make macro inside the Makefile.

For example, if FOO is set in the environment to foo when GNU Make is run then the following Makefile:

$(warning $(FOO))

will output Makefile:1: foo verifying that FOO was indeed set to bar inside the Makefile.   It's possible to discover where FOO got that value using GNU Make's $(origin) function (see http://www.gnu.org/software/make/manual/html_node/Origin-Function.html#Origin-Function for the full documentation of $(origin)).

For a variable VAR defined in the environment and automatically imported into GNU Make $(origin VAR) has the value environment.  So enhancing the Makefile above to:

$(warning $(FOO) $(origin FOO))

gives the output Makefile:1: foo environment.

A variable imported from the environment can be overriden inside the Makefile just by setting its value.  So changing the example Makefile to

FOO=bar

$(warning $(FOO) $(origin FOO))

now gives the output Makefile:3: bar file.  Notice how the value of $(origin FOO) has changed from environment to file.  file indicates that the variable got its value inside a Makefile.

It's possible to prevent a definition in a Makefile from overriding the environment by specifying the -e (or --environment-overrides) option on the command-line of GNU Make. 

Running the above Makefile with, as before, FOO set to foo in the environment and the -e command-line option gives the output Makefile:2: foo environment override. Notice here that FOO has the value from the environment (foo) and that the output of $(origin) has changed to environment override to inform us that the variable came from the environment and even though it was redefined in the Makefile the value from the environment
was being used.

$(origin) can still return environment (instead of environment override) if the variable being tested was defined in the environment, but there was no attempt to redefine it in the Makefile.

If all you care about is whether the variable got its value from the environment then using $(firstword $(origin VAR)) is always guaranteed to return the string environment if the variable VAR is getting its value from the environment (regardless of whether -e is specified or not).

And it's possible to override the overriding using the override directive.  Suppose you absolutely want to guarantee that FOO gets its value inside the Makefile, and not from the environment you override it (more on the override directive here http://www.gnu.org/software/make/manual/html_node/Override-Directive.html#Override-Directive):

override FOO=bar

$(warning $(FOO) $(origin FOO))

This will output Makefile:3: bar override regardless of the value of FOO in the environment and whether or not you specify the -e command-line option.  Note that $(origin) tells you this is an override by returning $(override).

The other way to get around -e and set the value of a macro is by setting it on the GNU Make command-line.  For example, running FOO=foo make -e FOO=fooey on the following Makefile outputs Makefile:3: fooey command line:

FOO=bar

$(warning $(FOO) $(origin FOO))

Here $(origin) says command line.  But if FOO=bar were override FOO=bar in that Makefile the output would be: Makefile:3: bar override.

Confused?   There's actually a simple rule that enables you to figure this out (or you can just resort to $(origin)): the override directive beats the command line which beats environment overrides (-e option) which beats macros defined in a Makefile file which beats the original environment.

The Basics: Getting environment variables out of GNU Make

The environment GNU Make uses when it runs commands (such as commands in
any rules it is running) is the environment GNU Make started with plus any variables exported in the Makefile (plus a few it adds itself).

Consider this simple Makefile:

FOO=bar

all:

    @echo FOO is $$FOO

If you run this with FOO undefined in the environment you'll see the output FOO is.  Without specifically exported FOO into the environment its value is not set in the environment used when GNU Make runs the echo command for the all rule.

If FOO had been set to foo in the environment before GNU Make was run you would see the output FOO is bar.  FOO has picked up the value inside the Makefile, but has been placed in the environment because it was already present.

If you are not sure whether FOO is in the environment, but want to ensure that it does make its way into the environment used for commands use the export
directive (see [urlhttp://www.gnu.org/software/make/manual/html_node/Variables_002fRecursio...
for more on export). 

For example, we can ensure that FOO appears in the environment of sub-processes like this:

export FOO=bar

all:

    @echo FOO is $$FOO

Alternatively, you can just put export FOO on a line by itself. 

On the other hand, you can remove a variable from the environment with unexport.   To ensure that FOO is excluded from the sub-process environment (no matter whether it was set or not in the parent environment) do the following:

FOO=bar

unexport FOO

all:

    @echo FOO is $$FOO

Run that and you'll always see the output FOO is.

You might be wondering what happens if you export and unexport a variable.  The answer is that the last directive wins.

The export and unexport directives can also be used with target-specific variables to modify the environment just for a particular rule.  For example, here I'm setting FOO to just for all and bar for any other rule:

export FOO=bar

all: export FOO=just for all

all:

   @echo FOO is $$FOO

You can't remove FOO environment of a specific rule with a target-specifc unexport.  If you write all: unexport FOO you'll get an error.

I also said that GNU Make adds a number of variables to the sub-process environment.  Specifically, GNU Make adds MAKEFLAGS, MFLAGS and MAKELEVEL to the environment.  For more detail on those variables see http://www.gnu.org/software/autoconf/manual/make/Options_002fRecursion.html.

There are also ways to get every single Makefile variable exported (either by writing export on a line on its own, or by specifying .EXPORT_ALL_VARIABLES:), but these shotgun approaches are probably a bad idea as the sub-process environment will be filled with useless (and perhaps harmful) variables.

The Gotcha: $(shell) doesn't have the same environment as a sub-process

You might expect that the environment used by a call to $(shell) would be the same as that used in the execution of a rule's commands.  In fact, the environment used by $(shell) is exactly the same as the environment when GNU Make was started.

You can verify this with the following Makefile that gets the value of FOO from within a $(shell) and a rule (here I ran it with FOO set to foo in the parent environment):

export FOO=bar

$(warning $(shell printenv | grep FOO))

all:

   @printenv | grep FOO

That outputs:

Makefile:3: FOO=foo

FOO=bar

No matter what you do $(shell) gets the parent environment.

This is, in fact, a bug in GNU Make (it's bug #10593, see http://savannah.gnu.org/bugs/?10593 for details).  Part of the reason this hasn't been fixed is that the obvious solution ('just do whatever you do for a rule') has a rather nasty consequence.  Suppose, I do the following:

export FOO=$(shell echo fooey)

all:

    @echo FOO is $$FOO

What's the value of FOO in the rule for all?  In order to get the value of FOO in the environment for all it has to be expanded which results in a loop as FOO would have to be placed in the environment of the $(shell) which requires getting the value of FOO and so on...

GNU Make's developers opted for an easy way: they just don't even try to fix the bug (for the moment).

Given that this bug isn't going away for the moment, we need a work around.  Luckily, most decent shells have a way to set an environment variable inline.  So I can modify the first Makefile in this section to:

export FOO=bar

$(warning $(shell FOO=$(FOO) printenv | grep FOO))

all:

   @printenv | grep FOO

And get the desired result:

Makefile:3: FOO=bar

FOO=bar

This works by setting the value of FOO within the shell used for the $(shell) with the string FOO=$(FOO). Since the argument to $(shell) gets expanded before execution that string becomes FOO=bar (taking its value from the value of FOO set in the Makefile).

The technique works fine if there's just one extra variable needed in the environment, but if there are many it's problematic.  A more comprehensive solution to the problem is to write a replacement for the $(shell) command that does export variables.   Here's env_shell that does just that:

env_file = /tmp/env

env_shell
= $(shell rm -f $(env_file))$(foreach V,$1,$(shell echo export $V=$($V)
>> $(env_file)))$(shell echo '$2' >> $(env_file))$(shell
/bin/bash -e $(env_file))

Before explaining how it works I'll use it to modify the Makefile above.  All that's necessary is to changing the $(shell) to $(call env_shell).  The first argument of env_shell
is the list of variables that need to be added to the environment and the second argument is that command to be executed.  Here's the updated Makefile with FOO exported:

export FOO=bar
 
$(warning $(call env_shell,FOO,printenv | grep FOO))

all:

   @printenv | grep FOO

And when run you'll see the output:

Makefile:3: FOO=bar

FOO=bar

Now back to how it works.  It's very simple.  env_shell makes a shell script that adds all the variables from its first argument to the environment and then executes the command.  By default the shell script is stored in the file named in the env_file macro (which was set to /tmp/env above).

In the example above /tmp/env ends up containing:

export FOO=bar

printenv | grep FOO

When you call env_shell it first deletes /tmp/env (that's the $(shell rm -f $(env_file)) part).  Next it adds to that file lines containing the definition of each of the variables named in the first argument, $1 (the $(foreach V,$1,$(shell echo export $V=$($V) >> $(env_file))) loop does the work).

Finally, it appends the actual command to execute (which was in the second argument $2 (the $(shell echo '$2' >> $(env_file)) bit) and then runs the /tmp/env file with a call to the shell (in this case I used bash) with the -e option (the $(shell /bin/bash -e $(env_file)) bit).

It's not perfect (because it would be nice if it just figured out what
should be in the environment), but it's a workable solution until GNU
Make's coders fix the bug.

About the author

CMCrossroads is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.