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
- #include
- BOOST_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的强大威力了。至于进一步的深入,就看各位自己的了。 |