Reputation: 24937
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.
Upvotes: 164
Views: 105914
Reputation: 11
I use the following, inspired by other answers in this topic and StackOverflow and similar sites:
Makefile
ℹ️ (This is just the top of Makefile
's i will create self...)
# SPDX-License-Identifier: CC-BY-NC-SA-4.0
#
SHELL = /usr/bin/bash
.SHELLFLAGS = -ec
.ONESHELL:
.SILENT:
UTILS :=
UTILS += setfacl
UTILS += chown
UTILS += chmod
# UTILS += nonexisting
include CheckUtils.mk
sh
.CheckUtils.mk
ℹ️ Bug report wrt highlighting: https://github.com/highlightjs/highlight.js/issues/3878
ℹ️ The latest version of this file is available in one-of-my repo's at GitLab. 😉🤝
# SPDX-License-Identifier: CC-BY-NC-SA-4.0
# Version: 2023-11-14
#
# This make-snippet will both check for the utils you define AND
# automatically create an uppercase variable with the util name
# for you to use in the rest of your Makefile(s).
#
# Usage:
# The utils to be used should be put inside the UTILS var,
# separated by space. Example:
# UTILS :=
# UTILS += setfacl
# UTILS += chown
# UTILS += chmod
# # UTILS += nonexisting
# include CheckUtils.mk
#
# Explanation of workings:
# Lines #50-51, #86, #88:
# For use in recursive make's.
# Makes sure that the code inside this make-snippet is only applied ONCE.
# Line #52:
# Feedback header
# Lines #54-60:
# Creates a macro for uppercase conversion.
# It auto-selects from two versions, which you can extend as needed.
# The first, bash variant, uses ${VAR^^}.
# The last, non-bash variant eg sh, uses sed.
# Lines #64, #74:
# Is to prevent checking and defining the SHELL itself,
# but allow it to be displayed in the feedback. 😉
# (The shell is included as first util to check in line #63)
# Lines #66-71:
# Is to let make auto-create the respective variable(s) in uppercase
# with the found executable as value.
# The export in line #67 makes the created variable(s) available in
# recursive make's without re-processing.
# Line #72 in combo with #65/#73:
# Aborts the make with an error if the requested util can not be found.
# Lines #75-83:
# Provide feedback about which util was found and where.
# The output is nicely right-aligned when the length of the util-name
# is <=20 (Line #78)
# Lines #84-85:
# Feedback footer
#
# Cross-posted-at:
# https://stackoverflow.com/a/77223358/22510042
# For recorsive invocations.
ifndef CheckUtils
$(info ### START: Utils check)
# Define macro w.r.t. shell in use.
ifeq (bash,$(findstring bash,$(SHELL)))
toUpper = $(shell string="$(strip $1)"; printf "%s" $${string^^})
else
# Insipred by: https://stackoverflow.com/a/37660916/22510042
toUpper = $(shell echo "$(strip $1)" | sed 's/.*/\U&/')
endif
# Inspired by: https://stackoverflow.com/a/37197276/22510042
$(foreach util,shell $(UTILS),\
$(if $(filter-out shell,$(util)),\
$(if $(shell command -v $(util) 2>/dev/null),\
$(eval $(strip \
export \
$(call toUpper,$(util) )\
:=\
$(shell command -v $(util) 2>/dev/null)\
))\
,$(error No '$(util)' in PATH)\
)\
)\
$(info \
$(shell \
printf "%*s = '%s'" \
20 \
"$(call toUpper,$(util))" \
"$($(call toUpper,$(util)))"\
)\
)\
)
$(info ### END: Utils check)
$(info ) # Empty line
export CheckUtils:=1 # Mark already applied for recorsive invocations.
# $(error END: Utils check, forced for debuging this snippet)
endif
make
without any changes will be similar to:
### START: Utils check
SHELL = /usr/bin/bash
SETFACL = /usr/bin/setfacl
CHOWN = /usr/bin/chown
CHMOD = /usr/bin/chmod
### END: Utils check
nonexisting
util uncommented. will be similar to:
### START: Utils check
SHELL = /usr/bin/bash
SETFACL = /usr/bin/setfacl
CHOWN = /usr/bin/chown
CHMOD = /usr/bin/chmod
Makefile:21: *** No 'nonexisting' in PATH. Stop.
Hope this will be useful to others, it will at least serve as a backup for myself 🤣
Upvotes: 1
Reputation: 173
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
Upvotes: 1
Reputation: 594
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)))
Upvotes: 1
Reputation: 13012
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
Upvotes: 13
Reputation: 9475
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}
Upvotes: 2
Reputation: 4841
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.
Upvotes: 134
Reputation: 954
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
Upvotes: 3
Reputation: 3802
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})
Upvotes: 6
Reputation: 2708
is this what you did?
check: PYTHON-exists
PYTHON-exists: ; @which python > /dev/null
mytarget: check
.PHONY: check PYTHON-exists
credit to my coworker.
Upvotes: 38
Reputation: 283243
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.
Upvotes: 16
Reputation: 1601
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.
Upvotes: 81
Reputation: 166813
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.
Upvotes: 4
Reputation: 24083
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.
Upvotes: 10
Reputation: 3718
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.
Upvotes: 25
Reputation: 24937
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
Upvotes: 1