我试图编写一个Makefile,其中多个源文件(在我的情况下是markdown)创建多个目标文件(pdfs)。但是,生成的目标文件在文件名中有额外的字符无法预测(它恰好是在源代码中编码的版本号),但理想情况下Makefile不必读取源本身。
所以,例如:
file1.md => file1-v1.pdf
file2.md => file2-v2.pdf
...
我可以根据目标名称计算源名称(通过排除连字符后面的任何内容并添加.md),但无法计算给定源的目标名称。
是否可以编写一个仅构建源已更新的目标的Makefile?
答案 0 :(得分:2)
这将是丑陋的,但它会起作用。
正如通常与Make一样,我们的问题分为两个问题:
1.构建目标列表
2.建立他们
假设我们有五个md文件映射到pdf文件(我们事先不知道其名称):
file1.md => file1-v1.pdf
file2.md => file2-v1.pdf
file3.md => file3-v1.pdf
file4.md => file4-v1.pdf
file5.md => file5-v1.pdf
我们不能将实际输出文件名用作目标,因为我们事先并不知道它们,但我们看到五个输入文件,并且知道我们必须为每个文件构建一个输出文件。 目前,虚拟目标名称将:
file1-dummy.pdf: file1.md
zap file1.md
当Make执行此规则时,它会生成文件file1-v1.pdf
。它不会产生名为file1-dummy.pdf
的文件这一事实令人不安,但并不是一个严重的问题。我们可以把它变成一个模式规则:
%-dummy.pdf: %.md
zap $<
然后我们要做的就是将现有输入文件列表(file1.md
,file2.md
,...)转换为虚拟目标列表(file1-dummy.pdf
,{{1 },...),并构建它们。到目前为止,非常好。
但是假设某些输出文件已经存在。如果file2-dummy.pdf
已经存在 - 并且比file2-v2.pdf
更新 - 那么我们希望Make not rebuild it(通过尝试构建file2.md
)。在这种情况下,我们希望file2-dummy.pdf
位于目标列表中,其规则如下:
file2-v2.pdf
这个不容易变成模式规则,因为 Make不能很好地处理通配符,并且无法在单个短语中处理多个通配符,而不是没有很多笨拙。但是有一种方法可以编写一个涵盖这两种情况的规则。首先请注意,我们可以使用此kludge在连字符之前获取变量的一部分:
file2-v2.pdf: file2.md
zap $<
有了这个,并且使用secondary expansion,我们可以编写一个适用于这两种情况的模式规则,并构建一个可以利用它的目标列表:
$(basename $(subst -,.,$(VAR)))
答案 1 :(得分:1)
Make&#39>算法始终以最终输出产品开始,然后向后移动到源文件,以查看需要更新的内容。
因此,您必须能够将最终输出产品枚举为目标名称,并将其与生成该输出的输入相关联,以使其生效。
这就是为什么make不是构建Java的好工具,例如,因为输出文件名不容易映射到输入文件名。
因此,您必须至少有一个目标/先决条件对,它是可派生的(对于隐式规则),或者是可状态的(对于显式规则) - 也就是说,在您编写makefile时已知。如果你没有,那么标记文件是你唯一的选择。请注意,除了已知的先决条件之外,您还可以添加额外生成的非衍生先决条件(例如,在编译器中,您可以添加头文件作为与源文件名无关的先决条件)。
答案 2 :(得分:1)
@ Beta的回答是有用且有帮助的,但我需要一个解决方案(使用GNU Make 4.1),当目标文件名与输入文件名没有相似之处时,例如,如果它是从其内容生成的。我想出了以下内容,它将每个文件与*.in
匹配,并通过读取源文件的内容,附加.txt
并将其用作要创建的文件名来创建文件。 (例如,如果test.in
存在且包含foo
,则makefile将创建foo.txt
文件。)
SRCS := $(wildcard *.in)
.PHONY: all
all: all_s
define TXT_template =
$(2).txt: $(1)
touch $$@
ALL += $(2).txt
endef
$(foreach src,$(SRCS),$(eval $(call TXT_template, $(src), $(shell cat $(src)))))
.SECONDARY_EXPANSION:
all_s: $(ALL)
解释:
define
块定义了从.in
文件生成文本文件所需的配方。它是一个带两个参数的函数; $(1)
是.in.
文件,$(2)
是其内容,或输出文件名的基础。将touch
替换为输出的任何内容。我们必须使用$$@
,因为eval会将所有内容扩展一次,但我们希望在此扩展后留下$@
。由于我们必须收集所有生成的目标,因此我们知道所有的内容,ALL
行将目标累积到一个变量中。 foreach
行遍历每个源文件,使用源文件名和文件内容调用函数(即我们想要成为目标的名称,这里你通常使用生成的任何脚本所需的文件名),然后评估结果块,动态地将配方添加到make
。感谢Beta解释.SECONDARY_EXPANSION
;我需要它的原因并不完全清楚,但它有效(将all: $(ALL)
置于顶部不起作用)。顶部的all:
取决于底部all_s:
的二次扩展,不知何故,这种魔法使其有效。欢迎评论。