How to compare two shell command output in Makefile? - shell

My Makefile is:
.PHONY: check
check:
ifneq $(shell echo 123), $(shell echo 123)
$(error Not equal)
endif
When I run, I've got the error:
$ make
Makefile:3: *** Not equal. Stop.
But this should happen only when they're different, but they're not. Why?

ifneq cannot be indented. the way you've written it, it's being run via a shell command which means the $(error) is being evaluated first by the make command.
i'm guessing you want the make check to actually run two commands only when make check is invoked, and compare their output. you can do:
.PHONY: check
check:
if [ "`echo 123`" != "`echo 123`" ]; then \
echo "Not equal"; \
exit 1; \
fi

According to GNU Make docs, Conditional Parts cannot be used to control shell commands at the time of execution, since conditionals control what make actually "sees" in the makefile.
So to perform condition during compilation process, shell syntax is preferred, e.g.
SHELL := /bin/bash -e
.PHONY: check
check:
#test "$(shell echo 123)" = "$(shell echo 123)" \
|| { echo Not equal; exit 2; } \
&& { echo Equal; }

Related

How to separate different parts with exit control in Makefile

I am writing a complicated Makefile which have multiple parts like:
step1:
if [[ ${input} == "delete" ]]; then \
echo "this is a test to delete files"; \
else \
echo "error stop"; \
fi
step2:
rm -f *.txt
test:
make step1
make step2
So if I enter "make test input=delete", it will delete all the .txt files. And if I enter "make test input=none", it would not do anything. I know the simplest way is to combine step1 and step2 as:
test:
if [[ ${input} == "delete" ]]; then \
echo "this is a test to delete files"; \
rm -f *.txt; \
else \
echo "error stop"; \
fi
But my Makefile is so complicated that I have to separate into several parts. Does Makefile support similar features? If yes, what should I look for? Thanks.
You should always use $(MAKE) and never use make directly when invoking a sub-make.
If you do that, then all command-line overrides you provide will be correctly passed to the sub-makes.
BTW, you should not use the bash syntax [[ x == y ]]. If you run this makefile on a system where the default shell is limited to POSIX standard syntax this won't work. You should use POSIX syntax: [ x = y ].
As you use GNU Make you can use its conditionals, e.g.
ifeq:
test:
ifeq ($(input),delete)
echo "this is a test to delete files"
rm -f *.txt
else
echo "error stop"
endif
Or, if you want to control the prerequisites of a target:
step1:
echo "this is a test to delete files"
step2:
rm -f *.txt
# test always depends on step1
test: step1
# test also depends on step2, if input = delete
ifeq ($(input),delete)
test: step2
endif

How can I test to see if a program is available in Make?

