GNU Make:用 order-only prerequisite 避免目录时间戳触发无效重建

51 次浏览3 条回复

构建产物放在单独目录时,目录只负责保证写入位置存在,不应参与产物的新旧比较。若把目录写成普通前置条件,目录内新增或删除文件会更新目录时间戳,可能让目标发生无效重建。GNU Make 可以用 | 把这类依赖声明为 order-only prerequisite:先确保它存在,但不因它变新而重建目标。

环境:GNU Make 4.3+;Linux 或 macOS;Bash;需要 mkdircptouch

创建 Makefile,注意命令行开头是 Tab:

build/app.txt: src/app.txt | build
	cp $< $@

build:
	mkdir -p $@

.PHONY: clean
clean:
	rm -rf build

准备输入并运行:

mkdir -p src
printf 'version=1\n' >src/app.txt

make
sleep 1
touch build
make
sleep 1
touch src/app.txt
make

第一次 make 会依次创建 build 并复制文件。更新 build 目录自身的时间戳后,第二次 make 不会执行 cp;更新真正的输入 src/app.txt 后,第三次才会重新复制。sleep 1 用于兼容时间戳精度较低的文件系统。

关键是规则中的竖线:

build/app.txt: src/app.txt | build

竖线左侧是普通前置条件,任何一项比目标新都会触发重建;右侧只保证在目标配方前完成。若改成 build/app.txt: src/app.txt build,执行 touch build 后就会再次运行 cp。目录目标也不应声明为 .PHONY,否则它的创建配方会在每次构建时执行。

补充一个并行构建场景:order-only prerequisite 不只是忽略目录时间戳,也会让 make -j 先完成目录创建,再启动依赖它的多个配方。可以把规则扩成:

build/%.txt: src/%.txt | build
	cp $< $@

build:
	mkdir -p $@

准备 src/a.txtsrc/b.txt 后执行:

make -j2 build/a.txt build/b.txt

build 的配方只执行一次,两个 cp 都会在其完成后运行。若产物分布在多级目录,建议为每个实际输出目录分别建立目录目标并作为 order-only prerequisite;不要只依赖某个配方恰好先执行,否则并行度提高后容易暴露竞态。

还有一个容易混淆的边界:| 右侧只适合不影响产物内容的结构性依赖。order-only prerequisite 即使刚被重建,也不会仅凭这一点让目标过期;生成配置、生成代码等语义输入仍应放在竖线左侧。下面的最小示例同时展示两类依赖:

build/app.txt: src/app.txt generated/version.txt | build
	cat $^ >$@

generated/version.txt: VERSION | generated
	sed 's/^/version=/' $< >$@

build generated:
	mkdir -p $@

环境:GNU Make 4.3+;Linux 或 macOS;需要 catsedmkdir。验证命令:

mkdir -p src
printf 'payload\n' >src/app.txt
printf '1\n' >VERSION
make build/app.txt

sleep 1
printf '2\n' >VERSION
make build/app.txt
cat build/app.txt

第二次构建会先更新 generated/version.txt,再更新 build/app.txt。若把 generated/version.txt 移到 | 右侧,版本文件虽然会更新,已有的 build/app.txt 却不会因此重建。另一个细节是 $^ 只展开普通前置条件,不包含 order-only prerequisite,因此目录名也不会意外传给 cat

如果输出分散在多级目录,可以用 .SECONDEXPANSION 从每个目标自动推导其目录,避免手工维护目录列表:

.SECONDEXPANSION:
.PRECIOUS: %/

build/%.txt: src/%.txt | $$(@D)/
	cp $< $@

%/:
	mkdir -p $@

环境:GNU Make 4.3+;Linux 或 macOS;Bash;需要 mkdircp。准备两个不同层级的输入后可并行验证:

mkdir -p src/a src/b
printf 'alpha\n' >src/a/result.txt
printf 'beta\n' >src/b/result.txt
make -j2 build/a/result.txt build/b/result.txt

第一次展开时,$$(@D) 保留为待展开表达式;进入具体目标的二次展开后,它分别得到 build/abuild/b。竖线仍确保目录先创建、目录时间戳又不触发产物重建。.PRECIOUS: %/ 则避免这些由模式规则生成的目录被当作中间目标清理。这个写法适合目录层级由目标名决定的构建;如果目录集合固定且很少,显式目录规则通常更直观。