如何使用 GNU make 使存档内容真正成为存档提取规则的目标?

How can I make archive contents true targets of an archive extraction rule with GNU make?

我想编写几个规则来提取 tar 档案的内容以生成一些文件,然后将这些文件用作其他规则的输入依赖项。我希望这甚至可以用于并行构建。我没有使用递归 make。

首先,对于马拉松式的问题,我深表歉意,但我认为我无法用较短的形式很好地解释它。

想想 untarring 一组源文件,然后使用存储在存档外部的规则对它们进行编译,以生成各种构建工件,然后这些工件又会被进一步使用。我不是在寻求导致遗漏这个问题的其他安排。想当然地认为我有充分的理由这样做。 :)

我将用一个人为的例子来证明我的问题。当然,我tar了解了一些基本知识:

TAR := test.tar.bz2

CONTENTS := $(addprefix out/,$(filter-out %/,$(shell tar -tf $(TAR))))

out: $(TAR)
        rm -rf out
        mkdir out
        tar -xvf $< -C out --touch || (rm -rf out; exit 1)

$(CONTENTS): out

sums: $(CONTENTS)
        md5sum $^ > $@

.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean
all: sums
clean:
        rm -rf out sums

这里的思路是,既然$(CONTENTS)是archive中的所有文件,而且都依赖于out,那么对运行 sums tar得到我们需要结束提取档案。

不幸的是,如果您在上一次构建之后仅更新 test.tar.bz2 时使用并行调用,这不会(总是)起作用,因为 make 可能决定检查 $( CONTENTS) before 运行ning out 规则,这意味着它认为每个来源都早于 sums,所以没有什么可以做:

$ make clean
rm -rf out sums

$ make -j6
rm -rf out
mkdir out
tar -xvf test.tar.bz2 -C out --touch || (rm -rf out; exit 1)
data.txt
file
weird.file.name
dir/
dir/another.c
dir/more
md5sum out/data.txt out/file out/weird.file.name out/dir/another.c out/dir/more > sums

$ touch test.tar.bz2 
$ make -j6
rm -rf out
mkdir out
tar -xvf test.tar.bz2 -C out --touch || (rm -rf out; exit 1)
data.txt
file
weird.file.name
dir/
dir/another.c
dir/more

糟糕! sums 规则没有 运行!

因此,下一个尝试是告诉 make untar 规则实际上确实直接生成了所有 $(CONTENTS)。这似乎更好,因为我们告诉 make 真正发生了什么,所以它知道什么时候忘记 targets 的任何缓存时间戳,当它们通过他们的规则重新制作时。

首先,让我们看看什么似乎有效,然后我将解决我的问题:

TAR := test.tar.bz2

CONTENTS := $(addprefix out/,$(filter-out %/,$(shell tar -tf $(TAR))))

# Here's the change.
$(addprefix %/,$(patsubst out/%,%,$(CONTENTS))): $(TAR)
        rm -rf out
        mkdir out
        tar -xvf $< -C out --touch || (rm -rf out; exit 1)

sums: $(CONTENTS)
        md5sum $^ > $@

.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean
all: sums
clean:
        rm -rf out sums

在这种情况下,我们实际上得到了一条规则:

%/data.txt %/file %/weird.file.name %/dir/another.c %/dir/more: test.tar.bz2
        rm -rf out
        mkdir out
        tar -xvf $< -C out --touch || (rm -rf out; exit 1)

现在您可以看到我将输出强制放入 out 目录的原因之一:给我一个使用 % 的地方,以便我可以使用模式规则。即使这里没有强模式,我也被迫使用模式规则,因为这是唯一可以告诉 make 一个规则从一次调用创建多个输出文件的方法。 (不是吗?)

如果有任何文件被触及(对我的用例不重要)或者 test.tar.bz2 文件被触及,即使在并行构建中,这也有效,因为 make 有它需要的信息:运行使用此食谱生成所有这些文件并将更改它们的所有时间戳。

例如,在上一次成功构建之后:

$ touch test.tar.bz2 
$ make -j6
rm -rf out
mkdir out
tar -xvf test.tar.bz2 -C out --touch || (rm -rf out; exit 1)
data.txt
file
weird.file.name
dir/
dir/another.c
dir/more
md5sum out/data.txt out/file out/weird.file.name out/dir/another.c out/dir/more > sums

那么,如果我有一个可行的解决方案,我的问题是什么?

好吧,我有很多这样的档案要提取,每个档案都有自己的一组 $(CONTENTS)。我可以做到这一点,但是麻烦在于编写一个好的模式规则。由于每个档案都需要定义自己的规则,因此即使档案具有相似(或相同)的内容,每个规则的模式也不能重叠。这意味着提取文件的输出路径 必须 对于每个存档都是唯一的,如:

TAR := test.tar.bz2

CONTENTS := $(addprefix out.$(TAR)/,$(filter-out %/,$(shell tar -tf $(TAR))))

$(patsubst out.$(TAR)/%,out.\%/%,$(CONTENTS)): $(TAR)
        rm -rf out.$(TAR)
        mkdir out.$(TAR)
        tar -xvf $< -C out.$(TAR) --touch || (rm -rf out.$(TAR); exit 1)

sums: $(CONTENTS)
        md5sum $^ > $@

.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean
all: sums
clean:
        rm -rf out.$(TAR) sums

因此,这可以与正确的 target-specific 变量一起使用,但现在这意味着提取点都是 "ugly" 以一种非常特定的方式绑定到makefile 的构造方式:

