156

How can I check if a program is callable from a Makefile?

(That is, the program should exist in the path or otherwise be callable.)

It could be used to check for which compiler is installed, for instance.

E.g. something like this question, but without assuming the underlying shell is POSIX compatible.

Community
  • 1
  • 1
Prof. Falken
  • 24,226
  • 19
  • 100
  • 173

14 Answers14

124

Sometimes you need a Makefile to be able to run on different target OS's and you want the build to fail early if a required executable is not in PATH rather than to run for a possibly long time before failing.

The excellent solution provided by engineerchuan requires making a target. However, if you have many executables to test and your Makefile has many independent targets, each of which requires the tests, then each target requires the test target as a dependency. That makes for a lot of extra typing as well as processing time when you make more than one target at a time.

The solution provided by 0xf can test for an executable without making a target. That saves a lot of typing and execution time when there are multiple targets that can be built either separately or together.

My improvement to the latter solution is to use the which executable (where in Windows), rather than to rely on there being a --version option in each executable, directly in the GNU Make ifeq directive, rather than to define a new variable, and to use the GNU Make error function to stop the build if a required executable is not in ${PATH}. For example, to test for the lzop executable:

 ifeq (, $(shell which lzop))
 $(error "No lzop in $(PATH), consider doing apt-get install lzop")
 endif

If you have several executables to check, then you might want to use a foreach function with the which executable:

EXECUTABLES = ls dd dudu lxop
K := $(foreach exec,$(EXECUTABLES),\
        $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))

Note the use of the := assignment operator that is required in order to force immediate evaluation of the RHS expression. If your Makefile changes the PATH, then instead of the last line above you will need:

        $(if $(shell PATH=$(PATH) which $(exec)),some string,$(error "No $(exec) in PATH")))

This should give you output similar to:

ads$ make
Makefile:5: *** "No dudu in PATH.  Stop.
thepith
  • 67
  • 6
