Scons学习【转】

scons 学习
作者:Sam(甄峰) sam_code@hotmail.com

http://www.scons.org/

Sam有个好朋友是做游戏的,整天嘲笑做嵌入式的用的工具多土多原始。gdb不是图形化,DDD又没VC调试好用,只会写Makefile等等。
刚好最近有需要使用scons。所以Sam准备学习之以回击这种挑衅。呵呵。

scons简介:
scons是一个Python写的自动化构建工具,从构建这个角度说,它跟GNU make是同一类的工具。

scons与其它工具最显著的差别就是:scons配置文件是Python script.

它的思想是跟GNU make完全不同的。GNU make的核心是“依赖关系”,我要做的事情,就是告诉系统,一个目标依赖什么东西,并且,当被依赖的东西发生变化时,我要做什么。
例如:
all: main.c
$(CC) $(CFLAGS) $(LFLAGS) main.c -o BT_remote -lbluetooth -lBluetooth_remote -lm -lpthread
意思是:all依赖于main.c。 如果main.c有了变化,作下面这些事。


这样做可以解决相当多的问题,但是也带来了一个最大的问题:我如何判别这个目标依赖什么?

对于一个两个,甚至十几个文件,我当然还比较容易搞清楚,谁依赖谁。但是当文件有成百上千个时,要分清楚谁依赖谁可就没这么容易了。尤其是C/C++头文件的依赖,如果手工分析的话,工程量可是不小。为了解决这个问题,GNU又提供了另外一套工具:Automake,使用程序来分析依赖性,然后辅助你产生makefile。

于是乎,就有人想了,既然如此,我干吗费那劲,用程序分析依赖性,然后生成一个文件,再交给另外一个程序去处理呢?既然依赖性需要用程序来分析,那么就直接交给构建工具本身去做不就好了吗?对的,这是一个非常自然的思路,于是,Java世界有了Ant,而 Python世界有了scons。

scons就是这样一个构建工具:你告诉它要做的任务,以及完成这个任务需要的输入,以及这个任务产生的输出,怎么做这个任务(当然其中就包括依赖性分析),就交给工具本身完成。


例 1:
只有一个 test.c文件,需要编译成可执行文件。
创建SConstruct,内容为:
Program('test.c')
这句话透露2层意思:
1.编译方法:想要编译成什么类型。(Program:可执行文件)
2.哪个东西想编译(test.c)
#scons
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o test.o -c test.c
cc -o test test.o
scons: done building targets.

clean:
#scons -c
则把编译出的东西又删除了,类似make clean


例2:
只有一个文件,想编译成object文件。
Object('test.c')
1. 编译方法:想要编译成什么类型。(Object: .o文件)
2.哪个东西想编译(test.c)

#scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
scons: done building targets.

清空:
#scons -c
则把编译出的东西又删除了,类似 make clean

例3:
有多个.c文件,并想指定生成的程序名:
Program('program', ['prog.c', 'file1.c', 'file2.c'])
生成可执行文件 program, 由prog.c file1.c file2.c 生成。
#scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o program prog.o file1.o file2.o

甚至可以这样写:
common_sources = ['file1.c', 'file2.c']
Program('program2', common_sources + ['program2.c'])



例4:
有很多个.c文件,不想一个一个指定,类似Makefile中 *.o: *.c
Program('test', Glob('*.c'))


不管使用common_sources = ['file1.c', 'file2.c'] 还是Program('test1.c'....'testn.c')
每个文件名都需要被单或者双引号包起来。这太不方便了。
SCons 提供了Split函数
Program('program', Split('main.c file1.c file2.c'))
或:
src_files = Split('main.c file1.c file2.c')
Program('program', src_files)



例5:
SCons还对 output file和source file有关键字,可以用来指定

src_files = Split('main.c file1.c file2.c')
Program(target = 'program', source = src_files)
因为有关键字指定,多以可以换前后:
Program(source = src_files, target = 'program')


例6:
创建库:
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])
#scons -Q
cc -o f1.o -c f1.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o f4.o
ranlib libfoo.a

例7:建立静态和动态库:
静态库:其实使用Library也是建立静态库。
StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
动态库:
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])

