ChatGPT解决这个技术问题 Extra ChatGPT

How to assign the output of a command to a Makefile variable

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.

Strange back ticks around the command work for executing other scripts for me in a Makefile. Might be something else.

O
OneCricketeer

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)

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?
J
Juraj

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.


V
Victor Sergienko

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)

"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
M
Mitchell Tracy

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)

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
F
Francesco Casula

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)

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?
A
AlanSE

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.


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.
A
Amritpal Singh

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

P
Poniros

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