本文发自 http://www.binss.me/blog/solving-make-doesn't-rebuild-when-headers-changed-problem/,转载请注明出处。

近日将HTTPServer项目的代码归档到src目录下,而makefile仍留在根目录下。归档后代码结构清晰了,用起来爽爽的。然而刚过不久就发现了一个问题,在修改头文件后,make不会重新编译,而是显示:

[email protected]:/home/binss/HTTPServer# make
make: Nothing to be done for `all'.

这种提示一般出现于make检测到要编译的文件相较于上次编译没有发生改变,也就是说先前生成的HTTPServer二进制文件是最新的。

奇怪,明明改了头文件,为何make会检测不到。反复检查makefile却无法发现问题。偶然之下把这两段注释了:

$(DEPENDS): %.d:%.cpp
  @set -e; rm -f [email protected]; \
  $(CC) -MM $(CFLAGS) $< > [email protected]$$$$; \
  sed 's,\($*\)\.o[ :]*,\1.o [email protected] : ,g' < [email protected]$$$$ > [email protected]; \
  rm -f [email protected]$$$$;

-include $(DEPENDS)

发现依然能够make成功,并且症状和之前一样——修改头文件后,make不进行重新编译。

直到这个时候我才发现了问题所在:makefile的自动推导只会推导出目标文件对源文件的依赖关系,而不会增加头文件的依赖关系。这导致的直接问题就是修改项目的头文件,不会导致make的自动更新。先前为了这个问题,我参照官方文档中的做法,可以见:

https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html

先通过g++ -MM生成依赖的文件列表,然后通过sed修改成我们需要的格式。然后通过include导入依赖文件,从而使头文件的修改能够被发现。如今加和没加一个样,估计就是这段代码出了问题。

检查生成的.d和.d.$$$$文件,发现G++ -MM 生成的HttpServer.d.7066和HttpServer.d一模一样,都是(为了易于表述,精简了依赖):

HttpServer.o: src/HttpServer.cpp src/HttpServer.h

而根据官方文档,.d文件应该为:

HttpServer.o HttpServer.d: src/HttpServer.cpp src/HttpServer.h

也就是说这段代码中的sed语句没有发挥作用。由于没有接触过sed,因此一时也看不出这个语句的意思。把语句粘到bash中,发现可以正常执行,更加令我摸不着头脑。

[email protected]:/home/binss/HTTPServer/src# sed 's,\($*\)\.o[ :]*,\1.o [email protected] : ,g' < HttpServer.d.7066
HttpServer.o [email protected] : src/HttpServer.cpp src/HttpServer.h

无奈之下,花了一个小时把sed大概了解了一下,终于知道这一句是在干嘛......

sed 's,\($*\)\.o[ :]*,\1.o [email protected] : ,g' < [email protected]$$$$ > [email protected];

语句属于sed 's,pattern,new,g'的模式,意为将全局中匹配pattern的内容替换为new。在这里是将匹配$*.o[ :]*的内容替换为\1.o [email protected] :$*为makefile中自动化变量,表示目标中%所代表的部分,在这里为src/HttpServer[ :]*匹配0-n个空格或冒号,而\1代表第一个括号括住的内容。[email protected]也是自动化变量,代表目标名,在这里为HttpServer.d。

因此这个语句的目的是在HttpServer.o后面插入HttpServer.d。这个语句原来工作的很好,但是当我们把要编译的文件放到src目录下时,$*不再是我们期待的HttpServer,而是包含了路径的src/HttpServer,因此pattern匹配失败,原样输出。

但是为啥在bash能够正常工作呀?等等,尽管$*在makefile有定义,但是它在bash环境下的sed中却并非如此,尽管一番实验,发现它直接匹配了.o之前的内容,反而歪打正着了......

知道问题在哪就好办了,直接改为:

sed 's,.*\.o[ :]*,$*.o [email protected] : ,g' < [email protected]$$$$ > [email protected]; \

直接匹配.o前的任意字符,然后替换为$*即可。

最终HttpServer.d文件内容为:

src/HttpServer.o src/HttpServer.d: src/HttpServer.cpp src/HttpServer.h

修改后,问题解决。