Jonathan Ben-Avraham
  • 4,615
  • 2
  • 34
  • 37
  • 3
    If you make EXECUTABLES all variables, (i.e. LS CC LD), and use $($(exec)), you can pass them to make seamlessly from the environment or other makefiles. Useful when cross-compiling. – rickfoosusa Sep 28 '15 at 14:31
  • 3
    My make chokes at the "," when running "ifeq (, $(shell which lzop))" =/ – mentatkgs Jan 13 '16 at 01:25
  • @mentatkgs: What version of `make` are you using, and on what OS? What error message do you see? Note that this post relates specifically to GNU `make`, not other `make` implementations. – Jonathan Ben-Avraham Jan 13 '16 at 15:15
  • Gnu make 4.0 on Fedora 23. The message I saw is a parse error on the ",". – mentatkgs Jan 20 '16 at 14:42
  • @mentatkgs: Works fine on Gnu make 4.0. I suspect that you are missing the backslash after the comma, `$(EXECUTABLES),\`, or you have a space or tab character after the backslash. – Jonathan Ben-Avraham Jan 27 '16 at 09:17
  • 4
    On Windows, use `where my_exe 2>NUL` instead of `which`. – Aaron Campbell Nov 21 '16 at 22:49
  • ```NUL``` is only available in ```cmd.exe```, a "safer" option is to use... ```where my_exe 2>&1```, which will redirect StdErr to StdOut. This works on all of the shells I know of. – burito Dec 10 '17 at 19:21
  • 9
    Beware of TABS indentations with ifeq which is a rule syntax! it must be WITHOUT TAB. Had a hard time with that. https://stackoverflow.com/a/21226973/2118777 – Pipo Dec 07 '18 at 12:23
  • 2
    If you're using `Bash`, there's no need to make an external call to `which`. Use the built-in `command -v` instead. [More info](https://stackoverflow.com/a/677212/1053066). – kurczynski Oct 27 '19 at 20:41
77

I mixed the solutions from @kenorb and @0xF and got this:

DOT := $(shell command -v dot 2> /dev/null)

all:
ifndef DOT
    $(error "dot is not available please install graphviz")
endif
    dot -Tpdf -o pres.pdf pres.dot 

It works beautifully because "command -v" doesn't print anything if the executable is not available, so the variable DOT never gets defined and you can just check it whenever you want in your code. In this example I'm throwing an error, but you could do something more useful if you wanted.

If the variable is available, "command -v" performs the inexpensive operation of printing the command path, defining the DOT variable.

mentatkgs
  • 1,541
  • 1
  • 13
  • 17
  • 8
    At least in some systems (i.e. Ubuntu 14.04), `$(shell command -v dot)` fails with `make: command: Command not found`. To fix this, redirect stderr output to /dev/null: `$(shell command -v dot 2> /dev/null)`. [Explanation](http://stackoverflow.com/a/12991757/882918) – J0HN Jan 13 '16 at 09:55
  • I thought command was a bash builtin, so it would not cause any trouble. I'm guessing I'm wrong then. Anyway, I have some Ubuntu 14.04 machines at work. I'm going to check that later. – mentatkgs Jan 20 '16 at 14:49
  • 3
    `command` is a bash builtin, for more portability, consider using `which` ? – Julien Palard Mar 10 '16 at 09:23
  • 12
    @JulienPalard I believe you're mistaken: the `command` utility is required by [posix](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html) (including the `-v` option) On the other hand `which` has no standardized bahaviour (that I know of) and is sometimes outright [unusable](http://stackoverflow.com/a/677212/117814) – Gyom Jul 21 '16 at 09:48
  • Note that the indentation is significant on macOS/OSX, see: http://stackoverflow.com/questions/4483313/make-error-for-ifeq-syntax-error-near-unexpected-token – Dynom Aug 05 '16 at 07:50
  • 4
    `command -v` requires a POSIX-like shell environment. This will not work on Windows without cygwin or similar emulation. – dolmen Aug 30 '16 at 09:31
  • 2
    Just want to warn that this doesn't work as part as a recipe, as `ifndef` is always evaluated. The answer from engineerchuan works better in that regard. – dotnetCarpenter Apr 07 '17 at 19:58
  • 12
    The reason why `command` often doesn't work is *not because of its absence*, but because of a [peculiar optimization](https://stackoverflow.com/a/17550243) that GNU Make does: if a command is "simple enough" it will bypass the shell; this unfortunately means built-ins sometimes won't work unless you mangle the command a bit. – Rufflewind Jun 15 '17 at 06:59
  • @J0HN While it may make sense in some specific cases to redirect errors as you mentioned, it has the downside of not behaving the same way as the original command. The correct way to force make to send your command through a shell without any side effect is to add a semicolon at the end of the arguments. – Johan Boulé Apr 07 '19 at 03:45
38

is this what you did?

check: PYTHON-exists
PYTHON-exists: ; @which python > /dev/null
mytarget: check
.PHONY: check PYTHON-exists

credit to my coworker.

dolmen
  • 8,126
  • 5
  • 40
  • 42
engineerchuan
  • 2,580
  • 3
  • 23
  • 22
24

Use the shell function to call your program in a way that it prints something to standard output. For example, pass --version.

GNU Make ignores the exit status of the command passed to shell. To avoid the potential "command not found" message, redirect standard error to /dev/null.

Then you may check the result using ifdef, ifndef, $(if) etc.

YOUR_PROGRAM_VERSION := $(shell your_program --version 2>/dev/null)

all:
ifdef YOUR_PROGRAM_VERSION
    @echo "Found version $(YOUR_PROGRAM_VERSION)"
else
    @echo Not found
endif

As a bonus, the output (such as program version) might be useful in other parts of your Makefile.

0xF
  • 3,214
  • 1
  • 25
  • 29
  • this gives error `*** missing separator. Stop.` If I add tabs at all lines after `all:` I get error `make: ifdef: Command not found` – Matthias 009 Jan 08 '14 at 16:24
  • also: `ifdef` will evaluate true even if `your_program` does not exist http://www.gnu.org/software/make/manual/make.html#Conditional-Syntax – Matthias 009 Jan 08 '14 at 17:47
  • 1
    Don't add tabs to the `ifdef`, `else`, `endif` lines. Just make sure the `@echo` lines start with tabs. – 0xF Jan 15 '14 at 16:56
  • I tested my solution on Windows and Linux. The documentation you linked states that `ifdef` checks for non-empty variable value. If your variable is not empty, what does it evaluate to? – 0xF Jan 15 '14 at 17:02
  • 2
    @Matthias009 when I test GNU Make v4.2.1, using `ifdef` or `ifndef` (as in the accepted answer) only works if the variable being evaluated is immediately set (`:=`) instead of lazily set (`=`). However, a downside of using immediately set variables is that they are evaluated at declaration time, whereas lazily set variables are evaluated when they are called. This means you would be executing commands for variables with `:=` even when Make is only running rules that never use those variables! You can avoid this by using a `=` with `ifneq ($(MY_PROG),)` – Dennis Jan 07 '18 at 20:49
16

Cleaned up some of the existing solutions here...

REQUIRED_BINS := composer npm node php npm-shrinkwrap
$(foreach bin,$(REQUIRED_BINS),\
    $(if $(shell command -v $(bin) 2> /dev/null),$(info Found `$(bin)`),$(error Please install `$(bin)`)))

The $(info ...) you can exclude if you want this to be quieter.

This will fail fast. No target required.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
10

My solution involves a little helper script1 that places a flag file if all required commands exist. This comes with the advantage that the check for the required commands is only done once and not on every make invocation.

check_cmds.sh

#!/bin/bash

NEEDED_COMMANDS="jlex byaccj ant javac"

for cmd in ${NEEDED_COMMANDS} ; do
    if ! command -v ${cmd} &> /dev/null ; then
        echo Please install ${cmd}!
        exit 1
    fi
done

touch .cmd_ok

Makefile

.cmd_ok:
    ./check_cmds.sh

build: .cmd_ok target1 target2

1 More about the command -v technique can be found here.

Community
  • 1
  • 1
Flow
  • 23,572
  • 15
  • 99
  • 156
  • 2
    I just tested it and it works and since you provided no hint about what is not working for you (the bash script or the Makefile code) or any error messages I can't help you find the problem. – Flow May 03 '13 at 11:45
  • I'm getting "unexpected end of file" on the first `if` statement. – Adam Grant Mar 27 '15 at 14:20
10

I am personally defining a require target which runs before all the others. This target simply runs the version commands of all requirements one at a time and prints appropriate error messages if the command is invalid.

all: require validate test etc

require:
    @echo "Checking the programs required for the build are installed..."
    @shellcheck --version >/dev/null 2>&1 || (echo "ERROR: shellcheck is required."; exit 1)
    @derplerp --version >/dev/null 2>&1 || (echo "ERROR: derplerp is required."; exit 1) 

# And the rest of your makefile below.

The output of the below script is

Checking the programs required for the build are installed...
ERROR: derplerp is required.
makefile:X: recipe for target 'require' failed
make: *** [require] Error 1
Rudi Kershaw
  • 12,332
  • 7
  • 52
  • 77
6

For me all above answers are based on linux and are not working with windows. I'm new to make so my approach may not be ideal. But complete example that works for me on both linux and windows is this:

# detect what shell is used
ifeq ($(findstring cmd.exe,$(SHELL)),cmd.exe)
$(info "shell Windows cmd.exe")
DEVNUL := NUL
WHICH := where
else
$(info "shell Bash")
DEVNUL := /dev/null
WHICH := which
endif

# detect platform independently if gcc is installed
ifeq ($(shell ${WHICH} gcc 2>${DEVNUL}),)
$(error "gcc is not in your system PATH")
else
$(info "gcc found")
endif

optionally when I need to detect more tools I can use:

EXECUTABLES = ls dd 
K := $(foreach myTestCommand,$(EXECUTABLES),\
        $(if $(shell ${WHICH} $(myTestCommand) 2>${DEVNUL} ),\
            $(myTestCommand) found,\
            $(error "No $(myTestCommand) in PATH)))
$(info ${K})        
Vit Bernatik
  • 3,566
  • 2
  • 34
  • 40
4

You can use bash built commands such as type foo or command -v foo, as below:

SHELL := /bin/bash
all: check

check:
        @type foo

Where foo is your program/command. Redirect to > /dev/null if you want it silent.

kenorb
  • 155,785
  • 88
  • 678
  • 743
3

Assume you have different targets and builders, each requires another set of tools. Set a list of such tools and consider them as target to force checking their availability

For example:

make_tools := gcc md5sum gzip

$(make_tools):  
    @which $@ > /dev/null

file.txt.gz: file.txt gzip
    gzip -c file.txt > file.txt.gz 
lkanab
  • 924
  • 1
  • 7
  • 20
2

The solutions checking for STDERR output of --version does not work for programs which print their version to STDOUT instead of STDERR. Instead of checking their output to STDERR or STDOUT, check for the program return code. If the program does not exist, its exit code will always be non zero.

#!/usr/bin/make -f
# https://stackoverflow.com/questions/7123241/makefile-as-an-executable-script-with-shebang
ECHOCMD:=/bin/echo -e
SHELL := /bin/bash

RESULT := $(shell python --version >/dev/null 2>&1 || (echo "Your command failed with $$?"))

ifeq (,${RESULT})
    EXISTS := true
else
    EXISTS := false
endif

all:
    echo EXISTS: ${EXISTS}
    echo RESULT: ${RESULT}
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144
1

A bit late but here is my take on that question

CMD_NOT_FOUND = $(error $(1) is required for this rule)
CHECK_CMD = $(if $(shell command -v $(1)),,$(call CMD_NOT_FOUND,$(1)))

you can either apply it globally:

$(call CHECK_CMD,gcc)

or within a specific rule:

do_stuff:
    $(call CHECK_CMD,md5sum)

or within a for loop

REQUIREMENTS := gcc md5sum gzip
 
$(foreach req,$(REQUIREMENTS),$(call CHECK_CMD,$(req)))
Yohann Boniface
  • 494
  • 3
  • 10
1

even later, got here while searching an answer

... but then I tried something else (on Linux) ...

--8<----8<----8<--

zig := $(word 1,$(foreach var,$(subst :, ,$(PATH)),$(wildcard $(var)/zig)))

ifdef zig
$(info $(zig) defined)
endif
tomi
  • 153
  • 1
  • 4
1

Solved by compiling a special little program in another makefile target, whose sole purpose is to check for whatever runtime stuff I was looking for.

Then, I called this program in yet another makefile target.

It was something like this if I recall correctly:

real: checker real.c
    cc -o real real.c `./checker`

checker: checker.c
    cc -o checker checker.c
Prof. Falken
  • 24,226
  • 19
  • 100
  • 173
  • Thank you. But this would abort make, right? Is it possible to prevent make from aborting? I'm trying to skip a build step if the required tool is not available.. – Marc-Christian Schulze May 04 '12 at 11:04
  • @coding.mof edited - it was more like that. The program checked something in the runtime and gave info accordingly to the rest of the compile. – Prof. Falken May 04 '12 at 11:25