发掘scons [开源世界] .

发掘Scons - [开源世界]

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://bigwhite.blogbus.com/logs/32477272.html

发现或者说知道SCons是缘于Google的comp.lang.c group上的一则名为"Best Build Tool for large C projects "的帖子,帖子的作者列出了11条他认为"Best Build Tool"应该具备的特点,并欲找到这样的Build Tool。在该帖子的回复中,有人提到了Scons,说来惭愧,这是我第一次听说到有这样一个工具。一直在Unix下编写C程序,习惯了Make,也对Make的复杂度和较为陡峭的学习曲线有所了解,曾经尝试使用AutoconfAutomake,但是都因上手困难而放弃。自己心底也一直想找到一个更简单一些的但又不失功能的适合C的Build Tool,Scons是否能满足的需要的呢?好奇心驱使着我去发掘一下Scons。

工具的进化一直在持续着。高手能把Make玩 弄于股掌之中,但是大多数人水平还是一般的,在经历了"Make hell"后他们要寻求更简单、更人性的工具,这也是工具进化的动力之一。Scons是用Python实现的一款跨平台的开源Build Tool,用Python实现意味着Scons比Make所使用的类Shell语言更贴近于自然语言,更易于理解和控制;用Python实现的另一个好处 也是Make所不具备的就是很好的跨平台能力,一次编写Build脚本,在多种平台上无需修改即可运行无误,特别是从Unix->Windows这 样的移植,如果使用Make则势必要修改。

先简单说说Scons的安装,要运行Scons势必你的机器上要有Python,虽然Python 3.0已经Release,但目前主流Python开源项目仍然在用2.x版本。我的机器上安装的就是Python 2.5。下载Scon-1.10稳定版,unzip,进入unzip后的目录,执行安装命令:python setup.py install即可。Scons会被安装到默认目录下,如果你想指定安装目标目录的话,可以使用--prefix=YOUR_INSTALL_DIR参数。

按照惯例,我们先来一个"Hello, World!"的例子,在你的测试目录下,编写一个HelloWorld.c
/* HelloWorld.c */
#include <stdio.h>
 
int main(int argc, char* argv[])
{
   printf("Hello, world!\n");
   return 0;
}
在同一级目录下,建立一个新文件SConstruct,编辑该文件,输入内容:
Program('HelloWorld.c')
在命令行下执行scons,一个名为HelloWorld.exe的可执行文件(在Unix下可执行文件为HelloWorld)被编译链接成功。第一次上手成功会给使用者带来莫大的成就感,提高该使用者继续发掘该工具的可能性。

SConstruct 是个什么文件?SConstruct之于Scons就好比Makefile之于Make;它是Scons的输入,SConstruct中的内容采用的是 Python的语法,而Python的语法比较简单,这样很容易被接受,而Program则只是一个方法调用。 Program('HelloWorld.c')意味着告诉Scons我要将HelloWorld.c编译成一个名为‘HelloWorld.exe'的 可执行文件,当然了Scons会自动分析HelloWorld.c,自动得出目标程序名字。

我们日常工作构建代码的类型不外乎如下几种: 简单一点的包括编译object文件、构建静态库、构建动态链接库和构建可执行程序;复杂的则是要对一个拥有众多目录和几十万、上百万行代码的项目进行整 体体系构建,而复杂的构建也是由一系列的简单构建组合而成的,我们先说说简单类构建。

HelloWorld例子只是一个最简单的由单个源文件构建程序的例子,现实中我们构建可执行程序可能依赖的不止是一个文件,可能还有头文件或链接其他第三方库;下面这个SConstruct文件中的语句就是一个稍微复杂些的例子:
Program(target = 'test', source = ['main.c', 'file1.c', 'file2.c'], LIBS = ['lib1', 'lib2'], LIBPATH = ['lib1/lib', 'lib2/lib'], CPPPATH = ['include', '/lib1/include', 'lib2/include'], CCFLAGS='-D_DEBUG')

这个例子中具备我们常用 的诸多元素,这些参数中:'test'是构建后的程序名,source是一个源文件数组,LIBPATH则是要链接库的目录数组,LIBS是要链接的具体 的库文件的名字。CPPPATH则是-I的替代品,是头文件所在目录的数组,CCFLAGS则是负责传递编译器的编译选项参数。

通过这些Keyword Arguments,Scons可以在用户和编译器之间传递信息,并控制编译器完成构建。同样的,编译目标文件,构建静态库、动态库可以由下面的一些builder来完成。

Library('foo', ['f1.c', 'f2.c', 'f3.c'])   #生成名为foo的静态库,在Windows上是foo.lib,在unix上为libfoo.a
<=> StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c']) #生成名为foo的静态库,在Windows上是foo.lib,在unix上为libfoo.a
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c']) #生成名为foo的动态库,在Windows上是foo.dll,在unix上为libfoo.so
Object('add.c') #生成名为add的目标文件,在Windows上是add.obj,在unix上为add.o

