Makefile: exit on conditional - bash

I want to check that an environment variable is set before executing some code in a Makefile. If it's not set I want to throw an error with a simple error message:
run:
[ -z "$(MY_APP)" ] && echo "MY_APP must be set" && exit 1
echo "MY_APP is set. Yay!"
echo "Let's continue on with the command..."
When MY_APP is not set I get the following error, which is desired:
[ -z "" ] && echo "MY_APP must be set" && exit 1
MY_APP must be set
make: *** [run] Error 1
However, when MY_APP is set I get the following error:
[ -z "EXAMPLE_NAME" ] && echo "MY_APP must be set" && exit 1
make: *** [run] Error 1
Any idea what I'm doing wrong? And is there a better way to do this?

Recall that the && condition require that all conditions must be TRUE to pass. Since the first condition fail, the whole command will return a status of 1 (-> false), effectively stopping the make
You can use the following, so that the test will fail only when MY_APP is missing.
Note that I'm using false instead of exit 1. Also better to use "${MY_APP}", which make it easier to copy/paste from Make to shell prompt/script.
run:
{ [ -z "$(MY_APP)" ] && echo "MY_APP must be set" && false } || true
...
# Or just if-Then-Else
if [ -z "${MY_APP}" ] ; then echo "MY_APP must be set" ; false ; fi
...

You can test environment variables with Makefile conditional syntax, like this:
sometarget:
ifndef MY_APP
#echo "MY_APP environment variable missing"
exit 1
endif
somecommand to_run_if_my_app_is_set
Note that ifndef/ifdef operate on the name of the variable, not the variable itself.

