Reputation: 998
I want to run certain action in the shell depending on whether a default makefile in the current directory contains a certain target.
#!/bin/sh
make -q some_target
if test $? -le 1 ; then
true # do something
else
false # do something else
fi
This works, because GNU make returns error code 2 if the target is absent, 0 or 1 otherwise. The problem is that is not documented this way. Here is a slice of the man:
-q, --question
``Question mode''. Do not run any commands, or print anything;
just return an exit status that is zero if the specified targets
are already up to date, nonzero otherwise.
Only zero/nonzero is distinguished. What is the right way to do that?
Upvotes: 8
Views: 14801
Reputation: 884
Simple solution:
output=$(make -n some_target 2>&1 | head -1)
if [[ "$output" != *"No rule to make target"* ]]; then
echo "target is present"
fi
The idea is to check output of make without running it. Not sure, it's 100% right way though.
Can be wrapped in function:
function check_make_target {
output=$(make -n "$1" 2>&1 | head -1)
[[ "$output" != *"No rule to make target"* ]]
}
if check_make_target some_target; then
echo "target is present"
fi
Upvotes: 1
Reputation: 6104
I needed a solution which would work for multiple goals/targets and also not trigger the Makefile to be remade.
Here is what I ended up using:
#!/usr/bin/env bash
set -euo pipefail
dir="$1"
targets="$2"
# If there is no Makefile then return.
[[ ! -f "${dir}/Makefile" ]] && exit 1
# First check if this build will remake itself.
# If it does then add Makefile as a target to stop it.
if eval ${MAKE} --dry-run "--directory=${dir}" Makefile &> /dev/null; then
db_target=Makefile
else
db_target=
fi
# Search the Make internal database for the targets.
eval ${MAKE} --dry-run --print-data-base "--directory=${dir}" "${db_target}" | \
awk -v targets="${targets}" '
BEGIN {
# Count the number of targets and put them in an array keyed by their own value.
numRemaining = split(targets, temp)
for (i in temp) remaining[temp[i]]
}
# If we found all the targets then consume all the input so the pipe does not fail.
numRemaining == 0 { next }
# Skip everything from the first line up to the start of the database comment inclusive.
NR == 1, /\# Make data base/ { next }
# Skip those that are not real targets.
/\# Not a target:/, /^[^#]/ { next }
{
# Check if this line starts with any of the targets.
for (target in remaining) {
if ($0 ~ "^"target":") {
delete remaining[target]
numRemaining--
next
}
}
}
END {
# If we get to the end then make sure that we found all the targets.
exit (numRemaining ? 1 : 0)
}'
The caveats are:
Makefile
(or not exist at all).export MAKE
statement in your Make file (you can hard code make
into the script if you prefer).eval
(Yuck!).I use it in my Make file to aggregate targets on sub-projects which may not exist like this:
top_targets := all setup build clean mostlyclean
sub_dirs := foo bar
.PHONY: $(top_targets)
$(top_targets): $(sub_dirs)
.PHONY: $(sub_dirs)
$(sub_dirs):
@if ./bin/make-target-exists.sh '$@' '$(MAKECMDGOALS)'; then $(MAKE) --directory='$@' $(MAKECMDGOALS); fi
Upvotes: 0
Reputation: 419
Another possibility is to grep within the database that results from reading the makefile:
some_target:
@ if $(MAKE) -C subdir -npq --no-print-directory .DEFAULT 2>/dev/null | grep -q "^$@:" ; \
then \
echo "do something" ; \
fi
where
-n --just-print, --dry-run
Print the commands that would be executed, but do not execute them.
-p, --print-data-base
Print the data base (rules and variable values) that results from reading
the makefiles
-q, --question
Question mode. Do not run any commands, or print anything; just return and
exit status that is zero if the specified targets are already up to date,
nonzero otherwise.
Upvotes: 3
Reputation: 11828
Late answer, but perhaps it will help people facing the same problem in the future.
I use the approach below, which does require you to modify the Makefile (you need to add a single %-rule-exists pattern rule). If you don't want to do that, you can just run make -n your-target-to-check &> /dev/null
directly from your script.
I use this to have commands like autobuild and autoupload, which are included for context in the snippet.
%-rule-exists:
@$(MAKE) -n $* &> /dev/null
auto%: %-rule-exists
$(info Press CTRL-C to stop $*ing)
@while inotifywait -qq -r -e move -e create -e modify -e delete --exclude "\.#.*" src; do make $* ||: ; date ; done
Example output:
$ make build-rule-exists; echo Exit code: $?
Exit code: 0
$ make nonsense-rule-exists; echo Exit code: $?
Exit code: 2
Note that it does not actually build the target to find out if the rule exists, courtesy of the -n
flag of make
. I needed it to work even if a build would fail.
Upvotes: 4
Reputation: 100781
You should read the GNU make manual rather than the man page: the man page is merely a summary not a complete definition. The manual says:
The exit status of make is always one of three values:
0 The exit status is zero if make is successful
2 The exit status is two if make encounters any errors. It will print messages
describing the particular errors.
1 The exit status is one if you use the ‘-q’ flag and make determines that
some target is not already up to date.
Since trying to create a target that doesn't exists is an error, you'll always get an exit code of 2 in that situation.
Upvotes: 8