国庆休完了,干了许多事情,几乎没闲着。接下来写这个系列,有点提不起兴致的感觉。
如果一直讲 GNU Make 的话,就有点离题了。本来我是想讲讲,离开 IDE ,程序员该如何处理问题的。Make 只是一个起点。写着写着,就已经写的足够的多,可又似乎什么都没讲出来。有心的同学应该已经找到 GNU Make 的中文手册自己去研究了,我想大家若结合自己做过的项目,会发现其中奥妙无穷。而比较乐意享受快餐文化的另一批同学,可能还在等我的下文。该怎么说?还是先引用 VIM 主页上介绍 vim 的一句话,
Vim isn't an editor designed to hold its users' hands. It is a tool, the use of which must be learned.
是的,Make 也是,更多的编程开发工具都是。既然是工具,就必须付出学习成本。如果你觉得使用某种工具不需要支付学习成本,那么你一定失去了一些东西。只不过,你未必意识的到而已。
为了利用计算机帮助我们达到一个目的,原本有许多方法。有普通人的方式,有程序员的方式。程序员的方式是教会计算机代我们去做;普通的人方式是利用计算机协助我们去做。在 IDE 里,有时我们也想办法用独特的方式去教计算机做一些事情。IMHO, C++ 以及其千奇百怪的特性挖掘,建立出丰富的模板库很大程度就是来源于此。比如 boost 里有个叫 Boost::Spirit 的库,可以让 C++ 程序员书写近似的 BNF 的式子,生成一个解析器去分析它。但是为什么不考虑直接用 yacc 呢?它天然的就是为了处理 bnf 而存在的。因为 IDE 并不是一个很好的粘合剂,用来粘合这些工具。所以,我们把粘合层下移到了 C++ 编译器的层面。不幸的是,一旦我们给一个单一工具附加了太多使命后,无论是 IDE 还是 C++ 编译器,它们都会迅速的臃肿,直到不能承担;或者,我们干脆改变问题,让问题去适应解决手段,而不是接受手段去适应问题。
Make 是一个好的起点,它展示了一个功能单一简洁的工具,能且只能完成一个工具粘合剂的使命。并且完全以计算机的方式去工作。工具和工具之间以命令行,返回码,标准输入输出管道的方式藕荷。和人交互的界面是简单可读的文本。层次分明,顾而可堪大任。
多说无益,既然已经没有多少兴致向下写。就把剩下的时间集中在几个前面提到过的问题的解决上。剩下的一切就靠同学们自己研究摸索了。离开 IDE ,你应该得到了一片更广阔的天空,一切适合计算机协作的工具(有命令行接口)都能拿来使用。你不用再局限于别人是否给你提供了源代码,是否有良好的 DLL 插件接口,而只用关心前人做的工具是否适合你现在的需要。你用自己的头脑去发现捷径,而不是 google 一个 step by step 的教程。
问题一,如何让编译流程自动得到 .h 头文件的依赖关系?
C 语言的源文件依赖头文件是一个语言层面的东西,跟 Make 工具无关。有些构建工具,比如我曾经使用过的 boost jam 可以编写代码扫描器来完成这些,但 GNU Make 是没有的。没有并不是坏事,一个优秀的工具的原则应该是:努力做好一件事,并只做这一件。其实,分析源代码找到依赖之头文件,并不是一件容易的事情,至少你需要实现 C 语言的宏的解析工作。这应该是 C/C++ 编译器的一部分。
可惜我在 VC 中没找到编译选项可以只完成头文件依赖关系的处理流程。所以,如果你想在 Windows 下使用机器自动生成依赖关系,还得装一个 gcc 。Mingw 的 gcc 是一个不错的选择。你完全可以用 gcc 去分析头文件的依赖关系,再用 vc 来编译代码,虽然有点奇怪,但有何不可呢?一切工具皆可为我所用。
gcc 在命令行加上参数 -MM ,即可生成 GNU Make 格式的依赖关系文本(包括直接和间接的关系)。这个文本输出在标准输出上。但是,其输入格式里,目标文件的后缀是 .o 而不是 VC 默认的 .obj 。不过没有关系,VC 编译 .c 生成 .o 而不是 .obj 就好了。
下面来看我们前面几篇提到的那个小项目,foo.c bar.c foo.h 最后编译生成 foobar.exe 。我写一个完整的改进版在下面:
SRCS=foo.c bar.c
OBJS=$(SRCS:.c=.o)
MAKEDEPEND=gcc -MM
CFLAGS=/Zi
all : foobar.exe
.depend:
$(MAKEDEPEND) $(SRCS) > $@
depend:
-del .depend
$(MAKE) .depend
clean :
-del foobar.exe $(OBJS)
foobar.exe : $(OBJS)
%.exe :
link /out:$@ $^
$(OBJS) : %.o : %.c
cl /c /Fo$@ $(CFLAGS) $<
include .depend
这里出现了许多前面没有提到过的陌生用法。下面一一讲解:
SRCS=foo.c bar.c
OBJS=$(SRCS:.c=.o)
仅凭猜测,我们就能知道,OBJS 会被定义成 foo.o bar.o 。GNU Make 有着强大的字符串处理功能,非常适合做这类事情。像这种后缀匹配替换,用这个简单的语法就可以表达了。如果在 SRCS 里定义了多种不同后缀的字符串,比如有 .c .cpp .asm 等等,我们可以先筛选出 .c 的部分,即 CSRCS = $(filter %.c,$(SRCS)) 。这里不再详述。
需要稍加说明的是这里的一个赋值:OBJS=$(SRCS:.c=.o) 。OBJS 的值依赖了另一个变量 SRCS 。那么,OBJS 的求值过程是什么是否发生的呢?这个对于敏感的程序员应该是头脑中立刻显现的问题。
答案是,直到 OBJS 被真正用到,取值的时候,$(SRCS:.c=.o) 的求值过程才被触发。也就是说,你可以在下面修改 SRCS 的值(通过 = 或 += 等)而 OBJS 的值总是正确的。
btw, 如果你希望求值过程立刻进行,可以用 := 而不是 = 。具体请翻阅 GNU Make 的文档。
在 Makefile 的最后,include .depend 包含了一个暂时还不存在的 .depend 文件。当 Make 工作的时候,发现一个文件不存在,都会试图构建它,include 指令中出现的文件也不例外。所以,第一次运行,会触发 .depend 文件的构建。
.depend:
$(MAKEDEPEND) $(SRCS) > $@
通过调用 gcc -MM ,gcc 会输出类似 foo.o : foo.c foo.h 这样的文本行,借助命令行管道操作,我们把输出定向到了 $@ ,即文件 .depend 中。
而第 2 次运行 Make ,由于 .depend 文件已经存在,依赖关系则不再构建。
如果我们修改了 .c 文件中引用的 .h 文件怎么办?简单的方法就是删除 .depend ,重新 Make 。当然,这里我们提供了一个叫做 depend 的伪目标来帮我们做这件事情。
depend:
-del .depend
$(MAKE) .depend
这里出现了一个 $(MAKE) 没有定义的变量。这个变量是由 Make 自己定义的,它的值即为自己的位置,方便 Make 递归调用自己。在以后的篇章里,我们会继续发现 Make 自身递归调用的威力。
还有问题吗?
自然是有的。
.depend 文件并不能自动随着项目源码的修改而变更。这显然不够自动化。只不过我们在改进这一点时,需要先问一下自己,你的项目真的需要这个吗?很多项目,并不常变更依赖关系。而作为开发人员自己,在不段的修改代码时,心里非常清楚什么时候应该重新 gmake depend 。而对于第一编译项目,且很有可能不再修改源代码重新编译的人来说,生成一次 .depend 文件足够了。
我们可以简单的让 .depend 文件每次重新生成,这或许比仔细检查每个文件的变更情况然后重新生成更为廉价。(为什么?怎么做?这两个问题同学们自己思考:)
简单的在 .depend : 后加一个依赖关系,比如 .depend : $(SRCS) 行不行?在这个例子里没有问题,但是如果更复杂的情况下,比如 .h 文件中又包含了新的 .h ,可能就不能正确的重构 .depend 文件了。
另一方面,.depend 包含了所有 .c 文件和 .h 文件的依赖关系,因为一个文件的变更而重新扫描所有源文件值得吗?
这个问题不好回答。如果项目太大太大,或许有性能问题。不过,为什么你要把这么大的项目所有源文件放在一个子项目中?要知道,使用 Make 构建子项目,比如说一个静态库,是非常廉价的。(这个问题以后再来探讨)
无论如何,没有不能解决的问题。我们自然可以去生成独立的依赖关系描述文件,然后把依赖关系的描述文件和构建过程本身建立依赖关系。这个做起来比较复杂,但是也很有趣。(提示:gcc 的新版本命令行支持 -MMD ,可以把 .o 换为 .d ,这个功能目的何在?)不过,云风提醒大家,切勿钻牛角尖。一切工具只是帮助我们更便捷的达到目标。我们要认清目标,再选择好工具和恰当的用法。
今天就到这里,下一次我们继续讨论另外几个常见问题:如何管理和组织源文件树木更多的项目,怎么组织负责的源代码目录树。以及怎样像 VS 做的那样,为 Release 版和 Debug 版的目标文件分目录存放,并保存源代码目录的清洁。