从单个源文件生成多个目标的GNU Makefile规则

123

我正在尝试做以下事情。有一个名为foo-bin的程序,它接收单个输入文件并生成两个输出文件。该程序的简单Makefile规则如下:

file-a.out file-b.out: input.in
    foo-bin input.in file-a.out file-b.out
然而,这并不会告诉make同时生成两个目标文件。在串行运行make时这没问题,但如果尝试运行make -j16或类似的操作,可能会出现问题。 问题是是否存在一种适合这种情况的正确的Makefile规则?显然,这将生成一个DAG,但一些GNU make手册未指定如何处理此类情况。
运行相同代码两次并仅生成一个结果是不可行的,因为计算需要时间(例如:几小时)。仅输出一个文件也非常困难,因为经常将其用作GNUPLOT的输入,后者不知道如何处理仅有部分数据文件的情况。

2
Automake有关于此的文档:https://www.gnu.org/software/automake/manual/html_node/Multiple-Outputs.html - Frank
从GNU make 4.3版本开始,这已经被分组目标支持。您只需要将:更改为&:即可。有关更多详细信息,请参见下面的答案。 - jonas-schulze
9个回答

138
使用具有多个目标的模式规则是关键。在这种情况下,make会假定两个目标都是由单个命令调用创建的。
all: file-a.out file-b.out
file-a%out file-b%out: input.in
    foo-bin input.in file-a$*out file-b$*out
模式规则和普通规则之间的这种解释差异并不完全合理,但对于像这样的情况非常有用,并且在手册中有记录。
只要输出文件的名称具有某些公共子字符串可以与 % 匹配,就可以使用此技巧来处理任意数量的输出文件。(在本例中,公共子字符串是“.”)

4
这并不正确。你的规则指定使用指定的命令从input.in创建与这些模式匹配的文件,但它并没有说它们要同时被创建。如果你实际上以并行方式运行它,make将同时运行相同的命令两次。 - makesaurus
53
在你说它不起作用之前,请先尝试一下;GNU make手册中提到:“模式规则可以有多个目标。与普通规则不同,这并不像有相同前置条件和命令的许多不同规则。如果一个模式规则有多个目标,make知道该规则的命令负责制作所有目标。命令仅执行一次以制作所有目标。” http://www.gnu.org/software/make/manual/make.html#Pattern-Intro - slowdog
5
如果我的输入完全不同呢?比如说,foo.in和bar.src?模式规则支持空模式匹配吗? - Grwlf
3
@dcow: 输出文件只需要共享一个子字符串(即使是一个单独的字符,如“.”,也足够了),不一定是前缀。如果没有公共子串,您可以像richard和deemer描述的那样使用中间目标。 @grwlf: 这意味着"foo.in"和"bar.src"可以这样做:foo%in bar%src: input.in :-) - slowdog
2
如果将相应的输出文件保存在变量中,则可以将配方编写为:$(subst .,%,$(varA) $(varB)): input.in(已在GNU make 4.1中进行测试)。 - stefanct
显示剩余5条评论

63

Make没有任何直观的方法来做到这一点,但有两个不错的解决方法。

首先,如果涉及的目标具有共同的前缀,你可以使用前缀规则(在GNU make中)。也就是说,如果你想修复以下规则:

object.out1 object.out2: object.input
    foo-bin object.input object.out1 object.out2

你可以这样写:

%.out1 %.out2: %.input
    foo-bin $*.input $*.out1 $*.out2

使用通配符变量$*表示匹配到的模式部分。

如果您希望在非GNU Make实现中具备可移植性,或者文件名不能与模式规则匹配,那么有另外一种方法:

file-a.out file-b.out: input.in.intermediate ;

.INTERMEDIATE: input.in.intermediate
input.in.intermediate: input.in
    foo-bin input.in file-a.out file-b.out

这告诉 make 在运行之前 input.in.intermediate 文件将不存在,因此其缺失(或时间戳)不会导致 foo-bin 被错误地运行。而且,无论 file-a.out、file-b.out 还是两者都相对于 input.in 过时,foo-bin 只会运行一次。你可以使用 .SECONDARY 替代 .INTERMEDIATE,这将指示 make 不删除名为 input.in.intermediate 的虚拟文件。该方法也适用于并行构建。