Scons没有明显的依赖定义,Scons会为我们自动扫描依赖。我们只需告诉它构建出一个目标需要什么即可。Scons检查依赖关系中的文件变化的方法,除了通过时间戳,还可以通过MD5来判别,你可以通过设置Env来决定使用哪个。另外更强大的是你也可以自己编写文件更新检查方法放到SConstruct中被Scons调用,这些都是高级一些的功能,这里不细说,详情可参见Scons的doc

前面说过,实际项目的代码往往不可能都放到单一目录下,而是按照一定规则被放到有层次结构的目录体系中,Scons提供一个叫SConscript的方法支持这种情形。下面用一个复杂一些的例子来说明这种情形。
我们假设有一项目的目录结构如下:
- Test_Proj
    - SConstruct
    - include
        - base.h
        - module1.h
        - module2.h
    - main
        - main.c
    - module1
        - module1.c
    - module2
        - module2.c
    - xlib
        - include
            - xlib_base.h
            - add.h
            - sub.h
        - add
            - add.c
        - sub
            - sub.c
        - lib
针对该Proj,我们要将整个工程构建为一个可执行程序。简单分析一下,这个程序依赖xlib下的两个库以及main、module1和module2下的多个目标文件,这样顶层的SConstruct文件大致应该是这样的:
SConscript(['xlib/SConscript',
                  'module1/SConscript',
                  'module2/SConscript',
                  'main/SConscript'])
obj_files = ['main/main.obj', 'module1/module1.obj', 'module2/module2.obj']
Program('test-main', obj_files, LIBS = ['add', 'sub'], LIBPATH = 'xlib/lib')

在 顶层执行Scons时,Scons将递归的执行子目录下的SConscript,最后将SConscript执行后的成果放入到Program中进行 build。在module1、module2和main下面的SConscript则负责将本目录下的.c编译为目标文件,大致是这样的:
Object('module1.c', CPPPATH=['../include', '../xlib/include'])

而在xlib下的SConscript文件如下:
SConscript(['add/SConscript', 'sub/SConscript']) # 调用两个子目录下的SConscript

add和sub下的SConscript完成的是将目录下的文件编译成静态库后,copy到上层目录的lib下:
libfile = StaticLibrary('sub.c', CPPPATH = ['../include'])
libfilename = str(libfile[0])
Command('../lib/'+ libfilename, libfilename, Copy("$TARGET", "$SOURCE"))

编辑好所有文件后,在顶层执行scons,工程被顺利编译。如果想clean proj,我们无需像Make那样写一个clean target,而是直接执行scons -c即可完成clean。

上 面的SConstruct虽然可以正确的进行项目构建,但是还有缺陷,Scons一个大的优点就是有很好的可移植性,但是上面的SConstruct 中,obj_files中显式的出现了.obj字样,这在Windows平台是没有问题的,但是在unix等其他平台上则会构建错误。

解决这个问题的方法就是通过环境参数自动获得目标文件的后缀,见下面代码:
# SConstruct
import os

env = Environment();
dict = env.Dictionary()
objsuffix = dict['OBJSUFFIX']

SConscript(['xlib/SConscript',
                  'module1/SConscript',
                  'module2/SConscript',
                  'main/SConscript'])
obj_files = ['main/main'+objsuffix, 'module1/module1'+objsuffix, 'module2/module2'+objsuffix]
Program('test-main', obj_files, LIBS = ['add', 'sub'], LIBPATH = 'xlib/lib')

上 面只是一个能满足初步build工程的示例,但绝不是理想的SConstruct。如果你对Python非常熟悉(我连Python的初学者都不算 ^_^),相信会有更多的技巧来组织SConstruct。经过上面的例子的实践,我发现Scons基本功能上不逊色于Make,而易理解方面则要远好于 Make。有人说在项目规模较大时,Scons性能被人质疑。但至少我目前还没有什么数据可参考。

在工作中,我们不是每次都去重新在顶层 进行build,而是在某个src目录下局部编译进行语法检查,比如只在module1下将module1.c编译为目标文件,在Scons中,我们可以 用以下命令来完成这个功能:scons -f SConscript;同样如果只在module1下clean,可以执行scons -f SConscript -c;在这方面Scons的确比make多输入一些命令字符,不过你可以想办法(比如设置alias)解决这个问题。

这次先发掘到这,Scons还有很多高级功能,在其Doc里有说明,有需要者自取^_^。

收藏到:Del.icio.us

Powered by Jekyll and Theme by solid

本站总访问量