博主参与了一个使用qmake
构建的项目,包含几百个源文件,最近遇到一个恼人的问题:有时仅仅修改了一个.cpp
文件,构建项目时就有可能触发全编译。但是编译时又会命中ccache
的缓存,这说明源代码实际上内容并没有发生变化。即使命中了ccache
缓存,几百个源文件编译下来还是要耗一小会儿时间的,博主对此不能熟视无睹。
本文中使用了一个demo项目stupid_qmake
来复现和分析该问题,其结构非常简单:
stupid_qmake/
├── main.cpp
├── stupid_qmake.pro
└── utility
├── foo.cpp
└── foo.h
main.cpp
文件内容如下,调用了std::swap
以及标准输出流:
#include <iostream>
#include <utility>
int main() {
int a = 1;
int b = 2;
std::swap(a, b);
std::cout << a << " " << b << std::endl;
return 0;
}
正常来说,如果我们修改了一个.h
文件,那么所有依赖这个.h
文件的.cpp
文件都需要重新编译,无论是直接include还是间接include;而修改一个.cpp
文件,则重新编译这个.cpp
就足够了。
这些重新编译的触发依赖于构建系统,以下面的Makefile
为例:
CXX = g++
CXXFLAGS = -Wall -g
TARGET = my_program
SRCS = main.cpp func.cpp
OBJS = main.o func.o
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
main.o: main.cpp func.h
$(CXX) $(CXXFLAGS) -c main.cpp -o main.o
func.o: func.cpp func.h
$(CXX) $(CXXFLAGS) -c func.cpp -o func.o
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean
不算伪目标,共有3个target:my_program
、main.o
和func.o
。target和target之间的依赖,以及target对源文件的依赖,如下图所示:
在执行make
时,make
不会也不可能真的去检查源文件内容是否发生了变化,而是会根据源文件和target的最后修改时间(mtime
)以及target之间的依赖关系,来决定哪些target需要重新生成:如果源文件的mtime
比target的mtime
大,说明源文件有更新,这个target需要重新生成,同时所有依赖这个target的其他target也需要重新生成。
在我们的demo项目中,从main.cpp
的内容来看,修改foo.h
/foo.cpp
不应当导致main.cpp
重新编译,但实际情况并非如此:每当使用qtcreator
编辑foo.cpp
后,总会触发main.cpp
的重新编译。
这个问题分析起来的入手点就是看看qmake
到底为我们生成了一个怎样的Makefile
—— 如果你在命令行中编译过qmake
项目,你应该知道,运行qmake
命令时会在构建目录中生成一个Makefile
文件,然后再运行make
命令才会正式开始项目的编译。
stupid_qmake.pro
文件生成的Makefile
中,main.o
和foo.o
两个target的生成规则如下:
main.o: ../main.cpp ../utility
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o ../main.cpp
foo.o: ../utility/foo.cpp ../utility/foo.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o foo.o ../utility/foo.cpp
foo.o
的生成规则没什么问题,main.o
的生成规则看起来有点奇怪:在依赖项中竟然有一个utility
目录。它为什么会在依赖项中呢?联想到main.cpp
源文件中依赖了utility
头文件,我们可以猜测,qmake
在生成依赖规则时,utility
目录被错误地视为了utility
头文件被添加到了main.cpp
的依赖项中。为了验证这个猜测,我们把main.cpp
中对utility
头文件的依赖去掉,然后重新运行qmake
,main.cpp
的生成规则就变成了:
main.o: ../main.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o ../main.cpp
没有那条对utility
目录的依赖了,所以我们的推测是正确的,是qmake
混淆了utility
目录和utility
头文件。
依赖项列表中有一项是目录会产生什么后果呢?make
似乎并不介意这件事,仍然会机械地扫描依赖项的mtime
,以此决定哪些target需要重新生成。所以现在需要探讨的问题是:目录的mtime
在什么情况下会更新?关于这个问题,我在另外一篇博客Linux:使用vim编辑文件为什么会影响目录的mtime中分析过,在那篇博客中我是以vim
为例来分析的,实际上qtcreator
也有相同的效果:当你使用qtcreator
编辑了某个目录下的文件后,这个目录的mtime
就会更新。所以说,在我们的demo项目中,如果你编辑了utility
目录下的文件,utility
目录的mtime
就会更新,进而引起main.cpp
的重新编译。
回到博文开头提到的那个项目,里面恰好有一个utility
目录,而且这个目录下的源文件的修改也比较频繁。那么现在我们可以还原出整个问题的全貌:C++
标准库中的utility
头文件是一个被广泛包含的头文件,整个项目中的大部分源文件都对它有直接或者间接的依赖。而qmake
错误地将项目中一个名为utility
的目录当成了utility
头文件这件事,就会导致utility
目录被添加到了大部分.o
文件的依赖项中。一旦我们编辑了utility
目录下的文件,utility
目录的mtime
就会被更新,这将会导致大部分源文件重新编译。
问题修改起来也简单:在将utility
目录重命名为util后,恼人的问题就消失了。
(按理说,遇到这种情况时,qmake
应当优先匹配系统目录下的头文件,有时间了研究一下qmake
的源码看看它为什么没有这么做)