第一行上的分号很重要。它为该规则创建了一个空的 recipe,以便 Make 知道我们真正将更新 file-a.out 和 file-b.out(感谢 @siulkiulki 和其他人指出这一点)。


7
好的,看起来“.INTERMEDIATE”方法运行良好。另请参考Automake文章描述这个问题(但他们试图以可移植的方式解决它)。 - Grwlf
3
这是我看到的最好的答案。还有其他特殊目标也很有趣:https://www.gnu.org/software/make/manual/html_node/Special-Targets.html - Arne Babenhauserheide
3
在我的苹果电脑(OSX)上无法运行这个(GNU Make 3.81)。 - Jorge Bucaran
3
我尝试使用并行构建时发现了一些微妙的问题 --- 依赖关系不总是正确地传递到中间目标(虽然使用-j1构建时一切正常)。此外,如果Makefile被打断并且它保留了“input.in.intermediate”文件,则会导致混乱。 - David Given
6
这个答案有错误。你可以完美地实现并行构建。只需要将 file-a.out file-b.out: input.in.intermediate 修改为 file-a.out file-b.out: input.in.intermediate;。更多细节请参见 https://dev59.com/dZjga4cB1Zd3GeqPFRvT。 - siulkilulki

43

自GNU Make 4.3(2020年1月19日)之后,您可以使用“分组显式目标”。 将:替换为&:

file-a.out file-b.out &: input.in
    foo-bin input.in file-a.out file-b.out

GNU Make 的 NEWS 文件中提到:

新功能:分组显式目标

模式规则一直具有使用单个配方调用生成多个目标的能力。现在可以声明一个显式规则使用单个调用生成多个目标。为此,请在规则中将“:”令牌替换为“&:”。要检测此功能,请在 .FEATURES 特殊变量中搜索“grouped-target”。该实现由 Kaz Kylheku <kaz@kylheku.com> 贡献。


11

我会按照以下方式解决:

file-a.out: input.in
    foo-bin input.in file-a.out file-b.out   

file-b.out: file-a.out
    #do nothing
    noop
在这种情况下,平行构建将“串行化”创建a和b,但由于创建b不执行任何操作,所以它不需要花费时间。

18
实际上,这里存在一个问题。如果按照所述创建了file-b.out,然后神秘地被删除,make将无法重新创建它,因为file-a.out仍然存在。有什么想法吗? - makesaurus
如果您神秘地删除了“file-a.out”,那么上述解决方案将很好地解决问题。这表明一种方法:当仅存在a或b中的一个时,应该按照依赖关系对缺失文件出现为“input.in”的输出进行排序。几个$(widcard...)$(filter...)$(filter-out...)等就可以解决问题。 呜。 - bobbogo
3
PS: foo-bin 显然会首先创建其中一个文件(使用微秒分辨率),因此当两个文件都存在时,您必须确保具有正确的依赖关系顺序。 - bobbogo
2
@bobbogo 要么这样,要么在 foo-bin 调用之后添加 touch file-a.out 作为第二个命令。 - Connor Harris
这里有一个可能的解决方法:在第一条规则中添加touch file-b.out作为第二个命令,并复制第二条规则中的命令。如果任何一个文件丢失或早于input.in,则该命令将仅执行一次,并使用file-b.out重新生成两个文件,其中file-b.outfile-a.out更近。 - chqrlie

10
这基于 @deemer 的第二个答案,它不依赖于模式规则,并且修复了我在使用解决方法时遇到的问题。

这是基于@deemer的第二个答案,它不依赖于模式规则,并修复了我在使用解决方案时遇到的嵌套问题。

file-a.out file-b.out: input.in.intermediate
    @# Empty recipe to propagate "newness" from the intermediate to final targets

.INTERMEDIATE: input.in.intermediate
input.in.intermediate: input.in
    foo-bin input.in file-a.out file-b.out