% scons -Q
cc -o f1.os -c f1.c
cc -o f2.os -c f2.c
cc -o f3.os -c f3.c
cc -o libfoo.so -shared f1.os f2.os f3.os

例8:使用静态和动态库:
使用$LIBS 来指定库,使用$LIBPATH 指定库位置。
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')
% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
cc -o prog.o -c prog.c
cc -o prog prog.o -L. -lfoo -lbar


Program('prog.c', LIBS = ['m'], LIBPATH = ['/usr/lib', '/usr/local/lib'])
% scons -Q
cc -o prog.o -c prog.c
cc -o prog prog.o -L/usr/lib -L/usr/local/lib -lm




以BTX为例使用scons:
Sam 创建了一个SConstruct,(类似于GNU make的Makefile文件,是scons的默认文件名).

Sam首先想生成 BTX library.
然后想将library 和 main.c和成一个可执行文件:BTX_Test
内容如下:
#指定生成文件名为:libBTX.a. 由BTX.c生成的。-I'/opt/hisilicon/toolchains/arm-uclibc-linux-soft/include -I../include
StaticLibrary('BTX', ['BTX.c'], CPPPATH=['/opt/hisilicon/toolchains/arm-uclibc-linux-soft/include', '../include'])
Program('BTX_Test', ['main.c'], CPPPATH=['/opt/hisilicon/toolchains/arm-uclibc-linux-soft/include', '../include'], LIBS=['BTX','blu\etooth','pthread'], LIBPATH=['.', '/usr/lib'])

#scons
提示之不到bluetooth库,Sam在 /usr/lib中将 libbluetooth.so 软连接到libbluetooth.so.2。
于是编译成功,所以,Sam觉得Scons和 Makefile很类似,只需要编译时指定-I ,-L, -l就好了。对应关系如下:
-I:CPPPATH=['/opt/hisilicon/toolchains/arm-uclibc- linux-soft/include', '../include']
-L:LIBPATH=['.', '/usr/lib']
-l: LIBS=['BTX','bluetooth','pthread']
CPPPATH,LIBPATH,LIBS,CC等从:
http://www.scons.org/doc/1.2.0/HTML/scons-user/a4774.html#cv-CPPPATH 中找到
同时请注意,最好在每个关键字赋值后面都跟 [],里面有多个时用逗号分开。


X5 Version:
StaticLibrary('BTX', ['BTX.c'], CC=['/opt/hisilicon/toolchains/arm-uclibc-linux-soft/bin/arm-uclibc-linux-gcc'], CPPPATH=['/opt/his\ilicon/toolchains/arm-uclibc-linux-soft/include', '../include'])
Program('BTX_Test', ['main.c'], CC=['/opt/hisilicon/toolchains/arm-uclibc-linux-soft/bin/arm-uclibc-linux-gcc'], CPPPATH=['/opt/his\ilicon/toolchains/arm-uclibc-linux-soft/include', '../include'], LIBS=['BTX','bluetooth','pthread'], LIBPATH=['./', '../resource/']\)



scons只需要描述任务,并不需要指定依赖关系,甚至我们都没有指出头文件,但是你修改BTX.h的时候仍然会触发构建。这是因为scons内部有个scanner,可以帮助扫描包含文件的关系。
我们还发现,这里我们根本没有指定编译器,也没有指定编译选项,但scons仍然很聪明的选择了gcc. 这是因为scons内置提供了很多编译器及其对应选项的选择,然后对于不同的平台,会有一个默认项。



Environments:
Scons有3种环境变量:
1. External Environment
2. Construction Environment
3. Execution Environment

当想要使用External Environment,必须通过os.environ。这意味着首先要加入:
import os
之后就可以在SConscript文件中使用os.environ来初始化 construction environments 为用户环境变量了。

1.创建Construction Environment
env = Environment()

例如:
import os
env = Environment(CC = 'gcc', CCFLAGS = '-O2')
env.Program('foo.c')



最重要的环境变量是:Construction Environment
它在复杂的工程中非常重要,例如,不同的源文件需要不同的编译选项。或者不同的可执行程序需要链接不同的库。scons提供Construction Environment。让我们可以创建不同的Construction Environment来创建不同的编译选项。

