400

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.

3
  • Strange back ticks around the command work for executing other scripts for me in a Makefile. Might be something else. Commented Jan 13, 2014 at 15:50
  • @LeifGruenwoldt that's likely a coincidence. Make is copying your backticks to your shell and your shell is interpreting them - see stackoverflow.com/questions/60628832/… 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. Commented Oct 14, 2022 at 14:47
  • MY_VAR != python3 -c "print('hi')" . In GNU Make at least Commented Nov 8, 2022 at 23:57

8 Answers 8

546

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)
Sign up to request clarification or add additional context in comments.

14 Comments

shell is not a standard Make builtin command. This is a GNU Make builtin.
stackoverflow.com/a/2373111/12916 adds an important note about escaping $.
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
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.
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?
|
76

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.

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;

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 to the inside of the recipes when possible.

2 Comments

This doesn't work for me doing MY_ID=$(GENERATE_ID), dependent on intent. If I want the output of a command to get stored, then what works is MY_ID=`echo foobar`, and that saves "foobar" to MY_ID. Then the rest of these suggestions work. Wonder if there's some OS dependence here as well.
I could not get the formatting to work. make sure to escape dollar sign symbol in makefile with dollar dollar. Then "dollar dollar (my command)" would work.
62

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)

3 Comments

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.
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.
+100 for a simple, to the point explanation
45

Wrapping the assignment in an eval is working for me.

set_opts:
  $(eval DOCKER_OPTS = -v $(shell mktemp -d -p /scratch):/output)

# tell Make that the set_opts rule shouldn't be
# looking for a physical file called "set_opts"
.PHONY: set_opts

4 Comments

"Note: The @true here prevents Make from thinking there's nothing to be done." Um, that's what .PHONY always make these targets is for.
Thank you! This helps me bypass "weird bash" in makefile
I thought .PHONY is supposed to be used the other way around: gnu.org/software/make/manual/html_node/Phony-Targets.html
@CiprianTomoiagă Fixed :-)
40

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)

3 Comments

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}'"))
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).
Can you explain why this syntax is needed?
30

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.

3 Comments

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).
Oh I did not think of that. Atom here.
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.
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

Comments

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

2 Comments

not for target
works on Linux, but does not work on my Mac M3 using GNU Make 3.81

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.