为什么 GNU Make 会错过 foreach 的最后一次迭代?

Why would GNU Make miss last iteration of foreach?

给定以下 Makefile:

PROGRAMS := aprogram
SYSTEMS := linux windows
ARCHS := 386 amd64

define PROGRAM_template =
CUR_PROG := _build/bin/$(1)_$(2)_$(3)/$(1)
$(CUR_PROG): export GOOS = $(2)
$(CUR_PROG): export GOARCH = $(3)
$(CUR_PROG):
    @echo "$(CUR_PROG)"
PROG_TARGETS += $(CUR_PROG)
endef

$(foreach prog,$(PROGRAMS),$(foreach sys,$(SYSTEMS),$(foreach arch,$(ARCHS),$(eval $(call PROGRAM_template,$(prog),$(sys),$(arch))))))

all: $(PROG_TARGETS)

输出是:

[0] % make all
_build/bin/aprogram_linux_386/aprogram
_build/bin/aprogram_linux_amd64/aprogram
_build/bin/aprogram_windows_386/aprogram

如果我添加另一个架构fakearch,输出是:

[0] % make all
_build/bin/aprogram_linux_386/aprogram
_build/bin/aprogram_linux_amd64/aprogram
_build/bin/aprogram_linux_fakearch/aprogram
_build/bin/aprogram_windows_386/aprogram
_build/bin/aprogram_windows_amd64/aprogram

这让我觉得只是没有执行最后一次迭代。我该如何纠正?

您需要 eval 定义中的临时变量,因为 make 在函数调用中同时扩展所有引用。

PROGRAMS := aprogram
SYSTEMS := linux windows
ARCHS := 386 amd64

define PROGRAM_template =
$(eval CUR_PROG := _build/bin/$(1)_$(2)_$(3)/$(1))
$(CUR_PROG): export GOOS = $(2)
$(CUR_PROG): export GOARCH = $(3)
$(CUR_PROG):
    @echo "$(CUR_PROG)"
PROG_TARGETS += $(CUR_PROG)
endef

$(foreach prog,$(PROGRAMS),$(foreach sys,$(SYSTEMS),$(foreach arch,$(ARCHS),$(eval $(call PROGRAM_template,$(prog),$(sys),$(arch))))))

all: $(PROG_TARGETS)

double-eval 会起作用。但更常见的方法是通过 $$ 转义来延迟内部变量 CUR_PROG 的扩展,如下所示:

PROG_TARGETS :=

define PROGRAM_template =
CUR_PROG := _build/bin/$(1)_$(2)_$(3)/$(1)
$$(CUR_PROG): export GOOS = $(2)
$$(CUR_PROG): export GOARCH = $(3)
$$(CUR_PROG):
        @echo "$$(CUR_PROG)"
PROG_TARGETS += $$(CUR_PROG)
endef

这是因为您首先使用了 call,然后是 evalcall 函数将在 eval 看到它们之前展开它的参数。

你的循环中有这个:

$(eval $(call PROGRAM_template,$(prog),$(sys),$(arch)))

为了扩展这个make首先会扩展内部函数:

$(call PROGRAM_template,$(prog),$(sys),$(arch))

这将 PROGRAM_template 扩展为一个简单的字符串扩展:请记住这不是 eval,因此它不会像生成文件一样解释文本,它只是扩展值。所以第一行的赋值没有生效,因为我们还没有运行 eval。在您第一次通过循环的原始实现中,CUR_PROGcall 之前没有值,因此 call 扩展为:

CUR_PROG := _build/bin/aprogram_linux_386/aprogram
: export GOOS = linux
: export GOARCH = 386
:
        @echo ""
PROG_TARGETS += 

然后该字符串被提供给eval进行评估,但它基本上是一个no-op,除了设置CUR_PROG

下一次循环,CUR_PROG 仍然有以前的值,所以当调用扩展字符串时,您得到:

CUR_PROG := _build/bin/aprogram_linux_amd64/aprogram
_build/bin/aprogram_linux_386/aprogram: export GOOS = linux
_build/bin/aprogram_linux_386/aprogram: export GOARCH = amd64
_build/bin/aprogram_linux_386/aprogram:
        @echo "_build/bin/aprogram_linux_386/aprogram"
PROG_TARGETS += _build/bin/aprogram_linux_386/aprogram

等基本上,每次通过循环你都使用 previous 循环中的 CUR_PROG 值,因为扩展发生在 call 函数期间,但是变量的重新分配直到 eval 函数才会发生。

通过转义 CUR_PROG 确保 call 不会扩展它,这意味着它将留给 eval 扩展。例如,在 call 扩展完成后,我上面的版本结果将是这样的:

CUR_PROG := _build/bin/aprogram_linux_386/aprogram
$(CUR_PROG): export GOOS = linux
$(CUR_PROG): export GOARCH = 386
$(CUR_PROG):
        @echo "$(CUR_PROG)"
PROG_TARGETS += $(CUR_PROG)

这就是你想要的。

理解eval的一个有用的调试工具是将其替换为info函数;这将导致 make 打印出 eval 看到的字符串,这有助于可视化正在发生的事情:

$(foreach ...,$(info $(call PROGRAM_template,$(prog),$(sys),$(arch))))))

此处解决方案的另一个选择是根本不使用 CUR_PROG 变量。在此示例中,您可以完全从 define 中取出食谱。这同样有效:

define PROGRAM_template =
PROG_TARGETS += _build/bin/$(1)_$(2)_$(3)/$(1)
_build/bin/$(1)_$(2)_$(3)/$(1): export GOOS = $(2)
_build/bin/$(1)_$(2)_$(3)/$(1): export GOARCH = $(3)
endef

$(foreach prog,$(PROGRAMS),$(foreach sys,$(SYSTEMS),$(foreach arch,$(ARCHS),$(eval $(call PROGRAM_template,$(prog),$(sys),$(arch))))))

$(PROG_TARGETS):
        @echo "$@"