How to get "at most once" semantics in variable assignments? - makefile

Shell commands sometimes take a long time to run, so you may not want to do VAR = $(shell slow-cmd) (with =, the slow-cmd will be run every time the variable is referenced). Using VAR := $(shell slow-cmd) can be useful, but if you are building a target that does not ever need the variable expanded, you will get one more invocation of the slow-cmd than is needed. In the following makefile (with gnu-make), you can get the desired behavior: the shell command to define a value for V2 is never invoked more than once, and for the target foo it is not invoked at all. But this is a heinous kludge. Is there a more reasonable way to ensure that a variable is only defined when needed, but never evaluated more than once?
V1 = $(shell echo evaluating V1 > /dev/tty; echo V1 VALUE)
all: foo bar V2
#echo $(V1) $#
#echo $(V2) $#
foo:
#echo $(V1) $#
bar: V2
#echo $(V1) $#
#echo $(V2) $#
V2:
$(eval V2 := $(shell echo evaluating V2 > /dev/tty; echo V2 VALUE))
.PHONY: all foo bar

There's no way to do it without tricks, but there's a cleaner way (maybe) than you're using. You can use:
V1 = $(eval V1 := $$(shell some-comand))$(V1)
For more details and explanation of exactly how this works see this page.

Target-specific deferred variables are an option:
host> cat Makefile
foo: VFOO = $(shell echo "VFOO" >> log.txt; echo "VFOO")
foo:
#echo '$(VFOO)' > $#
bar: VBAR = $(shell echo "VBAR" >> log.txt; echo "VBAR")
bar:
#echo '$(VBAR)' > $#
host> make foo
host> cat log.txt
VFOO
host> make foo
make: 'foo' is up to date.
host> cat log.txt
VFOO
host> make bar
host> cat log.txt
VFOO
VBAR
host> make bar
make: 'bar' is up to date.
host> cat log.txt
VFOO
VBAR

Related

How can I have a Makefile target update dependent on the value of an environment variable?

