如何确保 Makefile 变量被设置为先决条件?

一个生成文件的 abc0配方需要一个环境变量的 abc1来正确地执行它自己,而其他的配方并不关心,例如,

ENV =


.PHONY: deploy hello


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


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

如何确保设置了这个 ENV变量?是否有方法声明这个 makefile 变量作为部署配方的先决条件?例如:

deploy: make-sure-ENV-variable-is-set
108288 次浏览

您可以使用 ifdef代替不同的目标。

.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

正如我看到的,命令本身需要 ENV 变量,所以你可以在命令本身中检查它:

.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

如果没有定义 ENV并且某些东西需要它(无论如何,在 GNUMake 中) ,这将导致致命错误。

.PHONY: deploy check-env


deploy: check-env
	...


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


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

(注意,ifndef 和 endf 没有缩进 -它们控制着“看见”的东西,在 Makefile 运行之前生效。“ $(error”使用选项卡缩进,以便它只在规则的上下文中运行。)

您可以创建一个隐式的约束目标,用于检查变量词干中的变量是否已定义,如下所示:

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

然后,在需要断言已定义变量的任何位置添加 guard-ENVVAR目标,如下所示:

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

如果调用 make change-hostname,但没有在调用中添加 HOSTNAME=somehostname,那么将得到一个错误,构建将失败。

到目前为止给出的答案可能存在的一个问题是 make 中的依赖顺序没有定义。例如,跑步:

make -j target

target有一些依赖项时,并不能保证这些依赖项将以任何给定的顺序运行。

解决这个问题的方法(确保在选择食谱之前检查 ENV)是在 make 的第一次传递过程中检查 ENV,不包括任何食谱:

## 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
...

你可以阅读不同的函数/变量使用的 给你$()只是一种方式,明确声明,我们正在比较“没有”。

内嵌变体

在我的 makefile 中,我通常使用这样的表达:

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

原因:

  • 只是一句简单的俏皮话
  • 很紧凑
  • 它靠近使用变量的命令

不要忘记对调试很重要的注释:

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

... 强迫你查找 Makefile 而..。

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

直接解释出了什么问题

全局变体 (用于完整性,但不要求)

在 Makefile 之上,你还可以写:

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

警告:

  • 不要在那个区块使用 tab
  • 小心使用: 如果没有设置 ENV,甚至 clean目标也会失败。否则,请看哈顿的答案,哪个更复杂

我发现最好的答案不能用作一个要求,除了其他虚假的目标。如果作为实际文件的目标的依赖项使用,则使用 check-env将强制重新构建该文件目标。

其他的答案是全局的(例如,Makefile 中的 所有目标需要变量)或者使用 shell,例如,如果 ENV 丢失,make 将终止,而不管目标是什么。

我找到了解决这两个问题的办法

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


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


.PHONY: build
build:
echo "building"

输出看起来像

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

value有一些可怕的警告,但对于这种简单的使用,我相信它是最好的选择。

我知道这很古老,但是我想我应该把我自己的经历告诉未来的游客,因为这样更整洁。

通常,make将使用 sh作为其默认 shell (通过特殊的 SHELL变量设置)。在 sh及其衍生物中,对于 检索未设置或 null 的环境变量时,退出时会显示错误消息来说,只需要做: ${VAR?Variable VAR was not set or null}即可。

扩展这个功能,我们可以编写一个可重用的 make target,如果没有设置环境变量,它可以用来使其他目标失败:

.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!"

注意事项:

  • 转义美元符号($$)需要将扩展延迟到 shell,而不是在 make
  • 使用 test只是为了防止 shell 尝试执行 VAR的内容(它没有其他重要用途)
  • 可以对 .check-env-vars进行简单的扩展,以检查更多的环境变量,每个环境变量只添加一行(例如 @test $${NEWENV?Please set environment variable NEWENV})