$ make -j6
rm -rf out.test.tar.bz2
mkdir out.test.tar.bz2
tar -xvf test.tar.bz2 -C out.test.tar.bz2 --touch || (rm -rf out.test.tar.bz2; exit 1)
data.txt
file
weird.file.name
dir/
dir/another.c
dir/more
md5sum out.test.tar.bz2/data.txt out.test.tar.bz2/file out.test.tar.bz2/weird.file.name out.test.tar.bz2/dir/another.c out.test.tar.bz2/dir/more > sums

我采取的下一个自然步骤是尝试将静态模式规则与 multiple-targets-via-pattern-rule 方法相结合。这将使我保持模式非常通用,但将它们的应用限制在一组特定的 targets:

TAR := test.tar.bz2

CONTENTS := $(addprefix out/,$(filter-out %/,$(shell tar -tf $(TAR))))

# Same as second attempt, except "$(CONTENTS):" static pattern prefix
$(CONTENTS): $(addprefix %/,$(patsubst out/%,%,$(CONTENTS))): $(TAR)
        rm -rf out
        mkdir out
        tar -xvf $< -C out --touch || (rm -rf out; exit 1)

sums: $(CONTENTS)
        md5sum $^ > $@

.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean
all: sums
clean:
        rm -rf out sums

太棒了!除了它不起作用:

$ make
Makefile:5: *** multiple target patterns.  Stop.
$ make --version
GNU Make 4.0

那么,有没有办法通过静态模式规则使用多个 target 模式?如果没有,是否有另一种方法可以实现上面最后一个工作示例中的内容,但不限制输出路径来制作独特的模式?我基本上需要告诉 make "when you unpack this archive, all of the files in this directory (which I am willing to enumerate if necessary) have new timestamps"。当且仅当它解压缩存档时我可以强制 make 到 restart 的解决方案也是可以接受的,但不太理想。

您的原始 makefile 的问题是名称冲突。您有一个名为 out 的目标(非虚假)和一个名为 out 的目录。 make 认为它们是同一回事,因此感到非常困惑。

(注意:我在您的第一个 makefile 中添加了 .SUFFIXES: 以减少一些噪音,但它没有改变任何东西。-r-R 标志禁用 make built-在规则和变量中也用于降噪。)

$ make clean
....
$ make -j6
....
$ touch test.tar.bz2
$ make -rRd -j6
....
Considering target file 'all'.
 File 'all' does not exist.
  Considering target file 'sums'.
    Considering target file 'out/data.txt'.
     Looking for an implicit rule for 'out/data.txt'.
     No implicit rule found for 'out/data.txt'.
      Considering target file 'out'.
        Considering target file 'test.tar.bz2'.
         Looking for an implicit rule for 'test.tar.bz2'.
         No implicit rule found for 'test.tar.bz2'.
         Finished prerequisites of target file 'test.tar.bz2'.
        No need to remake target 'test.tar.bz2'.
       Finished prerequisites of target file 'out'.
       Prerequisite 'test.tar.bz2' is older than target 'out'.
      No need to remake target 'out'.
     Finished prerequisites of target file 'out/data.txt'.
     Prerequisite 'out' is older than target 'out/data.txt'.
    No recipe for 'out/data.txt' and no prerequisites actually changed.
    No need to remake target 'out/data.txt'.
.... # This following set of lines repeats for all the other files in the tarball.
    Considering target file 'out/file'.
     Looking for an implicit rule for 'out/file'.
     No implicit rule found for 'out/file'.
      Pruning file 'out'.
     Finished prerequisites of target file 'out/file'.
     Prerequisite 'out' is older than target 'out/file'.
    No recipe for 'out/file' and no prerequisites actually changed.
    No need to remake target 'out/file'.
....
   Finished prerequisites of target file 'sums'.
   Prerequisite 'out/data.txt' is older than target 'sums'.
   Prerequisite 'out/file' is older than target 'sums'.
   Prerequisite 'out/weird.file.name' is older than target 'sums'.
   Prerequisite 'out/dir/more' is older than target 'sums'.
   Prerequisite 'out/dir/another.c' is older than target 'sums'.
  No need to remake target 'sums'.
 Finished prerequisites of target file 'all'.
Must remake target 'all'.
Successfully remade target file 'all'.
make: Nothing to be done for 'all'.

这里的主要细节是这两行:

  1. Considering target file 'out'.
  2. Prerequisite 'out' is older than target 'out/data.txt'

out 目录在这里无关紧要。我们不关心它(并且 make 无论如何也不能很好地处理目录先决条件,因为目录上的修改时间戳与文件上的修改时间戳并不意味着相同的事情)。更重要的是,您不希望 out/data.txt 不被创建,因为构建工件目录目标已经存在(并且看起来更旧)。

您可以 "fix" 通过将 out 目标标记为 .PHONY 来做到这一点,但这只会在您每次 运行 make (你已经 运行 tar -tf 每次你 运行 make 所以如果你要这样做的话,最好把这两个步骤结合起来。

也就是说我不会那样做。我认为这个问题最简单的解决方案是来自 John Graham-Cunning 的 "atomic rules" 想法,并解释了 here

sp :=
sp +=

sentinel = .sentinel.$(subst $(sp),_,$(subst /,_,))

atomic = $(eval : $(call sentinel,) ; @:)$(call sentinel,):  ; touch $$@ $(foreach t,,$(if $(wildcard $t),,$(shell rm -f $(call sentinel,))))

.PHONY: all

all: a b

$(call atomic,a b,c d)

   touch a b

您也可以使用提取戳文件(tarball 上的先决条件)来执行此操作,将 tarball 提取到 "shadow" 目录并将 copy/link 提取到 "final" 位置(build/$file: shadow/$file target) 如果你愿意,但我认为这会有点复杂。