4

Consider the following:

$ cat a.sh
#!/bin/sh
echo in a.sh, BANANA=$BANANA
$ cat Makefile
.PHONY: foo
export BANANA = I am a banana

foo:
        $(eval F=`./a.sh`)  # BANANA is set in a.sh
        echo $F
        $(eval G=$(shell ./a.sh))  # BANANA is *not* set in a.sh
        echo $G
$ make
# BANANA is set in a.sh
echo `./a.sh`
in a.sh, BANANA=I am a banana
# BANANA is *not* set in a.sh
echo in a.sh, BANANA=
in a.sh, BANANA=

As demonstrated, the export directive to Make tells make to set the variable BANANA in the environment of its children. But that setting does not apply to the shell function. It does seem to apply to the backticks. Is this a bug? How can I easily set make variables to the environment of the shell function?

Note:

$ make --version
GNU Make 4.0
Built for x86_64-unknown-linux-gnu
Copyright (C) 1988-2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
blitzen9872
  • 320
  • 1
  • 7
  • Well, I see why the backticks work, so that's a red-herring. – blitzen9872 Mar 09 '17 at 23:20
  • I guess there's no answer to the question of whether this is a bug or not as `$(shell)` is a gmake-only thing; `export`, OTOH, is under consideration to become a POSIX make feature (http://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html); my guess why BANANA isn't exported here is that it goes through two levels of child process creation, and isn't exported from the direct child process of the main make process to the ultimate `a.sh` child-child process – imhotap Mar 09 '17 at 23:37
  • I suspect you're doing some other make stuff wrong. If all you really want to convince yourself of is that the `export` really works, you've put too many moving parts into your example. Just have the `foo` rule do one thing which is run a.sh. (Not with `eval` or `$(shell)`, just run it.) Perhaps you can have a.sh echo "$BANANA" to a file. You will then see that the export works. (Perhaps you will have other make issues, but you can similarly break each of them down.) – Mort Mar 10 '17 at 03:12

1 Answers1

3

First, be clear about the meaning of your recipe:

foo:
    $(eval F=`./a.sh`)  # BANANA is set in a.sh
    echo $F
    $(eval G=$(shell ./a.sh))  # BANANA is *not* set in a.sh
    echo $G

This recipe contains two (not four) commands:

foo:
    echo $F
    echo $G

and the two make-functions:

    $(eval F=`./a.sh`)
    $(eval G=$(shell ./a.sh))

will be evaluated, in that order, for the scope of the two-line recipe when make decides to run it. If you are surpised by this point, read this question and answer.

Be clear also that F and G are both make variables, not shell variables. You only get away with referring to $F and $G rather than $(F) and $(G) thanks to the last para of 6.1 Basics of Variable References

A dollar sign followed by a character other than a dollar sign, open-parenthesis or open-brace treats that single character as the variable name. Thus, you could reference the variable x with ‘$x’. However, this practice is strongly discouraged, except in the case of the automatic variables

It wouldn't work for, say, FF and GG.

So the normal way to write your makefile would be:

.PHONY: foo
export BANANA = I am a banana


foo: F=`./a.sh`
foo: G=$(shell ./a.sh)
foo:
    echo $(F)
    echo $(G)

which has exactly the same effect.

And this perhaps clarifies the difference between the output of echo $(F) and echo $(G).

$(shell ./a.sh) invokes a make function that executes ./a.sh in a shell directly spawned by make and returns the stdout of so doing. Thus for target foo, make-variable G will be defined as the stdout of executing ./a.sh in a child shell of make.

`./a.sh` does not invoke any make-function. As far as make is concerned, it is just a string. For the target foo, make-variable F will be defined as `./a.sh `

The exported make-variable BANANA is not injected into the environment of a shell spawned by $(shell ...). 5.7.2 Communicating Variables to a Sub-make

To pass down, or export, a variable, make adds the variable and its value to the environment for running each line of the recipe

An exported variable and its definition is only injected into the environments of the shells that run the lines of recipes.

Thus BANANA is not defined in the environment of a.sh when it is run by $(shell ./a.sh) to generate the definition of G. But it is defined in the environment of the shell that that runs the recipe line echo $(F), with $(F) = `a.sh`. That shell (not make) interprets `a.sh` as a back-tick invocation of a subshell, which inherits the definition of BANANA.

To get BANANA exported into the environment of $(shell ...), you have to do it yourself since it is not done by make:

G=$(shell export BANANA='$(BANANA)'; ./a.sh)
Community
  • 1
  • 1
Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182
  • Thanks, Mike. This is a well written description. Unfortunately, the explicit export of variables in each invocation of `shell` is exactly the problem I'm trying to solve. I find the documentation for make dishonest, where it states in reference to .EXPORT_ALL_VARIABLES: `Simply by being mentioned as a target, this tells 'make' to export all variables to child processes by default.` A shell invoked by `shell` is certainly a child process! – blitzen9872 Mar 11 '17 at 00:31
  • 1
    @blitzen9872 gnu make is deceptively complicated. The more you learn about it the more nuanced you realize it is. The manual cannot footnote every single statement with a proviso to go and read a previous or subsequent chapter. – Mort Mar 11 '17 at 02:40
  • @Mort I'm not asking the documentation to reference a previous chapter, I'm pointing out that "this tells 'make' to export all variables to child processes by default" is inaccurate. If there is another chapter that makes that correct, I would love a reference to it! – blitzen9872 Mar 24 '17 at 20:15