为什么在Makefile中使用shell命令导出变量需要很长时间?

qyuhtwio  于 2023-11-21  发布在  Shell
关注(0)|答案(2)|浏览(167)

下面是我的测试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)"

字符串
测试文件VERSION

echo "VERSION=1.2.3" > VERSION


这个makefile工作得很好,直到我用shell调用导出变量。

# 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


取消注解.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


看起来make在这里做了一些递归的事情。
我注意到要执行的命令echo "Test "-X=aaa=1 -X=bbb=2 -X=ccc=3 -X=ddd="2023-10-26T10:09"""显示的延迟很小,但实际输出延迟了几秒钟。
我是不是用了一些不好的方法?为什么会这样?
我的制作版本:

$ make --version
GNU Make 4.4.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

uurv41yg

uurv41yg1#

如果一个make变量还没有值,那么用?=设置它会使它成为一个 * 递归扩展 * 的变量。这意味着在变量本身被扩展之前,它的值中的变量和函数引用不会被扩展。相反,这是递归地完成的,* 每次 * 变量本身被扩展。这是make变量的传统处理。
例如,当您的VERSION变量具有递归扩展的值$(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)时,每次出现$(VERSION)都会导致make启动一个shell,并在其中运行catgrepcut
这些活动在交互式用户的时间尺度上是快速的,但是它们仍然足够慢,因此通过执行足够的迭代来消耗不平凡的时间并不太困难。即使没有.EXPORT_ALL_VARIABLES,您也会执行许多迭代:为了扩展$(SOME_NESTED)一次,我计算了8个shell,8个cuts,4个cats,4个greps和1个date(以及4个echo s,但由于是内置shell,这些可能便宜得多)。
但是如果你.EXPORT_ALL_VARIABLES,那么make在每次启动子进程时都会扩展你的每个变量。这包括每个配方的每一行,以及shell函数的每次运行。即使make足够聪明,能够识别并打破其中固有的递归,你仍然大量增加了正在运行的进程的数量。
我是不是用了一些不好的方法?为什么会这样?

在高层次上,你试图把makefile写得像脚本一样。这是一个导致各种麻烦的基本错误,尽管你经常可以让这样的东西工作。
从策略的Angular 来看,导出 * 所有 * 变量可能是不明智的,但如果你真的想导出所涉及的特定变量,那就没有意义了。
在细节层面上,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)"

字符串
然而,这破坏了条件赋值的语义。如果你想保留从环境(或从同一个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

# ...


但即使这样也不能完全达到你的目标,因为一个从环境中获取值的变量是递归扩展的,并且可以包含变量和函数引用(讨厌!)。如果你想绝对确保这些变量是递归扩展的,那么你需要对每个变量都遵循这个模型:

ifdef VERSION
  VERSION := $(VERSION)
else
  VERSION := $(shell cat VERSION | grep "VERSION" | cut -d'=' -f2)
endif

可选

做这类事情的传统方法是

  • make之前使用某种准备脚本,该脚本可以创建数据文件,甚至是具有适当内容的makefile本身。例如,基于Autotools的项目的configure脚本。

和/或

  • 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')" >> $@


沿着,好好利用(在配方中)构建环境中可以依赖的标准工具。例如,awk

nr9pn0ug

nr9pn0ug2#

假设你正在使用gnu make,另一个选择是使用make结构:

VERSION := $(if $(VERSION),$(VERSION),$(shell grep "VERSION" VERSION | cut -d'=' -f2)
VERSION_PARTS := $(subst ., ,$(VERSION))
VERSION_MAJOR := $(word 1,$(VERSION_PARTS))
VERSION_MINOR := $(word 2,$(VERSION_PARTS))
VERSION_PATCH := $(word 3,$(VERSION_PARTS))

字符串

相关问题