我本想将这个回答作为对 @deemer 的评论发表的,但我不能这样做,因为我刚刚创建了这个账户并且没有任何声望。
解释:需要使用空的配方来让Make进行正确的簿记,以便将file-a.out和file-b.out标记为已经被重新构建。如果还有另一个依赖于file-a.out的中间目标,那么Make将选择不构建外部中间目标,并宣称:
No recipe for 'file-a.out' and no prerequisites actually changed.
No need to remake target 'file-a.out'.

2
这是我的做法。首先,我总是将前置条件与步骤分开。然后,在这种情况下,设定一个新的目标来执行该菜谱。
all: file-a.out file-b.out #first rule

file-a.out file-b.out: input.in

file-a.out file-b.out: dummy-a-and-b.out

.SECONDARY:dummy-a-and-b.out
dummy-a-and-b.out:
    echo creating: file-a.out file-b.out
    touch file-a.out file-b.out

第一次:
1. 我们尝试构建 file-a.out,但需要先完成 dummy-a-and-b.out,因此 make 运行 dummy-a-and-b.out 的配方。
2. 我们尝试构建 file-b.out,dummy-a-and-b.out 已经是最新的。
第二次及以后:
1. 我们尝试构建 file-a.out:make 查看先决条件,正常的先决条件已经是最新的,次要的先决条件缺失,因此被忽略。
2. 我们尝试构建 file-b.out:make 查看先决条件,正常的先决条件已经是最新的,次要的先决条件缺失,因此被忽略。

1
这个出错了。如果你删除 file-b.out ,那么 make 就无法重新创建它。 - bobbogo
将所有文件:file-a.out和file-b.out添加为第一条规则。如果假设删除了file-b.out,并在命令行上运行make而没有指定目标,则不会重新构建它。 - ctrl-alt-delor
@bobbogo 已修复:请参见上面的评论。 - ctrl-alt-delor

0

从版本4.3开始,GNU make支持所谓的“分组目标”:

  • 新功能:分组显式目标

    模式规则一直具有使用单个调用生成多个目标的能力。现在可以声明显式规则使用单个调用生成多个目标。为了使用这个功能,在规则中将“:”令牌替换为“&:”。要检测此功能,请在.FEATURES特殊变量中搜索“grouped-target”。

    实现由Kaz Kylheku address@hidden贡献

发行说明:https://lists.gnu.org/archive/html/make-w32/2020-01/msg00001.html

文档:https://www.gnu.org/software/make/manual/make.html#Multiple-Targets

简而言之:使用&:代替:


0
作为对@deemer答案的扩展,我将其概括成一个函数。
sp :=
sp +=
inter = .inter.$(subst $(sp),_,$(subst /,_,$1))

ATOMIC=\
    $(eval s1=$(strip $1)) \
    $(eval target=$(call inter,$(s1))) \
    $(eval $(s1): $(target) ;) \
    $(eval .INTERMEDIATE: $(target) ) \
    $(target)

$(call ATOMIC, file-a.out file-b.out): input.in
    foo-bin input.in file-a.out file-b.out

分解:

$(eval s1=$(strip $1))

从第一个参数中删除所有前导/尾随空格

$(eval target=$(call inter,$(s1)))

创建一个名为"目标"的变量,并指定一个唯一值作为中间目标。在这种情况下,该值将是".inter.file-a.out_file-b.out."。

$(eval $(s1): $(target) ;)

创建一个空的配方,以唯一目标作为依赖项的输出。
$(eval .INTERMEDIATE: $(target) ) 

将唯一目标声明为中间值。

$(target)

在函数结尾处引用唯一目标,以便该函数可以直接在配方中使用。

还要注意这里使用 eval 是因为 eval 扩展为空,所以函数的完整扩展只是唯一目标。

必须感谢 GNU Make 中的原子规则,这个函数是从中得到灵感的。


-2

为了防止使用make -jN并行执行多个带有多个输出的规则,请使用.NOTPARALLEL: outputs。 在您的情况下:

.NOTPARALLEL: file-a.out file-b.out

file-a.out file-b.out: input.in
    foo-bin input.in file-a.out file-b.out

".NOTPARALLEL" 防止所有目标并行执行,无论其先决条件如何。 - AleXoundOS
呃,我觉得不支持并行操作的makefile是有问题的。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接