PAYATHON 2026

如何让 Makefile 在 .tpp 模板实现文件修改后触发重新编译?

数据小许

需要让 TestMRE.o 依赖 MRE.tpp。这样修改 MRE.tpp 后,make 才会重新编译 TestMRE.cpp,然后重新链接程序。.tpp 文件不需要单独生成 .o 文件。

这个示例可以直接补齐依赖。如果项目中的头文件较多,用 GCC 或 Clang 自动生成依赖文件会更方便。

为什么修改 .tpp 没有触发编译?

编译器实际处理的包含关系是:

TestMRE.cpp → MRE.hpp → MRE.tpp

MRE.hpp 和 MRE.tpp 都会影响 TestMRE.o 的内容,但 make 不会自行解析 C++ 的 #include。它需要通过 Makefile 或编译器生成的依赖文件了解这些关系。

原来的规则还有一个文件名匹配问题:

%.o: %.cpp %.hpp

对于 TestMRE.o,这里的 % 是 TestMRE,所以这条规则要求存在 TestMRE.cpp 和 TestMRE.hpp。它不会自动找到 MRE.hpp,也没有声明 MRE.tpp。

示例没有提供 TestMRE.hpp,因此这条自定义模式规则可能无法应用。如果 GNU make 启用了内置规则,目标文件仍可能通过内置的 .cpp 编译规则生成,但不会因此补上所需的头文件依赖。

方法一:显式列出依赖

对于这个最小示例,可以直接使用:

TestMRE: TestMRE.o
	$(CXX) $(CXXFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@

TestMRE.o: TestMRE.cpp MRE.hpp MRE.tpp
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

这里,TestMRE.o 依赖三个文件。只要其中任意一个文件的修改时间比 TestMRE.o 新,make 就会重新执行编译命令。生成新的 TestMRE.o 后,make 会继续重新链接 TestMRE。

$< 表示第一个依赖文件,这里是 TestMRE.cpp;$^ 表示所有依赖文件,在链接规则中是 TestMRE.o;$@ 表示当前目标。

Makefile 中的命令行必须以真正的 Tab 开头。

方法二:让编译器自动生成依赖

使用 GCC 或 Clang 时,可以在编译时生成 .d 文件,再让 GNU make 读取:

TestMRE: TestMRE.o
	$(CXX) $(CXXFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@

%.o: %.cpp
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -MMD -MP -MF $(@:.o=.d) -c $< -o $@

-include TestMRE.d

各选项的作用如下:

  • -MMD:在编译时生成依赖信息,包含通过 #include 引入的非系统头文件,也包括间接包含的文件。
  • -MP:为依赖项生成空目标规则,减少删除或重命名头文件后,旧依赖记录导致的 make 报错。它不会让编译器忽略缺失的 #include 文件。
  • -MF $(@:.o=.d):指定依赖文件路径。在这个例子中,TestMRE.o 对应 TestMRE.d。
  • -include TestMRE.d:读取依赖文件;首次构建时即使文件尚不存在,也不会因此报错。

编译器生成的依赖文件会表达类似下面的关系,具体路径写法和排版可能不同:

TestMRE.o: TestMRE.cpp MRE.hpp MRE.tpp

.tpp 扩展名不会妨碍依赖生成。只要文件通过 #include 被实际读取,就能记录为依赖。

如何让已有构建开始使用自动依赖?

如果 TestMRE.o 已存在,而 TestMRE.d 尚未生成,仅修改 Makefile 中的编译命令通常不会让 make 自动重新编译已有目标。

采用上面的自动依赖配置后,可以先在 GNU make 中执行一次:

make -B

这次强制构建会生成 TestMRE.o 和 TestMRE.d。之后修改 MRE.tpp,再执行:

make
./TestMRE

在文件时间戳正常、修改已保存且构建成功的前提下,make 应重新编译并链接,程序输出应变为:

final message

后续需要保留并读取 .d 文件。如果删除了依赖文件,却保留已有的 .o 文件,就需要重新生成依赖。其他编译器的依赖生成选项可能不同,需要使用相应工具链支持的方式。

备注:内容仅供参考。