1. 创建Construction Environment ,使用Environment function。
env = Environment() 所有的Construction Environment 项目值全用scons自己找到的。
env = Environment(CC = 'gcc', CCFLAGS = '-O2') ,其他的与上一个相同,但只有CC和CCFLAGS 自定义。

2. 察看Construction Environment 每个项目值:注[1]
需要使用Dictionary() function
env = Environment()
dict = env.Dictionary()
keys = dict.keys()
keys.sort()
for key in keys:
print "construction variable = '%s', value = '%s'" % (key, dict[key])
打印所有Construction Environment项目。








scons中Python模块的导入:
1. import os
os 模块包含普遍的操作系统功能。如果你希望你的程序能够与平台无关的话,这个模块是尤为重要的。即它允许一个程序在编写后不需要任何改动,也不会发生任何问题,就可以在Linux和Windows下运行。一个例子就是使用os.sep可以取代操作系统特定的路径分割符。


os.name 字符串指示你正在使用的平台。比如对于Windows,它是'nt',而对于Linux/Unix用户,它是'posix'。
os.getcwd() 函数得到当前工作目录,即当前Python脚本工作的目录路径。
os.getenv()和os.putenv()函数分别用来读取和设置环境变量。
os.listdir()返回指定目录下的所有文件和目录名。
os.remove()函数用来删除一个文件。
os.system() 函数用来运行shell命令。
os.linesep字符串给出当前平台使用的行终止符。例如,Windows使用'\r\n',Linux使用' \n'而Mac使用'\r'。
os.path.split()函数返回一个路径的目录名和文件名。
os.environ Shell variables
这里重点说os.environ:
它应该是Linux环境变量。

例 1:
想要得到当前Linux环境变量 PATH,LD_LIBRARY_PATH,HOME等。(这与前面讲的环境变量对应起来了)

#导入OS模块
import os
#创建一个Construction Environment
env=Environment()
print "HOME: ", os.environ["HOME"]
则打印结果为HOME:/home/sam。这样就得到了Linux环境变量。

例 2:
想要用当前Linux环境变量ARMGCC来设置toolchain.

首先:在Linux下:
export ARMGCC=arm-linux-gcc

之后,修改config
#导入OS模块
import os
#创建一个Construction Environment
env=Environment()
if os.environ.has_key('ARMGCC'):
env.Replace(CC = os.environ['ARMGCC'])
则使用arm-linux-gcc取代了Construction Environment中的CC。



2. import sys
sys模块包含系统对应的功能.sys模块包含了与Python解释器和它的环境有关的函数.
import sys
if sys.byteorder == 'little' or env['PLATFORM'] == 'win32':
env.Append(CPPDEFINES = ['LSB_FIRST'])


3. import platform
platform得到编译信息:
platform.processor() ---i686
platform.python_build()---('r251:54863', 'Jun 4 2007 15:03:52')
platform.python_compiler()----'GCC 3.4.4 20050721 (Red Hat 3.4.4-2)'
platform.python_version()----'2.5.1'
platform.release()----'2.6.9-22.ELsmp'
platform.system()----'Linux'
platform.version()----'#1 SMP Mon Sep 19 18:32:14 EDT 2005'
platform.uname()----('Linux', 'nec', '2.6.9-22.ELsmp', '#1 SMP Mon Sep 19 18:32:14 EDT 2005', 'i686' , 'i686')
platform.machine()----'i686'
platform.libc_ver()----('glibc', '2.0')




注[1]:
所有Construction Variables可以在下面找到:
Construction Variables可用在:
1. CPPPATH,LIBS,LIBPATH。(在BTX例子中可见)
2. Construction Environment(env=Environment)中的项目。

Appendix A. Construction Variables

 

 

 

 

 

 

 

 

 

 

 

 

 

scons简介by 令狐虫

云风在blog上写了一组《IDE 不是程序员的唯一选择》的文章。题目很吸引人,以至于我一直以为他要写什么鸿篇巨制,可惜到最后只出现了一组《GNU make入门指南》。(笑)

被IDE绑定的确是一件很悲哀的事情。作为一个程序员,当然应该搞清楚程序编译链接的整个流程,makefile给出了很好的一个路径,让我们能够了解这一点。

