分类: C/C++
2010-02-27 22:46:09
云风在blog上写了一组《IDE 不是程序员的唯一选择》的文章。题目很吸引人,以至于我一直以为他要写什么鸿篇巨制,可惜到最后只出现了一组《GNU make入门指南》。(笑)
被IDE绑定的确是一件很悲哀的事情。作为一个程序员,当然应该搞清楚程序编译链接的整个流程,makefile给出了很好的一个路径,让我们能够了解这一点。
但是,如果觉得程序员就应该比拼“手写汇编代码”、“用记事本写程序”,那就大大的错了。程序员需要了解细节,但不意味着程序员都是傻瓜。为什么要放着好好的提高生产力的工具不用,一切从零做起呢?(当然了,上面这句话并不能成为使用盗版的理由。)所以寻找并了解一些优秀的工具,也是我们这些程序员所需要做的事情之一。
GNU make 当然是一个很好的工具,云风已经讲了很多,我就不啰嗦了。我今天想介绍另一个优秀的自动构建工具。
scons是一个Python写的自动化构建工具,从构建这个角度说,它跟GNU make是同一类的工具。它有什么好处呢?在它自己的网站上,当然写了一大堆了,快速、稳定、强大、跨平台、可扩展……。不过我们还是从自己的角度来看看它到底好在哪里。
刚刚提到scons从目的而言跟GNU make是同一类的工具。但是实际上,它的思想是跟GNU make完全不同的。GNU make的核心是“依赖关系”,我要做的事情,就是告诉系统,一个目标依赖什么东西,并且,当被依赖的东西发生变化时,我要做什么。这样做可以解决相当多的问题,但是也带来了一个最大的问题:我如何判别这个目标依赖什么?
对于一个两个,甚至十几个文件,我当然还比较容易搞清楚,谁依赖谁。但是当文件有成百上千个时,要分清楚谁依赖谁可就没这么容易了。尤其是C/C++头文件的依赖,如果手工分析的话,工程量可是不小。为了解决这个问题,GNU又提供了另外一套工具:Automake,使用程序来分析依赖性,然后辅助你产生makefile。
于是乎,就有人想了,既然如此,我干吗费那劲,用程序分析依赖性,然后生成一个文件,再交给另外一个程序去处理呢?既然依赖性需要用程序来分析,那么就直接交给构建工具本身去做不就好了吗?对的,这是一个非常自然的思路,于是,Java世界有了Ant,而Python世界有了scons。
scons就是这样一个构建工具:你告诉它要做的任务,以及完成这个任务需要的输入,以及这个任务产生的输出,怎么做这个任务(当然其中就包括依赖性分析),就交给工具本身完成。
说了这么多,我们来看看一个现实世界的scons是什么样子的。
我们假设有一个C程序,由三个文件组成:
//-----func.cpp int add(int x, int y) { return x+y; } //----func.h #ifndef __FUNC_H__ #define __FUNC_H__ extern int add(int x, int y); #endif //-----main.cpp #include#include "func.h" int main() { printf("2+3=%d\n", add(2, 3)); }
然后我们写一个SConstruct文件(类似于GNU make的Makefile文件,是scons的默认文件名):
Program('add_main', ['main.cpp', 'func.cpp'])
然后执行scons,将会输出以下信息:
scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... g++ -o main.o -c main.cpp g++ -o func.o -c func.cpp g++ -o add_main main.o func.o scons: done building targets.
这时,我们就会得到一个add_main的可执行程序。
如果执行scons -c,我们会看到:
scons: Reading SConscript files ... scons: done reading SConscript files. scons: Cleaning targets ... Removed main.o Removed func.o Removed add_main scons: done cleaning targets.
生成的可执行程序连带中间结果都被清除了。
这里我们可以看到,scons只需要描述任务,并不需要指定依赖关系,甚至我们都没有指出头文件,但是你修改func.h的时候仍然会触发构建。这是因为scons内部有个scanner,可以帮助扫描包含文件的关系。(我们可以编写自己的构建任务,当然也可以编写自己的scanner,有兴趣的可以看帮助,这里就多说了)
我们还发现,这里我们根本没有指定编译器,也没有指定编译选项,但scons仍然很聪明的选择了g++(这是Linux上的结果,如果是Windows,默认会选择cl也就是Visual C++),并且给出了正确的编译选项。事实上,这是因为scons内置提供了很多编译器及其对应选项的选择,然后对于不同的平台,会有一个默认项。我们当然也可以自己选择编译环境,比如在Windows下,我同时安装了VC和mingW,但是我想用mingW来编译而不是VC,就可以这样指定:
import os env = Environment(ENV=os.environ, tools=['mingw']) env.Program('add_main', ['main.cpp', 'func.cpp'])
这里出现了一个Environment的概念,Environment可以设置编译的环境。这是一个简介,所以对于它我们就不多说了,感兴趣的可以自行查阅资料。嘿嘿。
在这里我们看到了一句熟悉的语句:import os。是的,SConstruct文件就是一个非常标准的Python程序,所以,Python能做什么,scons就能做什么。很好很强大阿,哈哈。(这里顺便说一句,我们也可以认为Makefile是shell程序,但是因为shell有平台相关性问题,我们很难写出一个通用平台的Makefile,但是我们还是写的出一个通用平台的SConstruct的。)
Program只是scons支持的构建任务其中的一种,用于根据后缀名自动构建C、C++、D和Fortran的可执行程序。scons还支持另外几十种构建目标,可以查看支持的列表。如果这里找不到的,还可以自己编写Builder和Scanner。
接下来我想给出一个一直提但是一直没有给出结果的东西,就是对C++程序的单元测试。
还是以刚刚那个小例子为例。我们对add函数作一个单元测试。我们知道,单元测试不是程序的一部分,所以需要一个独立的main函数。而被测试的单元是一样的。
现在假设我们有一个测试函数:(用的是boost的test库,这个库的使用方法不再赘述)
//--------test_main.cpp #include "func.h" #define BOOST_TEST_DYN_LINK #define BOOST_TEST_MAIN #includeBOOST_AUTO_TEST_CASE( add_test ) { BOOST_CHECK( add(2, 2) == 4 ); }
然后我们写一个SConstruct:
#所有的需要测试的单元文件(去除两个主文件) import glob obj_files = glob.glob('*.cpp') obj_files.remove('main.cpp') obj_files.remove('test_main.cpp') common = Object(obj_files) Program('add_main', ['main.cpp'] + common) Program('unittest', ['test_main.cpp'] + common, LIBPATH='/usr/lib', LIBS=['libboost_unit_test_framework'] ) Alias('test', 'unittest') Default('add_main')
这里出现了几个新玩意儿,一个是Object,其实也很好理解,Object就是将指定的文件编译成目标文件(.o或者.obj),然后我们用了2个Program,表示要生成两个可执行文件。在生成的时候,我们将通用的common附加到构建输入中。另一个是Default,这是表示默认的构建。当我们输入scons时,将构建add_main,而我们输入scons unittest时,则构建unittest。但是输入unittest感觉不太方便,我们想输入scons test来编译,但希望输出的文件名仍然是unittest,于是我们增加了Alias,将unittest取了一个别名叫test,这时,我们输入scons test,仍然会构建unittest。
我们注意到构建unittest时,使用了附加的信息,比如额外的库、额外的路径等等。还有为了方便起见,我们使用了Python标准库的glob函数展开文件通配符。
从这个例子我们大约可以感受到scons的强大威力了。至于进一步的深入,就看各位自己的了。