362

I need to execute some make rules conditionally, only if the Python installed is greater than a certain version (say 2.5).

I thought I could do something like executing:

python -c 'import sys; print int(sys.version_info >= (2,5))'

and then using the output ('1' if ok, '0' otherwise) in a ifeq make statement.

In a simple bash shell script it's just:

MY_VAR=`python -c 'import sys; print int(sys.version_info >= (2,5))'`

but that doesn't work in a Makefile.

Any suggestions? I could use any other sensible workaround to achieve this.

tshepang
  • 12,111
  • 21
  • 91
  • 136
fortran
  • 74,053
  • 25
  • 135
  • 175
  • Strange back ticks around the command work for executing other scripts for me in a Makefile. Might be something else. – Leif Gruenwoldt Jan 13 '14 at 15:50
  • @LeifGruenwoldt that's likely a coincidence. Make is copying your backticks to your shell and your shell is interpreting them - see https://stackoverflow.com/questions/60628832/does-make-not-support-backtick-notation That works often but is dangerous because if you use the variable inside a shell quoted environment it *may* not get executed. Use the answer from below in preference. – Michael Oct 14 '22 at 14:47
  • `MY_VAR != python3 -c "print('hi')"` . In GNU Make at least – Neil McGuigan Nov 08 '22 at 23:57

8 Answers8

502

Use the Make shell builtin like in MY_VAR=$(shell echo whatever)

me@Zack:~$make
MY_VAR IS whatever

me@Zack:~$ cat Makefile 
MY_VAR := $(shell echo whatever)

all:
    @echo MY_VAR IS $(MY_VAR)
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
Arkaitz Jimenez
  • 22,500
  • 11
  • 75
  • 105
  • 47
    shell is not a standard Make builtin command. This is a GNU Make builtin. – Dereckson Feb 18 '14 at 20:17
  • 21
    http://stackoverflow.com/a/2373111/12916 adds an important note about escaping `$`. – Jesse Glick Aug 25 '14 at 22:27
  • 9
    This simple example works. It also works with shell commands pipeline. But it is essential that you should use $$ to represent $ in the shell command – Sergey P. aka azure Oct 07 '14 at 09:12
  • 41
    While question is mildly old, it's best to do MY_VAR := $(shell ...), otherwise every time MY_VAR is evaluated, it'll execute $(shell ...) again. – Russ Schultz Aug 25 '15 at 15:57
  • I had a space between shell and the opening parentheses and only after removing the space did my makefile output text – Peter Chaula Sep 28 '17 at 15:51
  • 3
    Replace `shell echo whatever` with `python -c 'import sys; print int(sys.version_info >= (2,5))'`. You get "syntax error near unexpected token". I can't understand how anyone thought this answered the question. Can anyone please explain what I'm missing? – AlanSE Oct 23 '17 at 16:10
  • Add an answer that deals with the inexplicable errors that I referenced above. – AlanSE Oct 23 '17 at 16:32
  • 1
    If you use `$(MY_VAR)` again in another command within that target will it run `echo whatever` again or use the output from the last command? Because I need it to only run once. – jshbrntt Jan 11 '18 at 08:56
  • @AlanSE pointed out that it only works if the last line starts with a **leading tab** (not leading spaces). If you copy and paste, **depending on your editor's behavior** you may get spaces. E.g. pasting into the Eclipse Makefile editor produced a leading tab. – Technophile Jun 06 '18 at 20:10
  • 1
    @RussSchultz Thanks for mentioning the re-execution if `=` is used instead of `:=`. This had me puzzled now as I am using a non-deterministic command (`mktemp`). – exhuma Oct 29 '18 at 09:37
  • 1
    @Technophile That's *`make(1)`* itself; tabs are required rather than spaces (in targets at least). – Pryftan Jan 03 '19 at 13:24
  • @RussSchultz True but then if you follow it by a += it will append to the variable, just as a note of interest (very useful for e.g. CFLAGS because you can depending on build directory, for example, have different build flags). – Pryftan Jan 03 '19 at 13:25
  • @Pryftan yes I know it's make's required input format; my comment was about the behavior of text editors. – Technophile Jan 11 '19 at 01:17
  • 1
    = is right if you only use that variable in one target, if you use := then no matter what target you run the variable is always evaluated – lfmunoz Apr 25 '19 at 03:23
61

Beware of recipes like this

target:
    MY_ID=$(GENERATE_ID);
    echo $MY_ID;

It does two things wrong. The first line in the recipe is executed in a separate shell instance from the second line. The variable is lost in the meantime. Second thing wrong is that the $ is not escaped.

target:
    MY_ID=$(GENERATE_ID); \
    echo $$MY_ID;

Both problems have been fixed and the variable is useable. The backslash combines both lines to run in one single shell, hence the setting of the variable and the reading of the variable afterwords, works.

I realize the original post said how to get the results of a shell command into a MAKE variable, and this answer shows how to get it into a shell variable. But other readers may benefit.

One final improvement, if the consumer expects an "environment variable" to be set, then you have to export it.

my_shell_script
    echo $MY_ID

would need this in the makefile

target:
    export MY_ID=$(GENERATE_ID); \
    ./my_shell_script;

Hope that helps someone. In general, one should avoid doing any real work outside of recipes, because if someone use the makefile with '--dry-run' option, to only SEE what it will do, it won't have any undesirable side effects. Every $(shell) call is evaluated at compile time and some real work could accidentally be done. Better to leave the real work, like generating ids, to the inside of the recipes when possible.

Juraj
  • 1,120
  • 9
  • 10
47

