Makefile模式规则中wildcards无法匹配目录文件的问题
wildcard与模式规则%结合失效问题 我试图用Make从特定文件和共享文件构建一组Markdown文档,原Makefile代码如下:
PKGS := foo bar baz app DEFAULT := DEPENDENCIES.md RULES.md BUILD := .build VPATH := .:packages $(BUILD)/%.history.md: $(DEFAULT) $(wildcard %/RULES.*.md) $(foreach f,$(DEFAULT),%/$(f)) echo "!! BUILDING NOW $@" echo $^ echo
对应的目录结构:
. ├── ABSTRACT.md ├── DEPENDENCIES.md ├── IMPLEMENTATION.md ├── Makefile ├── packages │ ├── foo │ │ ├── ABSTRACT.md │ │ ├── DEPENDENCIES.md │ │ ├── IMPLEMENTATION.md │ │ ├── RULES.0.md -> ../COMMON.md │ │ └── RULES.md │ ├── COMMON.md │ ├── bar │ │ ├── ABSTRACT.md │ │ ├── DEPENDENCIES.md │ │ ├── IMPLEMENTATION.md │ │ └── RULES.md │ ├── baz │ │ ├── ABSTRACT.md │ │ ├── DEPENDENCIES.md │ │ ├── IMPLEMENTATION.md │ │ └── RULES.md │ └── app │ ├── ABSTRACT.md │ ├── DEPENDENCIES.md │ ├── IMPLEMENTATION.md │ ├── RULES.0.md -> ../COMMON.md │ └── RULES.md └── RULES.md
问题现象
$(wildcard %/RULES.*.md)始终展开为空列表,执行make .build/foo.history.md时,预期依赖项包含packages/foo/RULES.0.md,但实际输出中缺少该文件。
为排查问题,我添加了测试目标:
# 模式规则中%作为目录的情况正常工作 %.static: %/RULES.0.md echo $@ -> $^ %.semi: %/RULES.*.md echo $@ -> $^ %.x: $(wildcard packages/%/RULES.*.md) echo $@ -> $^ # 单独使用*通配符正常工作 app.y: $(wildcard packages/app/RULES.*.md) echo $@ -> $^ # 两者结合失效:预期目标为foo.z时,依赖展开为foo/RULES.*.md(通过VPATH查找) %.z: $(wildcard %/RULES.*.md) echo $@ -> $^
测试结果:
$ make app.static --just-print echo app.static -> packages/app/RULES.0.md $ make app.semi --just-print make: *** No rule to make target 'app.semi'. Stop. $ make app.x --just-print echo app.x -> $ make app.y --just-print echo app.y -> packages/app/RULES.0.md $ make app.z --just-print echo app.z ->
我尝试过.SECONDEXPANSION但未成功,且看到有人不推荐使用该方法。另外,$(foreach f,$(DEFAULT),%/$(f))能正常展开为packages/app/DEPENDENCIES.md packages/app/RULES.md,这更让我困惑为何$(wildcard)无法正常工作,特此求助解决方法。
问题原因与解决方案
核心原因
wildcard函数是在Makefile解析阶段执行的,此时模式规则中的%还只是一个通配符字符,并未被替换为实际的包名(比如foo或app),所以$(wildcard %/RULES.*.md)会去查找名为%的目录下的RULES.*.md文件,自然找不到任何内容。
而$(foreach f,$(DEFAULT),%/$(f))能正常工作,是因为这个表达式的展开是在目标匹配阶段完成的:当Make匹配到具体目标(比如.build/app.history.md)后,会先把%替换为app,再执行foreach展开,此时路径是合法的。
解决方案1:正确使用.SECONDEXPANSION
之前尝试失败大概率是转义处理不当,开启二次扩展后,需要用$$(wildcard ...)来延迟wildcard的执行时机,让%先被替换为实际包名,再执行通配符匹配:
.SECONDEXPANSION: $(BUILD)/%.history.md: $(DEFAULT) $$(wildcard %/RULES.*.md) $(foreach f,$(DEFAULT),%/$(f)) echo "!! BUILDING NOW $@" echo $^ echo
这里的$$是为了转义:第一次解析时$$会变成$,二次扩展时$(wildcard %/RULES.*.md)中的%已经被替换为实际包名,此时执行wildcard就能正确找到目标文件。
解决方案2:预生成所有依赖文件列表(无需二次扩展)
先提前扫描所有包的RULES.*.md文件,再通过模式过滤对应到目标:
PKGS := foo bar baz app DEFAULT := DEPENDENCIES.md RULES.md BUILD := .build VPATH := .:packages # 预扫描所有包的RULES.*.md文件 ALL_RULES_VARIANTS := $(wildcard packages/*/RULES.*.md) # 为每个目标匹配对应的RULES变体文件 $(BUILD)/%.history.md: $(DEFAULT) $(filter packages/%/RULES.*.md,$(ALL_RULES_VARIANTS)) $(foreach f,$(DEFAULT),%/$(f)) echo "!! BUILDING NOW $@" echo $^ echo
这种方式通过filter函数,把预扫描到的文件中对应当前%包的部分筛选出来,作为依赖项。
内容的提问来源于stack exchange,提问作者Kevin