但是,如果觉得程序员就应该比拼“手写汇编代码”、“用记事本写程序”,那就大大的错了。程序员需要了解细节,但不意味着程序员都是傻瓜。为什么要放着好好的提高生产力的工具不用,一切从零做起呢?(当然了,上面这句话并不能成为使用盗版的理由。)所以寻找并了解一些优秀的工具,也是我们这些程序员所需要做的事情之一。

GNU make 当然是一个很好的工具,云风已经讲了很多,我就不啰嗦了。我今天想介绍另一个优秀的自动构建工具scons

 

scons是一个Python写的自动化构建工具,从构建这个角度说,它跟GNU make是同一类的工具。它有什么好处呢?在它自己的网站上,当然写了一大堆了,快速、稳定、强大、跨平台、可扩展……。不过我们还是从自己的角度来看看它到底好在哪里。

刚刚提到scons从目的而言跟GNU make是同一类的工具。但是实际上,它的思想是跟GNU make完全不同的。GNU make的核心是“依赖关系”,我要做的事情,就是告诉系统,一个目标依赖什么东西,并且,当被依赖的东西发生变化时,我要做什么。这样做可以解决相当多的问题,但是也带来了一个最大的问题:我如何判别这个目标依赖什么?

对于一个两个,甚至十几个文件,我当然还比较容易搞清楚,谁依赖谁。但是当文件有成百上千个时,要分清楚谁依赖谁可就没这么容易了。尤其是C/C++头文件的依赖,如果手工分析的话,工程量可是不小。为了解决这个问题,GNU又提供了另外一套工具:Automake,使用程序来分析依赖性,然后辅助你产生makefile。

于是乎,就有人想了,既然如此,我干吗费那劲,用程序分析依赖性,然后生成一个文件,再交给另外一个程序去处理呢?既然依赖性需要用程序来分析,那么就直接交给构建工具本身去做不就好了吗?对的,这是一个非常自然的思路,于是,Java世界有了Ant,而Python世界有了scons。

scons就是这样一个构建工具:你告诉它要做的任务,以及完成这个任务需要的输入,以及这个任务产生的输出,怎么做这个任务(当然其中就包括依赖性分析),就交给工具本身完成。

说了这么多,我们来看看一个现实世界的scons是什么样子的。

我们假设有一个C程序,由三个文件组成:

//-----func.cppint 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 <stdio.h>#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.cppg++ -o func.o -c func.cppg++ -o add_main main.o func.oscons: done building targets.

这时,我们就会得到一个add_main的可执行程序。

如果执行scons -c,我们会看到:

scons: Reading SConscript files ...scons: done reading SConscript files.scons: Cleaning targets ...Removed main.oRemoved func.oRemoved add_mainscons: done cleaning targets.

生成的可执行程序连带中间结果都被清除了。

这里我们可以看到,scons只需要描述任务,并不需要指定依赖关系,甚至我们都没有指出头文件,但是你修改func.h的时候仍然会触发构建。这是因为scons内部有个scanner,可以帮助扫描包含文件的关系。(我们可以编写自己的构建任务,当然也可以编写自己的scanner,有兴趣的可以看帮助,这里就多说了)

我们还发现,这里我们根本没有指定编译器,也没有指定编译选项,但scons仍然很聪明的选择了g++(这是Linux上的结果,如果是Windows,默认会选择cl也就是Visual C++),并且给出了正确的编译选项。事实上,这是因为scons内置提供了很多编译器及其对应选项的选择,然后对于不同的平台,会有一个默认项。我们当然也可以自己选择编译环境,比如在Windows下,我同时安装了VC和mingW,但是我想用mingW来编译而不是VC,就可以这样指定:

import osenv = 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/test/unit_test.hpp>BOOST_AUTO_TEST_CASE( add_test ){ BOOST_CHECK( add(2, 2) == 4 );}

然后我们写一个SConstruct:

#所有的需要测试的单元文件(去除两个主文件)import globobj_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的强大威力了。至于进一步的深入,就看各位自己的了。

 

 原文地址 http://www.go4pro.org/?p=47

posted @ 2012-11-08 12:25  Leo Forest  阅读(1562)  评论(0编辑  收藏  举报