It seems that you are trying to use a Makefile to run commands which are not building targets (the target name run is a giveaway). You already got bitten by one of Makefile and shells caveats. Makefile caveat: exit status is inspected after each line and if not zero abort immediately. Shell caveat: the test command ([) returns a non zero exit status so the entire line returns non zero.
The rule of thumb is: a recipe of a rule should create a filename named like the target of the rule.
Here is a rule (to clarify the terms):
target:
recipe command lines
should create file named target
There are some exceptions to this rule of thumb. Most notably make clean and make install. Both typically do not create files named clean or install. One can argue that make run maybe also be an exception to this rule of thumb.
If your run is as simple as a typical clean then I might agree about making an exception. But usually commands are run with command line arguments. Before long you will want make run accept arguments. And making make accept custom command line arguments is not fun at all.
You tried to manipulate the behaviour using environment variables which is somewhat less problematic than command line arguments. But still problematic enough to make you trip over a caveat.
My suggestion for a fix:
Put complex recipes in a shell script. There you have all the power and flexibility of a shell script without the awkwardness of makefiles. For example as explained here: Basic if else statement in Makefile
In case of a typical run target write a wrapper shell script around the makefile which lets the makefile rebuild the target and then run the target. For exampe as explained here: Passing arguments to "make run"

You can conditionally exit the Makefile using error control function, at least in the GNU version.
This snippet is a helpful condition to put into the head of the Makefile. It exits with a message of help, if make was not called from within the directory of the Makefile.
MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
ifneq (${MAKEFILE_DIR}, $(shell pwd))
INVALID_LOCATION:=`make` must be called from within ${MAKEFILE_DIR} (or with option -C ${MAKEFILE_DIR})
$(error ERROR: $(INVALID_LOCATION))
endif
See: https://www.gnu.org/software/make/manual/html_node/Make-Control-Functions.html
Useful in case your paths are relative to the Makefile and you don't want them to prefix with a base.

Related

How to change the return value of a `make` command

I have a number of makefiles that build and run tests. I would like to create a script that makes each one and notes whether the tests passed or failed. Though I can determine test status within each make file, I am having trouble finding a way to communicate that status to the caller of the make command.
My first thought is to somehow affect the return value of the make command, though this does not seem possible. Can I do this? Is there some other form of communication I can use to express the test status to the bash script that will be calling make? Perhaps by using environment variables?
Thanks
Edit: It seems that I cannot set the return code for make, so for the time being I will have to make the tests, run them in the calling script instead of the makefile, note the results, and then manually run a make clean. I appreciate everyone's assistance.
Make will only return one of the following according to the source
#define MAKE_SUCCESS 0
#define MAKE_TROUBLE 1
#define MAKE_FAILURE 2
MAKE_SUCCESS and MAKE_FAILURE should be self-explanatory; MAKE_TROUBLE is only returned when running make with the -q option.
That's pretty much all you get from make, there doesn't seem to be any way to set the return code.
The default behavior of make is to return failure and abandon any remaining targets if something failed.
for directory in */; do
if ( cd "$directory" && make ); then
echo "$0: Make in $directory succeeded" >&2
else
echo "$0: Make in $directory failed" >&2
fi
done
Simply ensure each test leaves its result in a file unique to that test. Least friction will be to create test.pass if thes test passes, otherwise create test.fail. At the end of the test run gather up all the files and generate a report.
This scheme has two advantages that I can see:
You can run the tests in parallel (You do us the -jn flag, don't you? (hint: it's the whole point of make))
You can use the result files to record whether the test needs to be re-run (standard culling of work (hint: this is nearly the whole point of make))
Assuming the tests are called test-blah where blah is any string, and that you have a list of tests in ${tests} (after all, you have just built them, so it's not an unreasonable assumption).
A sketch:
fail = ${#:%.pass=%.fail}
test-passes := $(addsuffix .pass,${tests})
${test-passes}: test-%.pass: test-%
rm -f ${fail}
touch $#
$* || mv $# ${fail}
.PHONY: all
all: ${test-passes}
all:
# Count the .pass files, and the .fail files
echo '$(words $(wildcard *.pass)) passes'
echo '$(words $(wildcard *.fail)) failures'
In more detail:
test-passes := $(addsuffix .pass,${tests})
If ${tests} contains test-1 test-2 (say), then ${test-passes} will be test-1.pass test-2.pass
${test-passes}: test-%.pass: test-%
You've just gotta love static pattern rules.
This says that the file test-1.pass depends on the file test-1. Similarly for test-2.pass.
If test-1.pass does not exist, or is older than the executable test-1, then make will run the recipe.
rm -f ${fail}
${fail} expands to the target with pass replaced by fail, or test-1.fail in this case. The -f ensures the rm returns no error in the case that the file does not exist.
touch $# — create the .pass file
$< || mv $# ${fail}
Here we run the executable
If it returns success, our work is finished
If it fails, the output file is deleted, and test-1.fail is put in its place
Either way, make sees no error
.PHONY: all — The all target is symbolic and is not a file
all: ${test-passes}
Before we run the recipe for all, we build and run all the tests
echo '$(words $(wildcard *.pass)) passes'
Before passing the text to the shell, make expands $(wildcard) into a list of pass files, and then counts the files with $(words). The shell gets the command echo 4 passes (say)
You run this with
$ make -j9 all
Make will keep 9 jobs running at once — lovely if you have 8 CPUs.

Allow Makefile both append and override target

I have base Makefile for all my services, in some cases I want to use my default "test" target, in other cases I want to override\add to it. These are the files I have so far (and obviously its not working as expect..).
MakefileBase
test:
./.../run-tests.sh
Makefile
BASE_FILE := /path/to/MakefileBase
include ${BASE_FILE}
test:
#$(MAKE) -f $(BASE_FILE) test # un/comment this line in order to run the default tests.
# echo "custom test"
When I run the test with the first line commented out I get the following
Makefile:10: warning: overriding commands for target `test'
/.../MakefileBase:63: warning: ignoring old commands for target `test'
echo "no tests"
no tests
except of the warning it works as expected, the problem is when I try to use the parent function then I get the following errors:
Makefile:9: warning: overriding commands for target `test'
/.../MakefileBase:63: warning: ignoring old commands for target `test'
make[1]: test: No such file or directory
make[1]: *** No rule to make target `test'. Stop.
make: *** [test] Error 2
Actually, both answers so far are wrong or incomplete:
exit 0 in a rule will just exit the current shell (which runs only the exit 0 command, so it is a no-op in this case). So this won't override.
It's not true that you cannot override a command without warning. If it is not necessary that both targets have the same name, you can do:
MakefileBase
.PHONY: test-base
test-base:
echo base
%: %-base # handles cases where you don't want to override
Makefile1
include MakefileBase
.PHONY: test
test:
echo override
Makefile
include MakefileBase
.PHONY: test
test: test-base
echo append
As with double colon rules, the effects of each targets (on each other) have to be considered, especially if you move away from .PHONY (for example, files considered up-to-date because the other rule just updated them).
BTW, I don't see the problem with your approach (aside from the warning). For me it worked fine.
This is what double-colon rules are for:
test::
./.../run-tests.sh
and:
BASE_FILE := /path/to/MakefileBase
include ${BASE_FILE}
test::
#$(MAKE) -f $(BASE_FILE) test
This will "add to" an existing target. There is no way to override a target with a different recipe without incurring a warning.
If you want to do that the only way is to use variables to hold the recipe then override the variable value. For example:
test_recipe = ./.../run-tests.sh
test:
$(test_recipe)
and:
BASE_FILE := /path/to/MakefileBase
include ${BASE_FILE}
test_recipe = #$(MAKE) -f $(BASE_FILE) test
Hacky, but you can get add, and a limited form of override that can never be deeper than one override. Both use double colon rules.
add: use double colons on both rules
override: use double colons on both rules, appending command exit 0 to the last rule
# "addcmd" echoes "BA", "overridecmd" echoes "B"
addcmd ::
echo "A"
addcmd ::
echo "B"
overridecmd ::
echo "A"
overridecmd ::
echo "B"
exit 0

How to abort makefile without error message

How can I abort makefile from continuing other targets, if certain condition is met in current target.
For example:
Step01:
## Do something
Step02: Step01_Output
## Check that Step01_output meet certain condition, otherwise, abort
Step03: Step02
## Do somethings (of course if did not abort in Step02)
# And so on
I tried using "exit" with status 0 ==> But it continues nevertheless!
I tried using "exit 1" or other exist status ==> It aborts, but gives error message at output.
I want to abort, but still not to give an error message at make calling shell.
I tried also to set env variable from Step02 and surround Step03 and after within if check like this:
ifneq ($(ToAbort),1)
Step03:
...
StepN
endif
Unfortunately, it seems that make did not even look at the condition or the variable value has not been transferred between targets.
Any ideas? May be through adding additional target or so?
I think make exits only when all the targets are created or any error arises.
By the way, commands are run in sub shells so that using`exit' will not cause make to exit.
Not sure your thinking is correct on this one.
Leave the culling of work up to make.
However,
you can express what you requested with a smattering of shell and recursive make (sorry).
You want to express "if a completes with/without error, then we are done, else carry on with b."
Step01:
command1
command2
command3 && ${MAKE} Step02
command4
Step02:
cmd5
cmd6 && ${MAKE} Step03
⋮
Note those ampersands.
if command3; then ${MAKE} Step02; fi does not do what you want.
If the make fails,
you do not want to continue into command4.
P.S. Don't forget to mark those steps as PHONY if they aren't real files.

Require an environment variable to be set in a Makefile target

I'm trying to require that an environment variable be set in a Makefile when running a specific target. I'm using the technique from the answer to this question, where you setup another target that will guarantee an environment variable to be set.
Mine looks like this:
require-%:
# if [ "${${*}}" = "" ]; then \
$(error You must pass the $* environment variable); \
fi
With that target setup, this is expected:
$ make require-FOO
Makefile:3: *** You must pass the FOO environment variable. Stop.
However, when testing, I can never get it to not error:
$ make require-FOO FOO=something
Makefile:3: *** You must pass the FOO environment variable. Stop.
$ make require-FOO FOO=true
Makefile:3: *** You must pass the FOO environment variable. Stop.
$ make require-FOO FOO='a string'
Makefile:3: *** You must pass the FOO environment variable. Stop.
Even when I comment out the if block in the target:
require-%:
# # if [ "${${*}}" = "" ]; then \
# $(error You must pass the $* environment variable); \
# fi
I still get an error when running it:
$ make require-FOO FOO=something
Makefile:3: *** You must pass the FOO environment variable. Stop.
What's am I doing wrong? How can I get this to work?
You modified the solution presented in that linked answer without understanding the difference.
The linked answer uses a shell echo and a shell exit to do the message output and exiting.
Your modification uses the make $(error) function.
The difference is that the shell commands only execute when the shell logic says they should but the make function executes before make runs the shell commands at all (and always expands/executes). (Even in shell comments because those are shell comments.)
If you want this asserted at shell time then you need to use shell constructs to test and exit. Like the original answer.
If you want this asserted at recipe expansion time then you need to use make constructs to test and exit. Like this (untested):
require-%:
#: $(if ${${*}},,$(error You must pass the $* environment variable))
#echo 'Had the variable (in make).'

Bad comportement while check error during makefile

I want to make automatically the documentation of my project with my makefile.
I also create a target doc (and a variable DOC_DIRECTORY = ../doc) to specify the directory of the documentation. In my doxygen file, I added a log file name "doxyLog.log" in the ../doc/ directory.
Here is my target definition :
#Creation of the Doxygen documentation
doc: $(DOC_DIRECTORY)/path_finder_doc
doxygen $(DOC_DIRECTORY)/path_finder_doc
#echo $(shell test -s ../doc/doxyLog.log; echo $$?)
ifeq ($(shell test -s ../doc/doxyLog.log; echo $$?),1)
#echo "Generation of the doxygen documentation done"
else
#echo "Error during the creation of the documentation, please check $(DOC_DIRECTORY)/doxyLog.log"
endif
To test if my check is working, I manually introduce an error in my documentation (a bad command like \retufjdkshrn instead of \return). But, when I launch the make doc, this error appears after the second time :
First make doc (with an error in the doc ) --> Generation of the doxygen documentation done
Second make doc (always the error in the doc) --> Error during the creation of the documentation, please check ../doc/doxyLog.log
I don't understand why, can someone help me please?
There appear to be two things wrong here, so parts of this answer must be guesswork.
First:
ifeq ($(shell test -s ../doc/doxyLog.log; echo $$?),1)
#echo "Generation of the doxygen documentation done"
As I understand test, it will return 0 if the file exists and 1 if the file does not exist. I suspect that you didn't test this before putting it into your makefile.
Second, you are confusing shell commands with Make commands. This:
ifeq ($(shell test -s ../doc/doxyLog.log; echo $$?),1)
#echo "Generation of the doxygen documentation done"
else
#echo "Error..."
endif
is a Make conditional. Make will evaluate it before running any rule. Since the log file does not yet exist, the shell command will return 1 (see First), the conditional will evaluate to true and the entire if-then-else statement will become
#echo "Generation of the doxygen documentation done"
This will become part of the rule before the rule is executed. On the next pass, the file already exists, the shell command returns 0 and the the statement becomes
#echo "Error..."
This explains why you're getting strange results.
If you want Make to report on the results of the attempt it's just made, you must put a shell conditional in a command in the rule:
doc: $(DOC_DIRECTORY)/path_finder_doc
doxygen $(DOC_DIRECTORY)/path_finder_doc
#if [ -s ../doc/doxyLog.log ]; then echo Log done; else echo error...; fi

Resources