对于这个系列,我已经意兴阑珊了。时间拖的太长也不好。从一开始我就没打算写一个某某工具(GNU Make)的入门教程。本来是想给那些微软 IDE 深度中毒者展现一些不同的东西,顺便打破 Make 等 CUI 工具的神秘感。工具是为人服务的,不应该是用来给人增添麻烦的。IDE 是这样,RAD 工具是这样,那些 CUI 工具也是如此。如果你能熟悉工具背后的使用哲学,工具就能给你便捷。不同的工作需要不同的工具去做,不要拿着锤子,就想把一切都变成钉子。
既然每一类工具都拥有特别多的用户,而且这个用户群还不都是脑残,去看看不同领域总是好的。对于开发环境来说是这样,选择编程语言来说也是如此,又或者说到开发方式等等。
今天不打算写长篇,简单点把这个系列完结。只谈一下前面欠下的一些问题。
对于大工程,在 VS 里,我通常是以虚拟文件夹和子工程的方式来管理的。不知道其他同学跟我是不是一个习惯。从 VC 6.0 以后,我几乎就没碰过 VS 了。不太清楚现在微软的 IDE 目前的发展趋势。我想可能有更好的组织方式吧。
但如果离开 VS ,我们用 GNU Make ,或是别的类似的工具(例如我用过的 Boost Jam ,或是前面有朋友推荐的 CMake 等)按惯例,通常按 OS 的文件目录结构来管理大的项目。即,一个子项目放在一个子目录中。对于一个大模块,即使它可能是一个子项目中不可分割的一部分,通常也以静态库的形式被分离出来。哪怕这个静态库只被一个地方引用。
把源代码拆分成适当的规模,并分类组织在不同的文件目录下,是一个好的习惯。
那么你在编写 Makefile 的时候,就可以为每个源代码的子目录编写一个 Makefile 。那么怎么让若干处于不同子目录下,甚至多层深度下的 Makefile 协同工作?Make 的惯例是利用 Shell 递归调用自己。
比如你的 src 目录下有两个子项目,foo1 和 foo2 。你在 src 根下的 Makefile 一般会这样写:
all :
cd foo1 && $(MAKE) all
cd foo2 && $(MAKE) all
clean:
cd foo1 && $(MAKE) clean
cd foo2 && $(MAKE) clean
$(MAKE) 是一个预定义的变量,里面保存的就是调用 Make 自己的 Shell 指令。
看起来比较繁琐,所以我的习惯是,把 foo1 foo2 这些子目录提取出来。
DIRS= foo1 foo2
all : $(DIRS:=.all)
clean : $(DIRS:=.clean)
%.all :
cd $* && $(MAKE) all
%.clean:
cd $* && $(MAKE) clean
这个版本依然有许多重新的东西,也是可以去掉的,但那势必引出更多的“高级”用法,暂时就不展开了。其中用到的知识前面我们都介绍过了。除了 $* ,这个是表示目标中除掉 .ext 后缀的部分字符串。
按前几篇的流程走下来,你会发现,在 build 工程的时候,往往在源代码目录留下许多中间文件。我们在前面的例子中都写上了 clean 这个目标,用来清除中间文件。但事实上,在 GNU Make 的手册里,并不建议我们如此的污染源代码目录。一般来说,我们会定义一个中间文件的输出目录。这需要少许的技巧,但是不难办到,这里就不举例了。
因为早年使用 VS 的缘故,我喜欢同时维护至少两个版本的中间文件。一个 Debug 版,一个 Release 版,分放在不同的中间文件目录中,重新 build debug 版,不会影响到 Release 版的重构建。对于这个需求 Boost Jam 做的相当不错。甚至弄的更华丽,你可以轻易的拥有 "关闭 RTTI 设置的 Release 版“ 、”打开 C++ 异常的 Debug 版“ 等等。不过 boost jam 也为这华丽的功能付出了一点点小小的代价……
我们说回 Make ,最简单的方法是,再编译不同的版本的时候,选用不同的变量取值,例如:不同的优化开关、不同的输出目录。GNU Make 支持类似 C 语言中 #if 的条件语句,但是不能完全解决这个问题。还需要动用的一个特性是,GNU Make 支持定义目标相关的变量:
release : CFLAGS=/O2
debug : CFLAGS=/Zi
即这样的写法,它使得一个变量的取值在编译某个特定目标的时候才有效。具体怎么做达到你心目中的需求,就留给同学们自己思考了。
记住,是工具就需要学习。多用才可以提高对工具的掌握程度。对于编写 Makefile 文件,不要抱着修改配置文件那样的心态,而要把它当成一门语言,一门可以提高你的工作效率的语言。通过计算机语言教会计算机做本该你亲手来做的操作,这是程序员之道。