SCons 发掘 Scons

laofo · 2010年03月25日 · 4 次阅读

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明 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 的复杂度和较为陡峭的学习曲线有所了解,曾经尝试使用 Autoconf 和 Automake,但是都因上手困难而放弃。自己心底也一直想找到一个更简单一些的但又不失功能的适合 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

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 里有说明,有需要者自取^_^。

暂无回复。
需要 登录 后方可回复。