With GNU Make, you can use shell and eval to store, run, and assign output from arbitrary command line invocations. The difference between the example below and those which use := is the := assignment happens once (when it is encountered) and for all. Recursively expanded variables set with = are a bit more "lazy"; references to other variables remain until the variable itself is referenced, and the subsequent recursive expansion takes place each time the variable is referenced, which is desirable for making "consistent, callable, snippets". See the manual on setting variables for more info.

# Generate a random number.
# This is not run initially.
GENERATE_ID = $(shell od -vAn -N2 -tu2 < /dev/urandom)

# Generate a random number, and assign it to MY_ID
# This is not run initially.
SET_ID = $(eval MY_ID=$(GENERATE_ID))

# You can use .PHONY to tell make that we aren't building a target output file
.PHONY: mytarget
mytarget:
# This is empty when we begin
    @echo $(MY_ID)
# This recursively expands SET_ID, which calls the shell command and sets MY_ID
    $(SET_ID)
# This will now be a random number
    @echo $(MY_ID)
# Recursively expand SET_ID again, which calls the shell command (again) and sets MY_ID (again)
    $(SET_ID)
# This will now be a different random number
    @echo $(MY_ID)
Mitchell Tracy
  • 1,395
  • 1
  • 15
  • 16
  • You have the first nice explanation I've every come across. Thank you. Though one thing to add is that if `$(SET_ID)` lives inside of an `if` clause _that is false_ then _it is still called_. – Roman Nov 20 '19 at 03:06
  • 2
    It is still called because the if stmts are evaluated at makefile compile-time not at run-time. For run-time specific instructions, put them in recipes. If they are conditional, add if stmts, written in bash / shell, as part of the recipe. – Juraj Dec 05 '19 at 20:50
  • 1
    +100 for a simple, to the point explanation – Prakash P Dec 23 '21 at 13:12
43

Wrapping the assignment in an eval is working for me.

# dependency on .PHONY prevents Make from 
# thinking there's `nothing to be done`
set_opts: .PHONY
  $(eval DOCKER_OPTS = -v $(shell mktemp -d -p /scratch):/output)
Victor Sergienko
  • 13,115
  • 3
  • 57
  • 91
Dave
  • 431
  • 4
  • 2
39

Here's a bit more complicated example with piping and variable assignment inside recipe:

getpodname:
    # Getting pod name
    @eval $$(minikube docker-env) ;\
    $(eval PODNAME=$(shell sh -c "kubectl get pods | grep profile-posts-api | grep Running" | awk '{print $$1}'))
    echo $(PODNAME)
Francesco Casula
  • 26,184
  • 15
  • 132
  • 131
  • 3
    In case it ever comes handy I am using a bit similar approach to get `PODNAME` by deployment name: `$(eval PODNAME=$(shell sh -c "kubectl get pod -l app=sqlproxy -o jsonpath='{.items[0].metadata.name}'"))` – Jan Richter Aug 26 '18 at 19:22
  • 2
    The syntax confused me for a second until I realized you were using both the *shell builtin* eval (on the docker-env line) and the *make function* eval (on the next line). – Rag May 17 '20 at 04:42
  • Can you explain why this syntax is needed? – Edmondo Dec 28 '21 at 19:20
29

I'm writing an answer to increase visibility to the actual syntax that solves the problem. Unfortunately, what someone might see as trivial can become a very significant headache to someone looking for a simple answer to a reasonable question.

Put the following into the file "Makefile".

MY_VAR := $(shell python -c 'import sys; print int(sys.version_info >= (2,5))')

all:
    @echo MY_VAR IS $(MY_VAR)

The behavior you would like to see is the following (assuming you have recent python installed).

make
MY_VAR IS 1

If you copy and paste the above text into the Makefile, will you get this? Probably not. You will probably get an error like what is reported here:

makefile:4: *** missing separator. Stop

Why: Because although I personally used a genuine tab, Stack Overflow (attempting to be helpful) converts my tab into a number of spaces. You, frustrated internet citizen, now copy this, thinking that you now have the same text that I used. The make command, now reads the spaces and finds that the "all" command is incorrectly formatted. So copy the above text, paste it, and then convert the whitespace before "@echo" to a tab, and this example should, at last, hopefully, work for you.

AlanSE
  • 2,597
  • 2
  • 29
  • 22
  • Depends on what editor you are pasting into. I just copied and pasted into an Eclipse Makefile editor, and got a leading tab (as required). – Technophile Jun 06 '18 at 20:12
  • Oh I did not think of that. Atom here. – AlanSE Jun 06 '18 at 20:35
  • 1
    That's then because Eclipse converts these spaces into a tab, by recognizing the makefile syntax. In the SO web page, it's definitely spaces. – Gerhard Aug 03 '20 at 17:09
4

In the below example, I have stored the Makefile folder path to LOCAL_PKG_DIR and then use LOCAL_PKG_DIR variable in targets.

Makefile:

LOCAL_PKG_DIR := $(shell eval pwd)

.PHONY: print
print:
    @echo $(LOCAL_PKG_DIR)

Terminal output:

$ make print
/home/amrit/folder
Amritpal Singh
  • 4,496
  • 1
  • 10
  • 7
2

From the make manual

The shell assignment operator ‘!=’ can be used to execute a shell script and set a >variable to its output. This operator first evaluates the right-hand side, then passes >that result to the shell for execution. If the result of the execution ends in a >newline, that one newline is removed; all other newlines are replaced by spaces. The >resulting string is then placed into the named recursively-expanded variable. For >example:

hash != printf '\043'

file_list != find . -name '*.c'

source

Poniros
  • 131
  • 9