191

A Makefile deploy recipe needs an environment variable ENV to be set to properly execute itself, whereas other recipes don't care, e.g.,

ENV = 

.PHONY: deploy hello

deploy:
    rsync . $(ENV).example.com:/var/www/myapp/

hello:
    echo "I don't care about ENV, just saying hello!"

How can I make sure this ENV variable is set? Is there a way to declare this makefile variable as a prerequisite of the deploy recipe? e.g.,

deploy: make-sure-ENV-variable-is-set
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
abernier
  • 27,030
  • 20
  • 83
  • 114
  • What do you mean, "make sure this variable is set"? Do you mean verify or ensure? If it was not set before, should `make` set it, or give a warning, or generate a fatal error? – Beta Jan 18 '11 at 23:58
  • 1
    This variable has to be specified by the user himself — as he is the only one who knows his environment (dev, prod...) — for example by calling `make ENV=dev` but if he forgets to `ENV=dev`, `deploy` recipe will fail... – abernier Jan 19 '11 at 00:51

9 Answers9

259

This will cause a fatal error if ENV is undefined and something needs it (in GNUMake, anyway).

.PHONY: deploy check-env

deploy: check-env
	...

other-thing-that-needs-env: check-env
	...

check-env:
ifndef ENV
	$(error ENV is undefined)
endif

(Note that ifndef and endif are not indented - they control what make "sees", taking effect before the Makefile is run. "$(error" is indented with a tab so that it only runs in the context of the rule.)

philo
  • 3,580
  • 3
  • 29
  • 40
Beta
  • 96,650
  • 16
  • 149
  • 150
  • 20
    I'm getting `ENV is undefined` when running a task that **does not** have check-env as a prerequisite. – raine Apr 28 '13 at 23:34
  • @rane: That's interesting. Can you give a minimal complete example? – Beta Apr 29 '13 at 00:09
  • 2
    @rane is the difference in spaces vs a tab character? – esmit Aug 20 '13 at 22:00
  • 9
    @esmit: Yes; I should have replied about this. In my solution, the line starts with a TAB, so it's a command in the `check-env` rule; Make won't expand it unless/until executing the rule. If it doesn't start with a TAB (as in @rane's example), Make interprets it as not being in a rule, and evaluates it before running any rule, regardless of the target. – Beta Aug 22 '13 at 20:30
  • 1
    ``` In my solution, the line starts with a TAB, so it's a command in the check-env rule;``` Which line are u talking about? In my case, the if condition is evaluated everytime even when the line after ifndef starts with TAB – Dhawal Jan 16 '16 at 02:21
  • Note that there isn't a *() around the variable being checked. Just spent a few minutes to realize that myself. – user2699 Sep 21 '16 at 01:12
  • This is causing my makefile to rebuild everything each time – Adrian May Feb 16 '18 at 17:08
  • @AdrianMay: If you mean that Make is running the `deploy` rule (or the corresponding rule in your makefile) every time, even if the target is already up to date, that's because no file called `check-env` is being built. You can move the `ifndef...endif` block into your `deploy` rule and remove `check-env` from the prerequisite list. (Assuming that my solution works as it is, which I'm having trouble verifying...) – Beta Feb 16 '18 at 19:01
  • I don't see any command that would make a file called check-env, and if it did, it would defeat the object, because a terminal without the environment variable might see the file after another terminal had created it. – Adrian May Feb 17 '18 at 20:14
  • @AdrianMay: Mostly correct, but moot if you follow my suggestion. – Beta Feb 17 '18 at 22:25
  • The ifndev does not have to be inside a target, it could be at the top of your Makefile if for example you require a variable to be set before the execution of any target. – Yves Dorfsman Feb 19 '18 at 00:23
  • @Beta, your answer said "$(error is indented with spaces", which results in the error always being evaluated. Your comment then says "In my solution, the line starts with a TAB", which makes way more sense. I edited the answer to use a tab. Hopefully that's what you intended. – philo Feb 26 '19 at 23:24
139

You can create an implicit guard target, that checks that the variable in the stem is defined, like this:

guard-%:
    @ if [ "${${*}}" = "" ]; then \
        echo "Environment variable $* not set"; \
        exit 1; \
    fi

