这是我的测试 Makefile:
VERSION ?= $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
VERSION_MAJOR ?= $(shell echo $(VERSION) | cut -d'.' -f1)
VERSION_MINOR ?= $(shell echo $(VERSION) | cut -d'.' -f2)
VERSION_PATCH ?= $(shell echo $(VERSION) | cut -d'.' -f3)
DATE ?= "$(shell date +"%Y-%m-%dT%H:%M")"
SOME_NESTED ?= "-X=aaa=$(VERSION_MAJOR) -X=bbb=$(VERSION_MINOR) -X=ccc=$(VERSION_PATCH) -X=ddd=$(DATE)"
#.EXPORT_ALL_VARIABLES:
t:
echo "Test $(SOME_NESTED)"
Run Code Online (Sandbox Code Playgroud)
和一个测试文件VERSION:
VERSION ?= $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
VERSION_MAJOR ?= $(shell echo $(VERSION) | cut -d'.' -f1)
VERSION_MINOR ?= $(shell echo $(VERSION) | cut -d'.' -f2)
VERSION_PATCH ?= $(shell echo $(VERSION) | cut -d'.' -f3)
DATE ?= "$(shell date +"%Y-%m-%dT%H:%M")"
SOME_NESTED ?= "-X=aaa=$(VERSION_MAJOR) -X=bbb=$(VERSION_MINOR) -X=ccc=$(VERSION_PATCH) -X=ddd=$(DATE)"
#.EXPORT_ALL_VARIABLES:
t:
echo "Test $(SOME_NESTED)"
Run Code Online (Sandbox Code Playgroud)
在我使用 shell 调用导出变量之前,这个 makefile 工作得很好。
echo "VERSION=1.2.3" > VERSION
Run Code Online (Sandbox Code Playgroud)
取消注释,.EXPORT_ALL_VARIABLES该命令需要 5 秒才能执行。
我make -d t可以看到这样的大量日志:
Makefile:1: not recursively expanding VERSION to export to shell function
Makefile:8: not recursively expanding SOME_NESTED to export to shell function
Makefile:2: not recursively expanding VERSION_MAJOR to export to shell function
Makefile:3: not recursively expanding VERSION_MINOR to export to shell function
Makefile:4: not recursively expanding VERSION_PATCH to export to shell function
Makefile:6: not recursively expanding DATE to export to shell function
Makefile:2: not recursively expanding VERSION_MAJOR to export to shell function
Makefile:3: not recursively expanding VERSION_MINOR to export to shell function
Makefile:1: not recursively expanding VERSION to export to shell function
Makefile:6: not recursively expanding DATE to export to shell function
Makefile:1: not recursively expanding VERSION to export to shell function
Makefile:2: not recursively expanding VERSION_MAJOR to export to shell function
Makefile:3: not recursively expanding VERSION_MINOR to export to shell function
Makefile:4: not recursively expanding VERSION_PATCH to export to shell function
Makefile:6: not recursively expanding DATE to export to shell function
Makefile:2: not recursively expanding VERSION_MAJOR to export to shell function
Makefile:3: not recursively expanding VERSION_MINOR to export to shell function
Makefile:4: not recursively expanding VERSION_PATCH to export to shell function
Run Code Online (Sandbox Code Playgroud)
看起来make这里正在递归地做一些事情。
我注意到要执行的命令echo "Test "-X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd="2023-10-26T10:09"""显示时几乎没有延迟,但实际输出延迟了几秒钟。
我是否使用了一些不好的做法?为什么会发生这样的事?
我的制作版本:
# use the original makefile
$ time make t
echo "Test "-X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd="2023-10-26T10:00"""
Test -X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd=2023-10-26T10:00
make t 0.02s user 0.00s system 116% cpu 0.021 total
# uncomment the .EXPORT_ALL_VARIABLES
$ time make t
echo "Test "-X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd="2023-10-26T10:09"""
### (hanged for seconds here)
Test -X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd=2023-10-26T10:09
make t 5.15s user 0.41s system 111% cpu 4.979 total
Run Code Online (Sandbox Code Playgroud)
如果make变量还没有值,则将其设置为?=使其成为递归扩展变量。这意味着在变量本身扩展之前,其值中的变量和函数引用不会扩展。相反,每次扩展变量本身时都会递归地完成此操作。这是传统的变量处理方式make。
例如,当您的VERSION变量具有递归扩展 value时$(shell cat VERSION | grep "VERSION" | cut -d'=' -f2),每次出现 都会$(VERSION)导致make启动 shell,并在其中运行cat、grep和cut。
这些活动在交互式用户的时间尺度上单独来说是很快的,但它们仍然足够慢,因此通过执行足够的迭代来消耗不平凡的时间并不难。即使没有.EXPORT_ALL_VARIABLES,您也会执行多次迭代:为了扩展$(SOME_NESTED)一次,我数了 8 个 shell,8 cuts、 4 cats、 4greps和 1 date(和 4 echo,但由于是 shell 内置,这些可能要便宜得多)。这已经相当重了。
但是,如果您每次启动子进程时.EXPORT_ALL_VARIABLES都make扩展每个变量。这包括每个配方的每一行,以及或多或少的shell函数的每次运行。尽管make足够聪明,可以识别并打破其中固有的递归,但您仍然大大增加了正在运行的进程数量。
我是否使用了一些不好的做法?为什么会发生这样的事?
在较高的层次上,您尝试编写一个 makefile,就好像它是一个脚本一样。这是一个根本性的错误,会导致各种麻烦,尽管你经常可以坚持下去,让这样的事情发挥作用。
从策略的角度来看,导出所有变量可能是不明智的,但如果您确实想导出所涉及的特定变量,那就没有意义了。
在细节层面上,GNU make“功能”是$(shell)对 POSIX 的扩展make。如果你想要可移植性,那么使用它们中的任何一个都是错误的。即使可移植性不是您关心的问题,大多数 GNU 扩展最好还是谨慎使用,尤其$(shell)如此。
话虽如此,这有点像兔子洞。假设您不是特别想要变量的延迟递归扩展,您可以通过使用另一个扩展来通过使用分配给变量来立即扩展:=变量,从而挽救您的性能,如下所示:
VERSION := $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
VERSION_MAJOR := $(shell echo $(VERSION) | cut -d'.' -f1)
VERSION_MINOR := $(shell echo $(VERSION) | cut -d'.' -f2)
VERSION_PATCH := $(shell echo $(VERSION) | cut -d'.' -f3)
DATE := "$(shell date +"%Y-%m-%dT%H:%M")"
SOME_NESTED := "-X=aaa=$(VERSION_MAJOR) -X=bbb=$(VERSION_MINOR) -X=ccc=$(VERSION_PATCH) -X=ddd=$(DATE)"
Run Code Online (Sandbox Code Playgroud)
然而,这破坏了条件赋值语义。如果您想保留从环境(或从同一 makefile 中的早期分配)接受所有这些变量的值的行为,同时还为它们提供立即扩展的值,那么您需要使用另一个 GNU 扩展:条件。例如,
ifndef VERSION
VERSION := $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
endif
ifndef VERSION_MAJOR
VERSION_MAJOR := $(shell echo $(VERSION) | cut -d'.' -f1)
endif
# ...
Run Code Online (Sandbox Code Playgroud)
但即使这也不是达到您目标的全部方法,因为从环境中获取其值的变量是递归扩展的,并且可以包含变量和函数引用(讨厌!)。如果您想绝对确保这些变量被递归扩展,那么您需要对每个变量都遵循以下模型:
ifdef VERSION
VERSION := $(VERSION)
else
VERSION := $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
endif
Run Code Online (Sandbox Code Playgroud)
做这类事情的传统方法是
在前面使用某种准备脚本来make创建数据文件,甚至是具有适当内容的 makefile 本身。例如,configure基于 Autotools 的项目的脚本。
和/或
将make构建时生成的内容存储在文件中而不是变量中。例子:
t: unnested
echo "Test $$(<unnested)"
.PHONY: unnested
unnested:
@awk -F= '$$1 == "VERSION" { split($$2, v, "."); printf "-X=aaa=%s -X=bbb=%s -X=ccc=%s", v[1], v[2], v[3] }' VERSION > $@
@echo " -X=ddd=$$(date +'%Y-%m-%dT%H:%M')" >> $@
Run Code Online (Sandbox Code Playgroud)
除此之外,充分利用(在配方中)您可以依赖的构建环境中存在的标准工具。 awk, 例如。