Here's what I have so far. I'm not looking for RVM-specific answers – I need to be able to replace rvm with, say, evm. Just wanted to pick something everyone would be familiar with :)
install_rvm:
ifeq ("`which rvm > /dev/null; echo $?`", "0")
#echo "rvm already installed!"
else
#echo "installing rvm..."
install_rvm_cmd
endif
Edit
Sorry for not saying this up-front, but I would like install_rvm_cmd to be run as it would normally – I don't want to hide the install process from the user.
I would do it like this:
.PHONY: install_rvm
install_rvm:
#if which rvm > /dev/null; then \
echo "rvm already installed!"; \
else \
echo "installing rvm..."; \
install_rvm_cmd; \
fi
It would be possible to use ifeq but it would entail performing the which rvm check even if install_rvm is not an actual target in a specific run of make. (I'm assuming the general case in which install_rvm is just one target among many.)
Although the string install_rmv_cmd itself wont' be output by make, its output will be output as usual. If you really want to have install_rmv_cmd out on stdout, you can always add echo install_rmv_cmd just before the command itself. To avoid repetition you could have the else branch be:
echo "installing rvm..."; \
cmd=install_rmv_cmd; \
echo $$cmd; \
$$cmd; \
In addition to Louis's answer, you can use ifdef. For example:
EVM_LOCATION := $(shell which evm)
CASK_LOCATION := $(shell which cask)
install_cask:
ifdef CASK_LOCATION
$(info cask is already installed!)
else
curl -fsSkL https://raw.github.com/cask/cask/master/go | python
endif
install_evm:
ifdef EVM_LOCATION
$(info evm is already installed!)
else
curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash
endif
You could use something like this (untested) which uses a stamp file to avoid doing any work once it has been done.
install_rvm_stamp:
__rvm=$$(command -v rvm);\
if [ -z "$$__rvm" ] || ! "$$__rvm" --version >/dev/null 2>&1; then \
echo "installing rvm..."; \
install_rvm_cmd; \
else \
echo 'rvm already installed!'
fi
touch '$#'
.PHONY: install_rvm
install_rvm: install_rvm_stamp
Alternatively, if you don't mind not "function testing" the binary you could do something like this instead.
rvm_bin := $(or $(realpath /usr/local/bin/rvm),$(realpath /bin/rvm),$(realpath /usr/bin/rvm),do_install_rvm)
install_rvm: $(rvm_bin)
do_install_rvm:
#echo "installing rvm..."
install_rvm_cmd
One final comment, as an augmentation to the make-level ifdef option presented in your answer; if the targets in question are only intended to be run manually (and are not used as prerequisites for any other targets) then the extraneous which calls can be avoided with some extra make-level checking.
ifneq (,$(findstring install_evm,$(MAKECMDGOALS)))
EVM_LOCATION := $(shell which evm)
endif
Wrap that in the following and you can avoid undefined variable warnings about MAKECMDGOALS if --warn-undefined-variables is used.
ifneq (undefined,$(origin MAKECMDGOALS))
....
endif

If statement in makefile

I found that I can use ifneq in makefile and I tried to compare 0 and the output of command stat:
#for f in `find $(PATH_PAGES) -name *.hbs`; do \
ifneq "`stat -c '%Y' $$f`" "0";
//some code here
endif
done
But in terminal I've got an error: ifneq: command not found
Is there a different way to compare this or maybe I'm doing something wrong?
In this case you don't want to use Make's ifneq, because it does text substitution before handing over the command to the shell, but you have a shell loop that needs to do different things in each iteration depending on the output of a shell command.
Use the shell if instead:
if [ "`stat -c '%Y' $$f`" != "0" ]; then
//some code here
fi
If you want to use makefile's if condition then there should not be [TAB] before the if statement because if you specify [TAB] then it is treated as shell command thats why you are getting error that ifneq:command not found its not there in shell.
May be this Conditionals in Makefile: missing separator error?
can help in getting better understanding with makefiles
I found that I needed to prepend the if with a #, and backslashes proved to be necessary as well -
#if [ "`stat -c '%Y' $$f`" != "0" ]; then\
echo hello world;\
fi

How do I get $(error ...) to work conditionally in GNU Make?

I'd like to use $(error ...) to abort my make process if certain preconditions aren't met. The fails_to_work target should abort when failing test -d /foobar.
BAD.mk
all: this_works fails_to_work
this_works:
#echo echo works...
#test -d ~ || echo ~ is not a directory
#test -d /foobar || echo /foobar is not a directory
fails_to_work:
#echo error does not work...
#test -d ~ || $(error ~ is not a directory)
#test -d /foobar || $(error /foobar is not a directory)
$ make -f BAD.mk
echo works...
/foobar is not a directory
BAD.mk:9: *** ~ is not a directory. Stop.
As you can see, not even "error does not work..." is echoed to the screen. The recipe for fails_to_work fails before it gets started. How do I solve this? One of my use cases is#test -d $(MY_ENV_VAR), but I don't think that differs from the hard-coded paths given in the example.
UPDATE (version information)
$ make --version
GNU Make 3.81
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
This program built for x86_64-pc-linux-gnu
You're trying to get the shell stuff in a recipe to conditionally invoke makefile stuff, which doesn't work, as you've found.
I can think of two options:
Simply remove the $(error) stuff. If test fails, then it will return a non-zero exit status, and the Make process will terminate at that point.
Take the test out of the rule, and use a Make conditional (which in turn invokes shell functionality), e.g.:
ifeq ($(shell test -d /foobar; echo $$?),1)
$(error Not a directory)
endif
Shell commands for a make recipe are effectively stored as a single recursively expanded variable. At the point make decides to run the recipe, it expands the variable, and then runs each line in its own shell invocation. Any $(error ...) that gets expanded will cause make to abort even before invoking the first command.
Note though that the untaken branch of a $(if ...) or $(or ...) &c. will not be expanded. Thus, you could do
.PHONY: rule-with-assert
rule-with-assert:
$(if $(realpath ${should-be-file}/),$(error Assertion failure: ${should-be-file} is a folder!))
⋮
Note that trailing / in the realpath.
Of course macros help to tidy this up a lot.
assert-is-file = $(if $(realpath $1/),$(error Assertion failure: [$1] is a folder!))
.PHONY: rule-with-assert
rule-with-assert:
$(call assert-is-file,${should-be-file})
⋮
It's worth noting again that it doesn't matter where you put the $(call assert-is-file,…) in the recipe.
Any $(error)will be generated as the recipe is expanded,
before any shell commands are run.
Why don't you just use exit 1 shell command instead of $(error ...)? Is there any reason to use the latter?
try_this:
#test -d /foobar || { echo /foobar is not a directory; exit 1; }
or_this:
#if [ ! -d /foobar ]; then echo /foobar is not a directory; exit 1; fi
Both of these will abort the make process unless -k flag is specified.
-k
--keep-going
Continue as much as possible after an error. While the target that failed, and those that depend on it, cannot be remade, the other prerequisites of these targets can be processed all the same.

How to check return value from the shell directive

In my Makefile, I need to test if the current directory is an SVN repo or not and if it is not I want to indicate an error using the $(error) directive in Makefile.
So I plan to use the return value of $(shell svn info .) but I'm not sure how to get this value from within the Makefile.
Note: I'm not trying to get the return value in a recipe, but rather in the middle of the Makefile.
Right now I'm doing something like this, which works just because stdout is blank when it is an error:
SVN_INFO := $(shell svn info . 2> /dev/null)
ifeq ($(SVN_INFO),)
$(error "Not an SVN repo...")
endif
I'd still like to find out if it is possible to get the return value instead within the Makefile.
How about using $? to echo the exit status of the last command?
SVN_INFO := $(shell svn info . 2> /dev/null; echo $$?)
ifeq ($(SVN_INFO),1)
$(error "Not an SVN repo...")
endif
If you want to preserve the original output then you need to do some tricks. If you are lucky enough to have GNU Make 4.2 (released on 2016-05-22) or later at your disposal you can use the .SHELLSTATUS variable as follows.
var := $(shell echo "blabla" ; false)
ifneq ($(.SHELLSTATUS),0)
$(error shell command failed! output was $(var))
endif
all:
#echo Never reached but output would have been $(var)
Alternatively you could use a temporary file or play with Make's eval to store the string and/or the exit code into a Make variable. The example below gets this done but I would certainly like to see a better implementation than this embarrassingly complicated version.
ret := $(shell echo "blabla"; false; echo " $$?")
rc := $(lastword $(ret))
# Remove the last word by calculating <word count - 1> and
# using it as the second parameter of wordlist.
string:=$(wordlist 1,$(shell echo $$(($(words $(ret))-1))),$(ret))
ifneq ($(rc),0)
$(error shell command failed with $(rc)! output was "$(string)")
endif
all:
#echo Never reached but output would have been \"$(string)\"
This worked fine for me - based on #eriktous' answer with a minor modification of redirecting stdout as well to skip the output from svn info on a valid svn repo.
SVN_INFO := $(shell svn info . 1>&2 2> /dev/null; echo $$?)
ifneq ($(SVN_INFO),0)
$(error "Not an SVN repo...")
endif
Maybe something like this?
IS_SVN_CHECKED_OUT := $(shell svn info . 1>/dev/null 2>&1 && echo "yes" || echo "no")
ifne ($(IS_SVN_CHECKED_OUT),yes)
$(error "The current directory must be checked out from SVN.")
endif
I use .NOTPARALLEL and a make function:
.NOTPARALLEL:
# This function works almost exactly like the builtin shell command, except it
# stops everything with an error if the shell command given as its argument
# returns non-zero when executed. The other difference is that the output
# is passed through the strip make function (the shell function strips only
# the last trailing newline). In practice this doesn't matter much since
# the output is usually collapsed by the surroundeing make context to the
# same result produced by strip.
SHELL_CHECKED = \
$(strip \
$(if $(shell (($1) 1>/tmp/SC_so) || echo nonempty), \
$(error shell command '$1' failed. Its stderr should be above \
somewhere. Its stdout is in '/tmp/SC_so'), \
$(shell cat /tmp/SC_so)))

Resources