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

6 次浏览1 条回复

构建产物放在单独目录时,目录只负责保证写入位置存在,不应参与产物的新旧比较。若把目录写成普通前置条件,目录内新增或删除文件会更新目录时间戳,可能让目标发生无效重建。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;不要只依赖某个配方恰好先执行,否则并行度提高后容易暴露竞态。