I have a make target that will have different output depending on the value of an environment variable.
How can I:
skip the dependency and not re-make the target if the environment variable has not changed the last run
make or re-make the target if the environment variable is not set or has changed
I thought I could create or conditionally update a file with the current environment variable value and then use that file as a make dependency. I couldn't find an elegant way to do that with native tools. (sed -i always updated the file's timestamp, maybe awk is possible)
How about using a shell script to update a file that holds the variable value?
SHELL = /bin/bash
var_file := var.txt
var_name := NAME
is_var_updated = [[ ! -e $(var_file) ]] || [[ "$$(< $(var_file))" != "$($(var_name))" ]]
update_var_file = echo "$($(var_name))" > $(var_file)
$(shell $(is_var_updated) && $(update_var_file))
output.txt: $(var_file)
echo "Name is $$NAME" > $#
This works like this.
$ ls
Makefile
$ NAME=foo make
echo "Name is $NAME" > output.txt
$ NAME=foo make
make: `output.txt' is up to date.
$ NAME=bar make
echo "Name is $NAME" > output.txt
Make conditionals could be a starting point:
.PHONY: all
FILE := foobar
ifdef ENV_VAR
OLD_ENV_VAR := $(shell [ -f $(FILE) ] && cat $(FILE))
ifeq ($(ENV_VAR),$(OLD_ENV_VAR))
DONTRUN := 1
endif
endif
ifdef DONTRUN
all:
#echo 'ENV_VAR unmodified'
else
$(shell printenv ENV_VAR > $(FILE))
all:
#echo 'ENV_VAR undefined or modified'
endif

Makefile test if variable is not empty

In a makefile I'm trying to
run a shell command and capture the output in a make variable
do something if the variable is not empty
I've created this simplified makefile to demonstrate my problem. Neither make a or make b executes the body of the if, I don't understand why not.
.PHONY: a b
a:
$(eval MY_VAR = $(shell echo whatever))
#echo MY_VAR is $(MY_VAR)
$(info $(MY_VAR))
ifneq ($(strip $(MY_VAR)),)
#echo "should be executed"
endif
#echo done
b:
$(eval MY_VAR = $(shell echo ''))
#echo MY_VAR is $(MY_VAR)
$(info $(MY_VAR))
ifneq ($(strip $(MY_VAR)),)
#echo "should not be executed"
endif
#echo done
I'm using
$ make --version
GNU Make 3.81
Edit: as pointed out, the vars don't need to be make vars
If you want to dynamically test the content of MY_VAR, you may have to :
a:
$(eval MY_VAR = $(shell echo ''))
$(if $(strip $(MY_VAR)),echo ok,echo no)
if evaluation will become echo ok if MY_VAR is not empty, otherwise it will become echo no
Note that, due to the time of evaluation, make conditionals (ifeq, ifneq...) cannot be used in recipes the way you tried. Use shell conditionals, instead, as shown below.
As your MY_VAR variable is used only in recipes, is target-dependent and you want it to be computed only when needed, why don't you use shell variables, instead of make variables?
$ cat Makefile
.PHONY: a b
a:
MY_VAR=$$(echo 'whatever') && \
echo '$#: MY_VAR is $$MY_VAR' && \
if [ -n "$$MY_VAR" ]; then \
echo '$#: should be executed'; \
fi && \
echo '$#: done'
b:
MY_VAR=$$(echo '') && \
echo '$#: MY_VAR is $$MY_VAR' && \
if [ -n "$$MY_VAR" ]; then \
echo '$#: should not be executed'; \
fi && \
echo '$#: done'
$ make a
a: MY_VAR is whatever
a: should be executed
a: done
$ make b
b: MY_VAR is
b: done
In case you absolutely need MY_VAR to be a target-specific make variable, but want to execute only once (per target) the shell command that produces its value, MadScientist has a wonderful trick that you should probably look at. Applied to your case, it should look like:
$ make --version
GNU Make 4.1
...
$ cat Makefile
a: MY_VAR = $(eval a: MY_VAR := $$(shell echo 'whatever'))$(MY_VAR)
b: MY_VAR = $(eval b: MY_VAR := $$(shell echo ''))$(MY_VAR)
a:
#echo '$#: MY_VAR is $(MY_VAR)' && \
if [ -n "$(MY_VAR)" ]; then \
echo '$#: should be executed'; \
fi && \
echo '$#: done'
b:
#echo '$#: MY_VAR is $(MY_VAR)' && \
if [ -n "$(MY_VAR)" ]; then \
echo '$#: should not be executed'; \
fi && \
echo '$#: done'
$ make a
a: MY_VAR is whatever
a: should be executed
a: done
$ make b
b: MY_VAR is
b: done
$ make b a
b: MY_VAR is
b: done
a: MY_VAR is whatever
a: should be executed
a: done
It may look extremely strange but it guarantees that MY_VAR is computed if and only if targets a or b are invoked, and only at most once for each. Have a look at MadScientist's post for detailed explanations. Go, it's brilliant.
The ifeq and family of conditionals are evaluated when parsing the Makefile. If you want a conditional for a Make variable when expanding a rule, you'll want to use the $(if ) function:
.PHONY: a b
a b:
#$(if $(strip $(MY_VAR)),echo "MY_VAR isn't empty",)
#echo done
a: MY_VAR =
b: MY_VAR = something
A bit left field, but was useful in my case so I guess it's worth a share: pipe the value to xargs with the --no-run-if-empty option:
echo $(POSSIBLY_EMPTY) | xargs --no-run-if-empty echo
Note that this will not work on OSX. See How to ignore xargs commands if stdin input is empty? for more details on xargs --no-run-if-empty

Remove target from MAKECMDGOALS?

I have the following in my makefile. Its a GNUmakefile, so the additional make features are supported:
# Undefined Behavior Sanitzier (Clang and G++)
ifeq ($(findstring ubsan,$(MAKECMDGOALS)),ubsan)
CXXFLAGS += -fsanitize=undefined
MAKECMDGOALS := $(subst ubsan,,$(MAKECMDGOALS))
endif # UBsan
Running it results in:
make ubsan
make: *** No rule to make target 'ubsan'. Stop.
I know the code path is being executed (by introducing an error in the block). How do I remove the target from the command goals?
You cannot do what you want here as far as I know.
You can modify the variable value and you will see the changes if you check the value yourself but modifying the value of MAKECMDGOALS will not affect make in any way.
If you look at Appendix A Quick Reference in the GNU Make Manual you will see that it says:
MAKECMDGOALS
The targets given to make on the command line. Setting this variable has no effect on the operation of make.
See Arguments to Specify the Goals.
The closest you could get to what you are trying to do here, I think, would be to re-execute make on the Makefile (or whatever) manually.
That being said this sort of thing is probably better done as variables instead of targets.
$ cat Makefile
$(info $(origin UBSAN))
$ make
undefined
$ make UBSAN=
command line
So something like this.
# Undefined Behavior Sanitzier (Clang and G++)
ifneq (undefined,$(origin UBSAN))
CXXFLAGS += -fsanitize=undefined
endif # UBsan
Yes, you can. Based on my recent answer to this other question Force Makefile to execute script after building any target (just before exiting), it was very easy to do what you would like.
This has only a drawback, you will lose any command-line arguments you pass directly to make as make all --silent. However, you can look into the question: How to detect if the makefile `--silent/--quiet` command line option was set?, and recapture the command-line arguments you would like to repass to the second make call command on make -f ${MAKEFILE_JUSTNAME} ${MAKECMDGOALS}.
ECHOCMD:=/bin/echo -e
SHELL := /bin/bash
ifeq (${IS_MAKEFILE_RUNNING_TARGETS},)
MAKEFILE_JUSTNAME := $(firstword ${MAKEFILE_LIST})
# UBsan
ifeq ($(findstring ubsan,${MAKECMDGOALS}),ubsan)
MAKECMDGOALS := $(subst ubsan,,${MAKECMDGOALS})
USELESS := $(eval export IS_UBSAN_CXXFLAGS_SET=1)
endif
define DEFAULTTARGET :=
printf 'Calling "%s" "%s"\n\n' "${MAKEFILE_JUSTNAME}" "${MAKECMDGOALS}"
make -f ${MAKEFILE_JUSTNAME} ${MAKECMDGOALS}
printf '\n'
printf 'Running something after all rules finished\n'
endef
%:
#:
# printf 'IS_MAKEFILE_RUNNING_TARGETS="%s"\n' "${IS_MAKEFILE_RUNNING_TARGETS}"
$(if ${IS_MAKEFILE_RUNNING_TARGETS},,${DEFAULTTARGET})
$(eval export IS_MAKEFILE_RUNNING_TARGETS=1)
all:
#:
# printf 'IS_MAKEFILE_RUNNING_TARGETS="%s"\n' "${IS_MAKEFILE_RUNNING_TARGETS}"
$(if ${IS_MAKEFILE_RUNNING_TARGETS},,${DEFAULTTARGET})
$(eval export IS_MAKEFILE_RUNNING_TARGETS=1)
else
# UBsan
ifneq (${IS_UBSAN_CXXFLAGS_SET},)
CXXFLAGS += -fsanitize=undefined
endif
all:
printf 'Calling my all rule, CXXFLAGS="%s"\n' "${CXXFLAGS}"
foo:
printf 'Calling my foo rule, CXXFLAGS="%s"\n' "${CXXFLAGS}"
bar:
printf 'Calling my bar rule, CXXFLAGS="%s"\n' "${CXXFLAGS}"
xyzzy:
printf 'Calling my xyzzy rule, CXXFLAGS="%s"\n' "${CXXFLAGS}"
endif
Usage examples:
make
printf 'Calling "%s" "%s"\n\n' "Makefile" ""
Calling "Makefile" ""
make -f Makefile
make[1]: Entering directory '/cygdrive/d/User/Downloads'
printf 'Calling my all rule, CXXFLAGS="%s"\n' ""
Calling my all rule, CXXFLAGS=""
make[1]: Leaving directory '/cygdrive/d/User/Downloads'
printf '\n'
printf 'Running something after all rules finished\n'
Running something after all rules finished
make foo bar
printf 'Calling "%s" "%s"\n\n' "Makefile" "foo bar"
Calling "Makefile" "foo bar"
make -f Makefile foo bar
make[1]: Entering directory '/cygdrive/d/User/Downloads'
printf 'Calling my foo rule, CXXFLAGS="%s"\n' ""
Calling my foo rule, CXXFLAGS=""
printf 'Calling my bar rule, CXXFLAGS="%s"\n' ""
Calling my bar rule, CXXFLAGS=""
make[1]: Leaving directory '/cygdrive/d/User/Downloads'
printf '\n'
printf 'Running something after all rules finished\n'
Running something after all rules finished
make foo bar ubsan
printf 'Calling "%s" "%s"\n\n' "Makefile" "foo bar "
Calling "Makefile" "foo bar "
make -f Makefile foo bar
make[1]: Entering directory '/cygdrive/d/User/Downloads'
printf 'Calling my foo rule, CXXFLAGS="%s"\n' "-fsanitize=undefined"
Calling my foo rule, CXXFLAGS="-fsanitize=undefined"
printf 'Calling my bar rule, CXXFLAGS="%s"\n' "-fsanitize=undefined"
Calling my bar rule, CXXFLAGS="-fsanitize=undefined"
make[1]: Leaving directory '/cygdrive/d/User/Downloads'
printf '\n'
printf 'Running something after all rules finished\n'
Running something after all rules finished
I am using the following pattern to pass parameters around, it has its limitations (only [a-z,0-9] characters) but can become quite handy:
ifeq (console,$(firstword $(MAKECMDGOALS)))
CONSOLE_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS))
$(eval $(CONSOLE_ARGS):;#:)
endif
console::
run-some-command $(CONSOLE_ARGS)
For your case the following works:
# Undefined Behavior Sanitzier (Clang and G++)
ifeq (ubsan,$(firstword $(MAKECMDGOALS)))
CXXFLAGS += -fsanitize=undefined
$(eval $(CONSOLE_ARGS):;#:)
endif # UBsan

Force variable expansion once and when used only

In this example, I have a process that takes some time. Let's says 1 second. If I write the following Makefile, FOO will be expanded 3 times for make all and none for make clean.
If I want to save some execution time for all I can assign FOO using := instead of =. However this will cause FOO to be expanded for the target clean even if it doesn't use it.
FOO = $(shell echo -e "+1" >> foo && echo "Hello" && sleep 1)
all:
#echo $(FOO)
#echo $(FOO)
#echo $(FOO)
#cat foo
clean:
rm foo
The output:
$ make
Hello
Hello
Hello
+1
+1
+1
I would like to force Make to expand a variable only once only if required.
Is it possible to do it so?
The "best" solution I can come up with looks like this:
$ cat coin.mk
FOO = FOO:=$(shell echo -e "+1" >> foo && echo "Hello" && sleep 1)
defined = $(and $(filter-out undefined,$(origin $1)),$($1))
all:
echo $(eval $(FOO))$(FOO)
echo $(FOO)
echo $(FOO)
cat foo
clean:
rm foo
$ time make -f coin.mk clean
rm foo
rm: cannot remove `foo': No such file or directory
make: *** [clean] Error 1
real 0m0.003s
user 0m0.003s
sys 0m0.000s
$ time make -f coin.mk
echo Hello
Hello
echo Hello
Hello
echo Hello
Hello
cat foo
+1
real 0m1.009s
user 0m0.002s
sys 0m0.004s
$ time make -f coin.mk clean
rm foo
real 0m0.003s
user 0m0.000s
sys 0m0.003s
Which works but requires special-casing the first use of the variable in the make run ($(eval $(FOO)) run a second time will cause a make error).
I tried briefly to encapsulate the eval logic inside the value of FOO but most attempts were blocked by make complaining that *** Recursive variable 'FOO' references itself (eventually). Stop.
Try this:
FOO = $(eval FOO := $(shell echo "+1" >> foo && echo "Hello" && sleep 1))$(value FOO)
The first time make expands $(FOO) it will first expand the eval, which resets the variable FOO using :=. Then it resolves the value of the FOO variable. In subsequent expansions, due to the eval, FOO expands directly to the value.
I should also point out that if you have at least GNU make 4.0 you can use a new feature added to the POSIX standard for make recently, the != operator:
FOO != echo "+1" >> foo && echo "Hello" && sleep 1
which does exactly what you want here.