You then add a guard-ENVVAR target anywhere you want to assert that a variable is defined, like this:

change-hostname: guard-HOSTNAME
        ./changeHostname.sh ${HOSTNAME}

If you call make change-hostname, without adding HOSTNAME=somehostname in the call, then you'll get an error, and the build will fail.

the_storyteller
  • 2,335
  • 1
  • 26
  • 37
Clayton Stanley
  • 7,513
  • 9
  • 32
  • 46
  • I know that this is an ancient reply, but perhaps someone is still watching it otherwise I might re-post this as a new question... I am trying to implement this implicit target "guard" to check for set environment variables and it works in principle, however the commands in the "guard-%" rule are actually printed to the shell. This I would like to suppress. How is this possible? – genomicsio Mar 06 '14 at 10:50
  • 4
    OK. found the solution myself... @ at the beginning of the rule command lines is my friend... – genomicsio Mar 06 '14 at 11:04
  • @genomicsio Nice suggestion; incorporated into the answer. – Clayton Stanley Mar 06 '14 at 23:04
  • 6
    One-liner: `if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi` :D – c24w Dec 17 '15 at 11:52
  • 3
    Warning, breakage if guard-VARIABLENAME is a file that exists. Work-around by declaring an empty phony target then setting the guard-% implicit rule to depending on it. [example .mak gist.github.com](https://gist.github.com/brimston3/fc43658bdb6882ed13d942fa584dd2de). This forces make to re-evaluate the rule every time it sees it. – Andrew Domaszek Nov 29 '16 at 01:56
  • love this solution. Is there a way somehow to put the guard in the next line in case we have multiple guard clauses ? seems that when i add an endline character before guard-HOSTNAME it does not work – Rose Nov 25 '20 at 05:11
61

Inline variant

In my makefiles, I normally use an expression like:

deploy:
    test -n "$(ENV)"  # $$ENV
    rsync . $(ENV).example.com:/var/www/myapp/

The reasons:

  • it's a simple one-liner
  • it's compact
  • it's located close to the commands which use the variable

Don't forget the comment which is important for debugging:

test -n ""
Makefile:3: recipe for target 'deploy' failed
make: *** [deploy] Error 1

... forces you to lookup the Makefile while ...

test -n ""  # $ENV
Makefile:3: recipe for target 'deploy' failed
make: *** [deploy] Error 1

... explains directly what's wrong

Global variant (for completeness, but not asked)

On top of your Makefile, you could also write:

ifeq ($(ENV),)
  $(error ENV is not set)
endif

Warnings:

  • don't use tab in that block
  • use with care: even the clean target will fail if ENV is not set. Otherwise see Hudon's answer which is more complex
Daniel Alder
  • 5,031
  • 2
  • 45
  • 55
  • 1
    It's a good alternative, but I don't like that the "error message" appears even if successful (the whole line is printed) – Jeff Mar 29 '19 at 14:24
  • @Jeff That's makefile basics. Just prefix the line with a `@`. -> https://www.gnu.org/software/make/manual/make.html#Echoing – Daniel Alder Mar 31 '19 at 11:25
  • 1
    I tried that, but then the error message won't appear in case of failure. Hmm I'll try it again. Upvoted your answer for sure. – Jeff Apr 01 '19 at 13:01
  • 3
    I like the test approach. I used something like this: `@test -n "$(name)" || (echo 'A name must be defined for the backup. Ex: make backup name=xyz' && exit 1)` – swampfox357 Apr 25 '19 at 21:45
16

I know this is old, but I thought I'd chime in with my own experiences for future visitors, since it's a little neater IMHO.

Typically, make will use sh as its default shell (set via the special SHELL variable). In sh and its derivatives, it's trivial to exit with an error message when retrieving an environment variable if it is not set or null by doing: ${VAR?Variable VAR was not set or null}.

Extending this, we can write a reusable make target which can be used to fail other targets if an environment variable was not set:

.check-env-vars:
    @test $${ENV?Please set environment variable ENV}


deploy: .check-env-vars
    rsync . $(ENV).example.com:/var/www/myapp/


hello:
    echo "I don't care about ENV, just saying hello!"

Things of note:

  • The escaped dollar sign ($$) is required to defer expansion to the shell instead of within make
  • The use of test is just to prevent the shell from trying to execute the contents of VAR (it serves no other significant purpose)
  • .check-env-vars can be trivially extended to check for more environment variables, each of which adds only one line (e.g. @test $${NEWENV?Please set environment variable NEWENV})
Lewis Belcher
  • 539
  • 5
  • 4
9

I've found with the best answer cannot be used as a requirement, except for other PHONY targets. If used as a dependency for a target that is an actual file, using check-env will force that file target to be rebuilt.

Other answers are global (e.g. the variable is required for all targets in the Makefile) or use the shell, e.g. if ENV was missing make would terminate regardless of target.

A solution I found to both issues is

ndef = $(if $(value $(1)),,$(error $(1) not set))

.PHONY: deploy
deploy:
    $(call ndef,ENV)
    echo "deploying $(ENV)"

.PHONY: build
build:
    echo "building"

The output looks like

$ make build
echo "building"
building
$ make deploy
Makefile:5: *** ENV not set.  Stop.
$ make deploy ENV="env"
echo "deploying env"
deploying env
$

value has some scary caveats, but for this simple use I believe it is the best choice.

23jodys
  • 91
  • 1
  • 2
8

As I see the command itself needs the ENV variable so you can check it in the command itself:

.PHONY: deploy check-env

deploy: check-env
    rsync . $(ENV).example.com:/var/www/myapp/

check-env:
    if test "$(ENV)" = "" ; then \
        echo "ENV not set"; \
        exit 1; \
    fi
ssmir
  • 1,522
  • 8
  • 10
  • The problem with this is that `deploy` is not necessarily the only recipe which needs this variable. With this solution, I have to test the state of `ENV` for each of one... while I would have like to deal with it as a single (sort of) prerequisite. – abernier Jan 18 '11 at 21:17
7

One possible problem with the given answers so far is that dependency order in make is not defined. For example, running:

make -j target

when target has a few dependencies does not guarantee that these will run in any given order.

The solution for this (to guarantee that ENV will be checked before recipes are chosen) is to check ENV during make's first pass, outside of any recipe:

## Are any of the user's goals dependent on ENV?
ifneq ($(filter deploy other-thing-that-needs-ENV,$(MAKECMDGOALS)),$())
ifndef ENV 
$(error ENV not defined)
endif
endif

.PHONY: deploy

deploy: foo bar
    ...

other-thing-that-needs-ENV: bar baz bono
    ...

You can read about the different functions/variables used here and $() is just a way to explicitly state that we're comparing against "nothing".

Hudon
  • 1,636
  • 18
  • 28
6

You can use ifdef instead of a different target.

.PHONY: deploy
deploy:
ifdef ENV
    rsync . $(ENV).example.com:/var/www/myapp/
else
    @echo 1>&2 "ENV must be set"
        false                            # Cause deploy to fail
endif
gerardw
  • 5,822
  • 46
  • 39
Daniel Gallagher
  • 6,915
  • 25
  • 31
  • Hey, thx for your answer but can't accept it because of duplicate code your suggestion generates... all the more `deploy` is not the only one recipe having to check the `ENV` state variable. – abernier Jan 18 '11 at 23:22
  • then just refactor. Use the `.PHONY: deploy` and `deploy:` statements before the ifdef block and remove the duplication. (btw I've edit the answer to reflect the correct method) – Dwight Spencer Aug 17 '15 at 18:54
0

Like @philo's answer but with less PHONY. I used the name of my Makefile as a rule name. This is valid.

Advantage: I do not need to list _check-make-vars-defined as dependency for every recipe in the Makefile. It will run with any recipe invocation.

# Force the _check-make-vars-defined recipe to always run. Verify our make variables have been defined.
# While Makefile will not usually have changed, its prerequisite will have to run regardless.
# Do not use .PHONY on the Makefile rule.
Makefile: _check-make-vars-defined

.PHONY: _check-make-vars-defined
_check-make-vars-defined:
    @#Verify our make variables have been defined.
ifndef _GITLAB_USER
    $(error _GITLAB_USER is not set)
endif
sqqqrly
  • 873
  • 1
  • 7
  • 10