使用SECONDEXPANSION基于目标特定变量设依赖的GNU Make版本兼容问题
我希望基于目标特定变量设置依赖,因此使用SECONDEXPANSION编写了如下测试Makefile:
foo.h: touch foo.h bar.h: touch bar.h .SECONDEXPANSION: baz.h: $$(HEADER_NAME) echo "In baz.h with HEADER_NAME = $(HEADER_NAME)" foo: HEADER_NAME=foo.h foo: baz.h file $(HEADER_NAME) bar: HEADER_NAME=bar.h bar: baz.h file $(HEADER_NAME) clean: rm -f *.h
但我的系统上安装了多个GNU Make版本,该Makefile仅在4.4.1版本中正常工作。4.3和4.2.1版本始终不会执行foo.h或bar.h目标(仿佛第二次展开时HEADER_NAME为空)。我是否错误使用了SECONDEXPANSION,仅在v4.4.1中偶然生效?还是早期版本存在问题?我确认这并非新增特性,因为4.2.1版本的文档已包含SECONDEXPANSION。
问题原因
你没有错误使用SECONDEXPANSION,问题出在GNU Make的版本行为差异上:
在GNU Make 4.4之前的版本中,目标特定变量(比如foo: HEADER_NAME=foo.h)的作用域仅限于目标本身及其执行命令,不会传递给依赖规则的二次展开阶段。也就是说,当处理baz.h: $$(HEADER_NAME)时,早期版本无法获取到foo或bar目标定义的HEADER_NAME变量,导致二次展开后依赖为空,因此不会触发foo.h或bar.h的构建。
而GNU Make 4.4改进了目标特定变量的作用域规则,允许它们传递给通过SECONDEXPANSION展开的依赖目标,所以你的写法在4.4.1中能正常工作。
修正方案
如果需要兼容4.3及更早版本,可以调整Makefile的规则结构,确保HEADER_NAME在依赖展开时能被正确识别:
方案1:直接让顶层目标依赖对应头文件
将foo和bar的依赖直接包含对应的头文件,同时保留baz.h的规则:
foo.h: touch foo.h bar.h: touch bar.h .SECONDEXPANSION: baz.h: echo "In baz.h with HEADER_NAME = $(HEADER_NAME)" foo: HEADER_NAME=foo.h foo: $$(HEADER_NAME) baz.h file $(HEADER_NAME) bar: HEADER_NAME=bar.h bar: $$(HEADER_NAME) baz.h file $(HEADER_NAME) clean: rm -f *.h
方案2:用eval动态生成规则
通过eval为每个目标生成对应的baz.h依赖规则,确保变量作用域正确:
foo.h: touch foo.h bar.h: touch bar.h define generate_target $(1): HEADER_NAME=$(1).h $(1): baz.h-$(1) file $$(HEADER_NAME) baz.h-$(1): $$(HEADER_NAME) echo "In baz.h with HEADER_NAME = $$(HEADER_NAME)" endef $(foreach t,foo bar,$(eval $(call generate_target,$(t)))) clean: rm -f *.h baz.h-*
这两种写法都能在GNU Make 4.2.1、4.3及4.4.1版本中正常工作。
内容的提问来源于stack exchange,提问作者Carcophan