reading variable value declared in script.sh in Makefile

I run makefile to generate an image file for a target device. After I burn the image into the target device during one of the operation funtion1.sh calls script.sh where my VAR is declared.
I want during running Makefile to generate the target image access script.sh knowing the path, read the value of VAR and use it in Makefile.
example:
script.sh:
...
VAR=some_value
...
=====Now what script do I need for Makefile ???===============
-I tried this method but it did not work--------------------------
Makefile:
PLAT_SCRIPT := /path/to/script.sh
PLAT_VAR := VAR
PLAT_SCRIPT_TEXT := $(shell grep ${PLAT_VAR} ${PLAT_SCRIPT}) VAR := $(filter-out ${PLAT_VAR}, $(strip $(subst =, , $(subst ",, $(strip ${PLAT_SCRIPT_TEXT})))))
all:
#echo VAR=$(VAR)
It did not work for some reason. Maybe I should replace line 4 with:
VAR := $(shell echo $(PLAT_SCRIPT_TEXT)|cut -d, -f1|awk -F'=' '{print $2 }' )
all:
#echo VAR=$(VAR)
You must export the variable to make it visible in subprocess.
exporting variable from Makefile to bash script:
export variable := Stop
all:
/path/to/script.sh
or export it using shell style:
all:
variable=Stop /path/to/script.sh
exporting variable from shell to make:
export variable=Stop
make -C path/to/dir/with/makefile
or:
variable=Stop make -C path/to/dir/with/makefile
or:
make -C path/to/dir/with/makefile variable=Stop
If you need to read variable from script you can find it's declaration and extract the value like that:
script.sh:
...
VAR=some_value
...
Makefile:
VAR := $(shell sed -n '/VAR=/s/^.*=//p' script1.sh)
all:
#echo VAR=$(VAR)
But, think this is not a very good method.
Better is to output results of execution in the script and fetch it in Makefile.
Example:
script.sh:
#!/bin/bash
VAR=some_value
# some work here
echo "some useful output here"
# outputting result with the variable to use it in Makefile
echo "result: $VAR"
Makefile:
# start script and fetch the value
VAR := $(shell ./script.sh | sed -n '/^result: /s/^.*: //p')
all:
#echo VAR=$(VAR)

Resources