scons

96
仙灵儿
0.2 2018.11.20 23:33 字数 15201

第一章:编译和安装SCons
第二章:简单编译
第三章:编译相关的一些事情
第四章:编译和链接库文件
第五章:节点对象
第六章:依赖性
第七章:环境
第八章:自动将命令行选项赋值给Construction变量

1、安装Python
因为SCons是用Python编写的,所以你必须在使用SCons之前安装好Python。你在安装Python之前,应该注意查看Python是否在你的系统里已经可用了(在系统的命令行中运行python -V或python --version)。
$python -V
Python 2.5.1
在一个Windows系统里,
C:>python -V
Python 2.5.1
如果Python没有安装,你会看到一条错误消息比如“command not found”(在UNIX或Linux里)或"python is not recognized as an internal or external command, operable program or batch file"(在Windows里)。在这种情况下,在你安装SCons之前需要先安装Python。
有关下载和安装Python的信息可以从http://www.python.org/download/得到。

2、从预编译包中安装SCons
2.1、在Red Hat(或者基于RPM)Linux系统里安装SCons
在使用RPM(Red Hat Package Manager)的Red Hat Linux,Fedora或者任何其他Linux发行版里,SCons是预编译好的RPM格式,准备被安装的。你的发行版可能已经包含了一个预编译好的SCons RPM。
如果你的发行版支持yum安装,你可以运行如下命令安装SCons:

yum install scons

如果你的Linux发行版没有包含一个特定的SCons RPM文件,你可以下载SCons项目提供的通用的RPM来安装。这会安装SCons脚本到/usr/bin目录,安装SCons库模块(library modules)到/usr/lib/scons。
从命令行安装,下载合适的.rpm文件,然后运行:

rpm -Uvh scons-2.1.0-1.noarch.rpm

2.2、在Debian Linux系统里安装SCons
如果你的系统已经连上了因特网,你可以运行如下命令来安装最新的官方Debian包:

apt-get install scons

2.3、在Windows系统里安装SCons
SCons提供了一个Windows installer,使得安装变得非常容易。从http://www.scons.org/download.php下载scons-2.1.0.win32.exe。然后你需要做的就是执行这个文件。

3、在任何系统里编译和安装SCons
如果你的系统里没有一个预编译的SCons包,你可以使用本地python distutils包很容易地编译和安装SCons。
第一步就是下载scons-2.1.0.tar.gz或scons-2.1.0.zip,地址http://www.scons.org/download.html
解压下载的文件,会创建一个叫scons-2.1.0的目录,进入这个目录执行如下命令安装SCons:

cd scons-2.1.0

python setup.py install

这将会编译SCons,安装scons脚本到python目录(/usr/local/bin或C:\Python25\Scripts),同时会安装SCons编译引擎到python使用的库目录(/usr/local/lib/scons或C:\Python25\scons)。因为这些都是系统目录,你可能需要root或管理员权限去安装SCons。

3.1、编译和安装多个版本的SCons
SCons的setup.py脚本有一些扩展,这些扩展支持安装多个版本的SCons到不同的位置。这让下载和体验不同版本的SCons变得很容易。
安装SCons到指定版本的位置,调用setup.py的时候增加--version-lib选项:

python setup.py install --version-lib

这将会安装SCons编译引擎到/usr/lib/scons-2.1.0或C:\Python25\scons-2.1.0目录。

3.2、安装SCons到其他的位置
你可以安装SCons到其他的位置,而不是默认的位置,指定--prefix=选项:

python setup.py install --prefix=/opt/scons

这将会安装scons脚本到/opt/scons/bin,安装编译引擎到/opt/scons/lib/scons。
你可以同时指定--prefix和--version-lib,这个时候setup.py将会安装编译引擎到相对于指定prefix的特定版本的目录,在刚才的例子上加上--version-lib,将会安装编译引擎到/opt/scons/lib/scons-2.1.0。

3.3、没有管理员权限的情况下编译和安装SCons
如果你没有权限安装SCons到系统目录,使用--prefix选项安装到你选择的其他的位置。例如,安装SCons到相对于用户HOME目录的合适的位置,scons脚本安装到HOME/bin,编译引擎安装到$HOME/lib/scons,使用如下命令:

python setup.py install --prefix=$HOME

1、编译简单的C/C++程序

这是一个用C语言编写的著名的"Hello,World!"程序:
int main()
{
printf("Hello, World!\n");
}
用SCons编译它,需要在一个名为SConstruct的文件中输入如下命令:
Program('hello.c')
这个短小的配置文件给了SCons两条信息:你想编译什么(一个可执行程序),你编译的输入文件(hello.c)。Program是一个编译器方法(builder_method),一个Python调用告诉SCons,你想编译一个可执行程序。

现在运行scons命令编译这个程序。在Linux或Unix系统上,你会看到如下输出:
% scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.

在一个带有微软Visual C++编译器的Windows系统上,你会看到如下输出:
C:>scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target,source,env)
scons: done building targets.

首先,你仅仅需要指定源文件,SCons会正确地推断出目标文件和可执行文件的名字。
其次,同样的SConstruct文件,在Linux和Windows上都产生了正确的输出文件:在POSIX系统上是hello.o和hello,在Windows系统上是hello.obj和hello.exe。这是一个简单的例子,说明了SCons使得编写程序编译脚本变得很容易了。

2、编译目标程序

Program编译方法是SCons提供的许多编译方法中一个。另一个是Object编译方法,告诉SCons从指定的源文件编译出一个目标文件:
Object('hello.c')

现在运行scons命令编译,在POSIX系统里它仅仅编译出hello.o目标文件:
% scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
cc -o hello.o -c hello.c
scons: done building targets.

在Windows系统里编译出hello.obj目标文件:
C:>scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
cl /Fohello.obj /c hello.c /nologo
scons: done building targets.

3、简单的JAVA编译

SCons同样使得编译Java也很容易了。不像Program和Object两个编译方法,Java编译方法需要你指定一个目录,这个目录是用来存放编译后的class文件的,以及一个存放.java源文件的目录:
Java('classes', 'src')

如果src目录仅仅包含一个hello.java文件,那么运行scons命令的输出会如下所示(在POSIX系统里):
% scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
javac -d classes -sourcepath src src/hello.java
scons: done building targets.

4、编译之后清除

使用SCons,编译之后想要清除不需要增加特殊的命令或目标名。你调用SCons的时候,使用-c或--clean选项,SCons就会删除合适的编译产生的文件。
% scons -c

5、SConstruct文件

如果你使用过Make编译系统,你应该可以推断出SConstruct文件就相当于Make系统中的Makefile。SCons读取SConstruct文件来控制程序的编译。

5.1、SConstruct文件是Python脚本
SConstruct文件实际上就是一个Python脚本。你可以在你的SConstruct文件中使用Python的注释:

Arrange to build the "hello" program.

Program('hello.c') #"hello.c" is the source file.

5.2、SCons的函数是顺序无关的
重要的一点是SConstruct文件并不完全像一个正常的Python脚本那样工作,其工作方式更像一个Makefile,那就是在SConstruct文件中SCons函数被调用的顺序并不影响SCons你实际想编译程序和目标文件的顺序。换句话说,当你调用Program方法,你并不是告诉SCons在调用这个方法的同时马上就编译这个程序,而是告诉SCons你想编译这个程序,例如,一个程序由一个hello.c文件编译而来,这是由SCons决定在必要的时候编译这个程序的。
SCons通过打印输出状态消息来显示它何时在读取SConstruct文件,何时在实际编译目标文件,进而来区分是在调用一个类似Program的编译方法还是在实际地编译这个程序。

看下面这个例子:
print "Calling Program('hello.c')"
Program('hello.c')
print "Calling Program('goodbye.c')"
Program('goodbye.c')
print "Finished calling Program()"

执行SCons,我们看到print语句的输出是在读取Sconstruct文件消息之间,说明了那才是Python语句执行的时候:
% scons
scons: Reading Sconscript files...
Calling Program('hello.c')
Calling Program('goodbye.c')
Finished Calling Program()
scons: done reading SConscript files...
scons: Building targets...
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.

6、使Scons输出更简洁

你已经看到过SCons编译的时候会打印一些消息,那些消息围绕着实际用来编译程序的命令:
C:\scons
scons: Reading SConscript files...
scons: done reading SConscript files.
scons: Building targets...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.
这些消息反映了SCons工作时候的顺序。

一个缺点就是,这些消息使得输出看起来很混乱。当调用SCons的时候使用-Q选项,可以屏蔽掉那些与实际编译程序命令无关的消息:
C:>scons -Q
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)

1、指定目标文件的名字
当你调用Program编译方法的的时候,它编译出来的程序名字是和源文件名是一样的。下面的从hello.c源文件编译一个可执行程序的调用将会在POSIX系统里编译出一个名为hello的可执行程序,在windows系统里会编译出一个名为hello.exe的可执行程序。
Program('hello.c')
如果你想编译出来的程序的名字与源文件名字不一样,你只需要在源文件名的左边声明一个目标文件的名字就可以了:
Program('new_hello','hello.c')
现在在POSIX系统里运行scons,将会编译出一个名为new_hello的可执行程序:
% scons -Q
cc -o hello.o -c hello.c
cc -o new_hello hello.o

2、编译多个源文件
通常情况下,你需要使用多个输入源文件编译一个程序。在SCons里,只需要就多个源文件放到一个Python列表中就行了,如下所示:
Program(['prog.c','file1.c','file2.c'])
运行scons编译:
% 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 prog prog.o file1.o file2.o
注意到SCons根据源文件列表中的第一个源文件来决定输出程序的名字。如果你想指定一个不同的程序名称,你可以在源文件列表的右边指定程序名,如下所示指定输出程序名为program:
Program('program',['prog.c','file1.c','file2.c'])

3、使用Glob指定文件列表
你可以使用Glob函数,定义一个匹配规则来指定源文件列表,比如,?以及[abc]等标准的shell模式。如下所示:
Program('program', Glob('
.c'))

4、指定单个文件以及文件列表
有两种方式为一个程序指定源文件,一个是文件列表:
Program('hello', ['file1.c', 'file2.c'])
一个是单个文件:
Program('hello', 'hello.c')
也可以把单个文件放到一个列表中,
Program('hello', ['hello.c'])
对于单个文件,SCons函数支持两种方式。实际上,在内部,SCons把所有的输入都是看成列表的,只是在单个文件的时候,允许我们省略方括号。

5、使文件列表更易读
为了更容易处理文件名长列表,SCons提供了一个Split函数,这个Split函数可以将一个用引号引起来,并且以空格或其他空白字符分隔开的字符串分割成一个文件名列表,示例如下:
Program('program', Split('main.c file1.c file2.c'))
或者
src_files=Split('main.c file1.c file2.c')
Program('program', src_files)
同时,Split允许我们创建一个文件列表跨跃多行,示例如下:
src_files=Split("""main.c
file1c
file2.c""")
Program('program', src_files)

6、关键字参数
SCons允许使用Python关键字参数来标识输出文件和输入文件。输出文件是target,输入文件是source,示例如下:
src_files=Split('main.c file1.c file2.c')
Program(target='program', source=src_files)
或者
src_files=Split('main.c file1.c file2.c')
Program(source=src_files, target='program')

7、编译多个程序
如果需要用同一个SConstruct文件编译多个文件,只需要调用Program方法多次:
Program('foo.c')
Program('bar', ['bar1.c', 'bar2.c'])

8、在多个程序之间共享源文件
在多个程序之间共享源文件是很常见的代码重用方法。一种方式就是利用公共的源文件创建一个库文件,然后其他的程序可以链接这个库文件。
另一个更直接,但是不够便利的方式就是在每个程序的源文件列表中包含公共的文件,示例如下:
Program(Split('foo.c common1.c common2.c'))
Program('bar', Split('bar1.c bar2.c common1.c common2.c'))
如果程序之间共享的源文件过多,可以简化:
common=['common1.c', 'common2.c']
foo_files=['foo.c'] + common
bar_files=['bar1.c', 'bar2.c'] + common
Program('foo', foo_files)
Program('bar', bar_files)

1、编译库文件
你可以使用Library方法来编译库文件:
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
SCons会根据你的系统使用合适的库前缀和后缀。所以在POSIX系统里,上面的例子会如下编译:
% 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
如果你不显示指定目标库的名字,SCons会使用第一个源文件的名字。

1.1、使用源代码或目标文件编译库文件
除了使用源文件外,Library也可以使用目标文件,如下所示:
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])

1.2、使用StaticLibrary显示编译静态库
Library函数是用来编译静态库的。如果你想显示指定需要编译静态库,可以使用StaticLibrary替代Library:
StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])

1.3、使用SharedLibrary编译动态库
如果想编译动态库(在POSIX系统里)或DLL文件(Windows系统),可以使用SharedLibrary:
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
在POSIX里运行scons编译:
% 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

2、链接库文件
链接库文件的时候,使用LIBS变量指定库文件,使用LIBPATH指定存放库文件的目录:
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')
注意到,你不需要指定库文件的前缀(比如lib)或后缀(比如.a或.lib),SCons会自动匹配。
% scons -Q
cc -o f1.os -c f1.c
cc -o f2.os -c f2.c
cc -o f3.os -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

3、LIBPATH告诉去哪里找库 默认情况下,链接器只会在系统默认的库目录中寻找库文件。SCons也会去LIBPATH指定的目录中去寻找库文件。$LIBPATH由一个目录列表组成,如下所示:
Program('prog.c', LIBS='m', LIBPATH=['/usr/lib', '/usr/local/lib'])
使用Python列表的好处是可以跨平台。另一种可选方式是,把库目录用系统特定的路径分隔符连接成一个字符串:
在POSIX系统里:
LIBPATH='/usr/lib:/usr/local/lib'
在Windows里:
LIBPATH='C:\lib;D:\lib'
当链接器执行的时候,SCons会创建合适的flags,使得链接器到指定的库目录寻找库文件。上面的例子在POSIX系统里编译:
% scons -Q
cc -o prog.o -c prog.c
cc -o prog prog.o -L/usr/lib -L/usr/local/lib -lm

1、编译方法返回目标节点列表
所有编译方法会返回一个节点对象列表,这些节点对象标识了那些将要被编译的目标文件。这些返回出来的节点可以作为参数传递给其他的编译方法。
例如,假设我们想编译两个目标文件,这两个目标有不同的编译选项,并且最终组成一个完整的程序。这意味着对每一个目标文件调用Object编译方法,如下所示:
Object('hello.c', CCFLAGS='-DHELLO')
Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(['hello.o', 'goodbye.o'])
这样指定字符串名字的问题就是我们的SConstruct文件不再是跨平台的了。因为在Windows里,目标文件成为了hello.obj和goodbye.obj。
一个更好的解决方案就是将Object编译方法返回的目标列表赋值给变量,这些变量然后传递给Program编译方法:
hello_list = Object('hello.c', CCFLAGS='-DHELLO')
goodbye_list = Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(hello_list + goodbye_list)
这样就使得SConstruct文件是跨平台的了。

2、显示创建文件和目录节点
在SCons里,表示文件的节点和表示目录的节点是有清晰区分的。SCons的File和Dir函数分别返回一个文件和目录节点:
hello_c=File('hello.c')
Program(hello_c)
classes=Dir('classes')
Java(classes, 'src')
通常情况下,你不需要直接调用File或Dir,因为调用一个编译方法的时候,SCons会自动将字符串作为文件或目录的名字,以及将它们转换为节点对象。只有当你需要显示构造节点类型传递给编译方法或其他函数的时候,你才需要手动调用File和Dir函数。
有时候,你需要引用文件系统中一个条目,同时你又不知道它是一个文件或一个目录,你可以调用Entry函数,它返回一个节点可以表示一个文件或一个目录:
xyzzy=Entry('xyzzy')

3、打印节点文件名
你可能需要经常做的就是使用一个节点来打印输出这个节点表示的文件名。因为一个编译方法调用返回的对象是一个节点列表,你必须使用Python脚本从列表中获得单个节点。例如,如下的SConstruct文件:
hello_c=File('hello.c')
Program(hello_c)
classes=Dir('classes')
Java(classes, 'src')
object_list=Object('hello.c')
program_list=Program(object_list)
print "The object file is:", object_list[0]
print "The program file is:", program_list[0]

4、将一个节点的文件名当作一个字符串
如果你不是想打印文件名,而是做一些其他的事情,你可以使用内置的Python的str函数。例如,你想使用Python的os.path.exists判断一个文件是否存在:
import os.path
program_list=Program('hello.c')
program_name=str(program_list[0])
if not os.path.exists(program_name):
print program_name, "does not exist!"
在POSIX系统里执行scons:
% scons -Q
hello does not exist!
cc -o hello.o -c hello.c
cc -o hello hello.o

5、GetBuildPath:从一个节点或字符串中获得路径
env.GetBuildPath(file_or_list)返回一个节点或一个字符串表示的路径。它也可以接受一个节点或字符串列表,返回路径列表。如果传递单个节点,结果就和调用str(node)一样。路径可以是文件或目录,不需要一定存在:
env=Environment(VAR="value")
n=File("foo.c")
print env.GetBuildPath([n, "sub/dir/$VAR"])
将会打印输出如下:
% scons -Q
['foo.c', 'sub/dir/value']
scons: . is up to date.
有一个函数版本的GetBuildPath,不需要被一个Environment调用,它是基于SCons默认的Environment来使用的。

到目录为止,我们已经看到了SCons是如何一次性编译的。但是SCons这样的编译工具的一个主要的功能就是当源文件改变的时候,只需要重新编译那些修改的文件,而不会浪费时间去重新编译那些不需要重新编译的东西。如下所示:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: '.' is up to date.
第二次执行的时候,SCons根据当前的hello.c源文件判断出hello程序是最新的,避免了重新编译。

1、决定一个输入文件何时发生了改变:Decider函数
默认情况下,SCons通过每个文件内容的MD5签名,或者校验和来判断文件是否是最新的,当然你也可以配置SCons使用文件的修改时间来判断。你甚至可以指定你自己的Python函数来决定一个输入文件是否发生了改变。

1.1、使用MD5签名来决定一个文件是否改变
默认情况下,SCons根据文件内容的MD5校验和而不是文件的修改时间来决定文件是否改变。如果你想更新文件的修改时间,来使得SCons重新编译,那么会失望的。如下所示:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
上面的例子中即使文件的修改时间变了,SCons认为文件的内容没有改变,所以不需要重新编译。但是如果文件的内容改变了,SCons会侦测到并且重新编译的:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
你也可以显示指定使用MD5签名,使用Decider函数:
Program('hello.c')
Decider('MD5')

1.1.1、使用MD5签名的衍生
使用Md5签名去决定一个输入文件是否改变,有一个好处:如果一个源文件已经改变了,但是由它重新编译出来的目标文件的内容和由它修改前编译出来的目标文件一样,那么那些依赖这个重新编译的但是内容没变的目标文件的其他目标文件是不需要重新编译的。
例如,一个用户仅仅改变了hello.c文件中的注释,那么重新编译出来的hello.o文件肯定是不变的。SCons将不会重新编译hello程序:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE A COMMENT IN hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.

1.2、使用时间戳(Time Stamps)来决定一个文件是否改变
SCons允许使用两种方式使用时间戳来决定一个输入文件是否已经改变。
最熟悉的方式就是Make使用时间戳的方式:如果一个源文件的修改时间比目标文件新,SCons认为这个目标文件应该重新编译。调用Decider函数如下:
Object('hello.c')
Decider('timestamp-newer')
并且因为这个行为和Make的一样,你调用Decider函数的时候可以用make替代timestamp-newer:
Object('hello.c')
Decider('make')
使用和Make一样时间戳的一个缺点就是如果一个输入文件的修改时间突然变得比一个目标文件旧,这个目标文件将不会被重新编译。例如,如果一个源文件的一个旧的拷贝从一个备份中恢复出来,恢复出来的文件的内容可能不同,但是目标文件将不会重新编译因为恢复出来的源文件的修改时间不比目标文件文件新。
因为SCons实际上存储了源文件的时间戳信息,它可以处理这种情况,通过检查源文件时间戳的精确匹配,而不是仅仅判断源文件是否比目标文件新。示例如下:
Object('hello.c')
Decider('timestamp-match')

1.3、同时使用MD5签名和时间戳来判断一个文件是否改变
SCons提供了一种方式,使用文件内容的MD5校验和,但是仅仅当文件的时间戳改变的时候去读文件的内容:
Program('hello.c')
Decider('MD5-timestamp')
使用Decider('MD5-timestamp')的唯一缺点就是SCons将不会重新编译一个目标文件,如果SCons编译这个文件后的一秒以内源文件被修改了。

1.4、编写你自己的Decider函数
我们传递给Decider函数的不同的字符串实际上是告诉SCons去选择内部已经实现的决定文件是否改变的函数。我们也可以提供自己的函数来决定一个依赖是否已经改变。
例如,假设我们有一个输入文件,其包含了很多数据,有特定的格式,这个文件被用来重新编译许多不同的目标文件,但是每个目标文件仅仅依赖这个输入文件的一个特定的区域。我们希望每个目标文件仅仅依赖自己在输入文件中的区域。但是,因为这个输入文件可能包含了很多数据,我们想仅仅在时间戳改变的时候才打开这个文件。这个可以通过自定义的Decider函数实现:
Program('hello.c')
def decide_if_changed(dependency,target,prev_ni):
if self.get_timestamp()!=prev_ni.timestamp:
dep=str(dependency)
tgt=str(target)
if specific_part_of_file_has_changed(dep,tgt):
return True
return False
Decider(decide_if_changed)
在函数定义中,depandency(输入文件)是第一个参数,然后是target。它们都是作为SCons节点对象传递给函数的,所以我们需要使用str()转换成字符串。
第三个参数,prev_ni,是一个对象,这个对象记录了目标文件上次编译时所依赖的签名和时间戳信息。prev_ni对象可以记录不同的信息,取决于dependency参数所表示的东西的类型。对于普通的文件,prev_ni对象有以下的属性:
.csig:target上次编译时依赖的dependency文件内容的内容签名或MD5校验和
.size:dependency文件的字节大小
.timestamp:dependency文件的修改时间
注意如果Decider函数中的一些参数没有影响到你决定dependency文件是否改变,你忽略掉这些参数是很正常的事情。
以上的三个属性在第一次运行的时候,可能不会出现。如果没有编译过,没有target创建过也没有.sconsign DB文件存在过。所以,最好总是检查prev_ni的属性是否可用。
以下是一个基于csig的decider函数的例子,注意在每次函数调用的时候,dependency文件的签名信息是怎么样通过get_csig初始化的:

env = Environment()

def config_file_decider(dependency, target, prev_ni):
import os.path

We always have to init the .csig value...

dep_csig = dependency.get_csig()

.csig may not exist, because no target was built yet...

if 'csig' not in dir(prev_ni):
return True

Target file may not exist yet

if not os.path.exists(str(target.abspath)):
return True
if dep_csig != prev_ni.csig:

Some change on source file => update installed one

return True
return False

def update_file():
f = open("test.txt","a")
f.write("some line\n")
f.close()

update_file()

Activate our own decider function

env.Decider(config_file_decider)

env.Install("install","test.txt")

1.5、混合使用不同的方式来决定一个文件是否改变
有些时候,你想为不同的目标程序配置不同的选项。你可以使用env.Decider方法影响在指定construction环境下编译的目标程序。
例如,如果我们想使用MD5校验和编译一个程序,另一个使用文件的修改时间:
env1=Environment(CPPPATH=['.'])
env2=env1.Clone()
env2.Decider('timestamp-match')
env1.Program('prog-MD5','program1.c')
env2.Program('prog-timestamp','program2.c')

2、决定一个输入文件是否改变的旧函数
SCons2.0之前的两个函数SourceSignatures和TargetSignatures,现在不建议使用了。

3、隐式依赖:$CPPPATH Construction变量
现在假设"Hello,World!"程序有一个#include行需要包含hello.h头文件:

include

int main()
{
printf("Hello, %s!\n",string);
}
并且,hello.h文件如下:

define string "world"

在这种情况下,我们希望SCons能够认识到,如果hello.h文件的内容发生改变,那么hello程序必须重新编译。我们需要修改SConstruct文件如下:
Program('hello.c', CPPPATH='.')
CPPPATH告诉SCons去当前目录('.')查看那些被C源文件(.c或.h文件)包含的文件。 % scons -Q hello cc -o hello.o -c -I. hello.c cc -o hello hello.o % scons -Q hello scons: `hello' is up to date. % edit hello.h [CHANGE THE CONTENTS OF hello.h] % scons -Q hello cc -o hello.o -c -I. hello.c cc -o hello hello.o 首先注意到,SCons根据CPPPATH变量增加了-I.参数,使得编译器在当前目录查找hello.h文件。
其次,SCons知道hello程序需要重新编译,因为它扫描了hello.c文件的内容,知道hello.h文件被包含。SCons将这些记录为目标文件的隐式依赖,当hello.h文件改变的时候,SCons就会重新编译hello程序。
就像LIBPATH变量,CPPPATH也可能是一个目录列表,或者一个被系统特定路径分隔符分隔的字符串。
Program('hello.c', CPPPATH=['include', '/home/project/inc'])

4、缓存隐式依赖
扫描每个文件的#include行会消耗额外的处理时间。
SCons让你可以缓存它扫描找到的隐式依赖,在以后的编译中可直接使用。这需要在命令行中指定--implicit-cache选项:
% scons -Q --implicit-cache hello
如果你不想每次在命令行中指定--implicit-cache选项,你可以在SConscript文件中设置implicit-cache选项使其成为默认的行为:
SetOption('implicit-cache', 1)
SCons默认情况下不缓存隐式依赖,因为--implicit-cache使得SCons在最后运行的时候,只是简单的使用已经存储的隐式依赖,而不会检查那些依赖还是不是仍然正确。在如下的情况中,--implicit-cache可能使得SCons的重新编译不正确:
1>当--implicit-cache被使用,SCons将会忽略CPPPATH或LIBPATH中发生的一些变化。如果$CPPPATH的一个改变,使得不同目录下的内容不相同但文件名相同的文件被使用,SCons也不会重新编译。
2>当--implicit-cache被使用,如果一个同名文件被添加到一个目录,这个目录在搜索路径中的位置在同名文件上次被找到所在的目录之前,SCons将侦测不到。

4.1、--implicit-deps-changed选项
当使用缓存隐式依赖的时候,有些时候你想让SCons重新扫描它之前缓存的依赖。你可以运行--implicit-deps-changed选项:
% scons -Q --implicit-deps-changed hello

4.2、--implicit-deps-unchanged选项
默认情况下在使用缓存隐式依赖的时候,SCons会注意到当一个文件已经被修改的时候,就会重新扫描文件更新隐式依赖信息。有些时候,你可能想即使源文件改变了,但仍然让SCons使用缓存的隐式依赖。你可以使用--implicit-deps-unchanged选项:
% scons -Q --implicit-deps-unchanged hello

5、显示依赖:Depends函数
有些时候一个文件依赖另一个文件,是不会被SCons扫描器侦测到的。对于这种情况,SCons允许你显示指定一个文件依赖另一个文件,并且无论何时被依赖文件改变的时候,需要重新编译。这个需要使用Depends方法:
hello=Program("hello.c")
Depends(hello,'other_file')
注意Depends方法的第二个参数也可以是一个节点对象列表:
hello=Program('hello.c')
goodbye=Program('goodbye.c')
Depends(hello,goodbye)
在这种情况下,被依赖的对象会在目标对象之前编译:
% scons -Q hello
cc -c goodbye.c -o goodbye.o
cc -o goodbye goodbye.o
cc -c hello.c -o hello.o
cc -o hello hello.o

6、来自外部文件的依赖:ParseDepends函数
SCons针对许多语言,有内置的扫描器。有些时候,由于扫描器实现的缺陷,扫描器不能提取出某些隐式依赖。
下面的例子说明了内置的C扫描器不能提取一个头文件的隐式依赖:

define FOO_HEADER

include FOO_HEADER

int main()
{
return FOO;
}
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit foo.h
% scons -Q
scons: '.' is up to date.
显然,扫描器没有发现头文件的依赖。这个扫描器不是一个完备的C预处理器,没有扩展宏。
在这种情况下,你可能想使用编译器提取隐式依赖。ParseDepends可以解析编译器输出的内容,然后显示建立所有的依赖。
下面的例子使用ParseDepends处理一个编译器产生的依赖文件,这个依赖文件是在编译目标文件的时候作为副作用产生的:
obj=Object('hello.c', CCFLAGS='-MD -MF hello.d', CPPPATH='.')
SideEffect('hello.d',obj)
ParseDepends('hello.d')
Program('hello', obj)
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% edit foo.h
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
从一个编译器产生的.d文件解析依赖有一个先有鸡还是先有蛋的问题,会引发不必要的重新编译:
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding hello.o' becausefoo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.
第一次运行的时候,在编译目标文件的时候,依赖文件产生了。在那个时候,SCons不知道foo.h的依赖。第二次运行的时候,目标文件被重新生成因为foo.h被发现是一个新的依赖。
ParseDepends在调用的时候立即读取指定的文件,如果文件不存在马上返回。在编译过程中产生的依赖文件不会被再次自动解析。因此,在同样的编译过程中,编译器提取的依赖不会被存储到签名数据库中。这个ParseDepends的缺陷导致不必要的重新编译。因此,仅仅在扫描器对于某种语言不可用或针对特定的任务不够强大的情况下,才使用ParseDepends。

7、忽略依赖:Ignore函数
有些时候,即使一个依赖的文件改变了,也不想要重新编译。在这种情况下,你需要告诉SCons忽略依赖,如下所示:
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore(hello_obj, 'hello.h')

% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: hello' is up to date. % edit hello.h [CHANGE THE CONTENTS OF hello.h] % scons -Q hello scons:hello' is up to date.
上面的例子是人为做作的,因为在真实情况下,如果hello.h文件改变了,你不可能不想重新编译hello程序。一个更真实的例子可能是,如果hello程序在一个目录下被编译,这个目录在多个系统中共享,多个系统有不同的stdio.h的拷贝。在这种情况下,SCons将会注意到不同系统的stdio.h拷贝的不同,当你每次改变系统的时候,重新编译hello。你可以避免这些重新编译,如下所示:
hello=Program('hello.c', CPPPATH=['/usr/include'])
Ignore(hello, '/usr/include/stdio.h')
Ignore也可以用来阻止在默认编译情况下文件的产生。这是因为目录依赖它们的内容。所以为了忽略默认编译时产生的文件,你指定这个目录忽略产生的文件。注意到如果用户在scons命令行中请求目标程序,或者这个文件是默认编译情况下另一个文件的依赖,那么这个文件仍然会被编译。
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore('.',[hello,hello_obj])

% scons -Q
scons: .' is up to date. % scons -Q hello cc -o hello.o -c hello.c cc -o hello hello.o % scons -Q hello scons:hello' is up to date.

8、顺序依赖:Requires函数
有时候,需要指定某一个文件或目录必须在某些目标程序被编译之前被编译或创建,但是那个文件或目录如果发生了改变,那个目标程序不需要重新编译。这样一种关系叫做顺序依赖(order-only dependency)因为它仅仅影响事物编译的顺序,它不是一种严格意义上的依赖关系,因为目标程序不需要随着依赖文件的改变而改变。
例如,你想在每次编译的时候创建一个文件用来标识编译执行的时间,版本号等等信息。这个版本文件的内容在每次编译的时候都会改变。如果你指定一个正常的依赖关系,那么每个依赖这个文件的程序在你每次运行SCons的时候都会重新编译。例如,我们可以使用一些Python代码在SConstruct文件中创建一个新的version.c文件,version.c文件会记录我们每次运行SCons的当前日期,然后链接到一个程序:
import time
version_c_text="""
char *date="%s"
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)
hello=Program(['hello.c', 'version.c'])
如果我们将version.c作为一个实际的源文件,那么version.o文件在我们每次运行SCons的时候都会重新编译,并且hello可执行程序每次会重新链接。
% scons -Q hello
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
我们的解决方案是使用Requires函数指定version.o在链接之前必须重新编译,但是version.o的改变不需要引发hello可执行程序重新链接:
import time

version_c_text = """
char *date = "%s";
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)

version_obj = Object('version.c')

hello = Program('hello.c',
LINKFLAGS = str(version_obj[0]))

Requires(hello, version_obj)
注意到因为我们不再将version.c作为hello程序的源文件,我们必须找到其他的方式使其可以链接。在这个例子中,我们将对象文件名放到LINKFLAGS变量中,因为LINKFLAGS已经包含在$LINKCOM命令行中了。
通过这些改变,当hello.c改变的时候,hello可执行程序才重新链接:
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: hello' is up to date. % sleep 1 % edit hello.c [CHANGE THE CONTENTS OF hello.c] % scons -Q hello cc -o version.o -c version.c cc -o hello.o -c hello.c cc -o hello version.o hello.o % sleep 1 % scons -Q hello cc -o version.o -c version.c scons:hello' is up to date.

9、AlwaysBuild函数
当一个文件传递给AlwaysBuild方法时,
hello=Program('hello.c')
AlwaysBuild(hello)
那么指定的目标文件将总是被认为是过时的,并且被重新编译:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o
AlwaysBuild函数并不意味着每次SCons被调用的时候,目标文件会被重新编译。在命令行中指定某个其他的目标,这个目标自身不依赖AlwaysBuild的目标程序,这个目标程序仅仅当它的依赖改变的时候才会重新编译:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello.o
scons: `hello.o' is up to date.

一个环境就是能够影响一个程序如何执行的值的集合。SCons里面有三种不同类型的环境:

External Environment(外部环境):
外部环境指的是在用户运行SCons的时候,用户环境中的变量的集合。这些变量在SConscript文件中通过Python的os.environ字典可以获得。

Construction Environment(构造环境):
一个构造环境是在一个SConscript文件中创建的一个唯一的对象,这个对象包含了一些值可以影响SCons编译一个目标的时候做什么动作,以及决定从那一个源中编译出目标文件。SCons一个强大的功能就是可以创建多个构造环境,包括从一个存在的构造环境中克隆一个新的自定义的构造环境。

Execution Environment(执行环境):
一个执行环境是SCons在执行一个外部命令编译一个或多个目标文件时设置的一些值。这和外部环境是不同的。
与Make不同,SCons不会自动在不同的环境之间拷贝或导入值。这是一个刻意的设计选择,保证了不管用户外部环境的值是怎么样的,编译总是可以重复的。这会避免编译中的一些问题,比如因为一个自定义的设置使得使用了一个不同的编译器或编译选项,开发者的本地代码编译成功,但是checked-in后编译不成功,因为使用了不同的环境变量设置。

7.1、使用来自外部环境的值
当执行SCons的时候,外部环境的值通过os.environ字典获得。这就以为着在任何一个你想使用外部环境的SConscript文件需要增加一个import os语句:
import os

7.2、构造环境
在一个大型复杂的系统中,所有的软件都按照同样的方式编译是比较少见的。例如,不同的源文件可能需要不同的编译选项,或者不同的可执行程序需要链接不同的库。SCons允许你创建和配置多个构造环境来控制和满足不同的编译需求。

7.2.1、创建一个构造环境:Environment函数
一个构造环境由Environment方法创建:
env=Environment()
默认情况下,SCons基于你系统中工具的一个变量集合来初始化每一个新的构造环境。
当你初始化一个构造环境时,你可以设置环境的构造变量来控制一个是如何编译的。例如:
import os
env=Environment(CC='gcc', CCFLAGS='-O2')
env.Program('foo.c')

7.2.2、从一个构造环境中获取值
你可以使用访问Python字典的方法获取单个的构造变量:
env=Environment()
print "CC is:", env['CC']
一个构造环境实际上是一个拥有方法的对象。如果你想直接访问构造变量的字典,你可以使用Dictionary方法:
env=Environment(FOO='foo', BAR='bar')
dict=env.Dictionary()
for key in ['OBJSUFFIX', 'LIBSUFFIX', 'PROGSUFFIX']:
print "key=%s, value=%s" % (key,dict[key])
如果你想循环并打印出构造环境中的所有变量:
env=Environment()
for item in sorted(env.Dictionary().items()):
print "construction variable = '%s', value = '%s'" % item

7.2.3、从一个构造环境中扩展值:subst方法
另一种从构造环境中获取信息的方式是使用subst方法。例如:
env=Environment()
print "CC is:", env.subst('CC') 使用subst展开字符串的优势是结果中的构造变量会重新展开直到不能扩展为止。比如获取CCCOM:
env=Environment(CCFLAGS='-DFOO')
print "CCCOM is:", env['CCCOM']
将会打印出没有扩展开的CCCOM: % scons -Q CCCOM is:CC CCFLAGSCPPFLAGS _CPPDEFFLAGS_CPPINCFLAGS -c -o TARGETSOURCES
scons: '.' is up to date.
调用subst方法来获取CCCOM: env=Environment(CCFLAGS='-DFOO') print "CCCOM is:", env.subst('CCCOM')
将会递归地扩展所有的构造变量:
% scons -Q
CCCOM is: gcc -DFOO -c -o
scons: '.' is up to date.

7.2.4、处理值扩展的问题
如果扩展一个构造变量的时候,发生了问题,默认情况下会扩展成''空字符串,不会引起scons失败。
env=Environment()
print "value is:", env.subst('->$MISSING<-')

% scons -Q
value is: -><-
scons: '.' is up to date.
使用AllowSubstException函数会改变默认的行为。当值扩展的时候发生了异常,AllowSubstExceptions控制了哪一个异常是致命的,哪一个允许安全地发生。默认情况下,NameError和IndexError这两个异常允许发生。如果需要所有的构造变量名字存在,调用AllowSubstExceptions:
AllowSubstExceptions()
env=Environment()
print "value is:", env.subst('->$MISSING<-')

% scons -Q
value is:
scons: *** NameError MISSING' trying to evaluateMISSING' File "/home/my/project/SConstruct", line 3, in 也可以用来允许其他的异常发生,使用{...}构造变量语法。例如,下面的代码允许除零发生:
AllowSubstExceptions(IndexError, NameError, ZeroDivisionError)
env = Environment()
print "value is:", env.subst( '->${1 / 0}<-' )

% scons -Q
value is: -><-
scons: `.' is up to date.

7.2.5、控制默认的构造环境:DefaultEnvironment函数
我们已经介绍过的所有的Builder,比如Program和Library,实际上使用一个默认的构造环境。
你可以控制默认构造环境的设置,使用DefaultEnvironment函数:
DefaultEnvironment(CC='/usr/local/bin/gcc')
这样配置以后,所有Program或者Object的调用都将使用/usr/local/bin/gcc编译目标文件。
注意到DefaultEnvironment返回初始化了的默认构造环境对象,这个对象可以像其他构造环境一样被操作。所以如下的代码和上面的例子是等价的:
env=DefaultEnvironment()
env['CC']='/usr/local/bin/gcc'
DefaultEnvironment函数常用的一点就是用来加速SCons的初始化。为了使得大多数默认的配置能够工作,SCons将会搜索本地系统已经安装的编译器和其他工具。这个搜索过程会花费时间。如果你知道哪一个编译器或工具你想配置,你可以控制SCons执行的搜索过程通过指定一些特定的工具模块来初始化默认的构造环境:
env=DefaultEnvironment(tools=['gcc','gnulink'], CC='/usr/local/bin/gcc')
上面的例子告诉SCons显示配置默认的环境使用GNU编译器和GNU链接器设置,使用/usr/local/bin/gcc编译器。

7.2.6、多个构造环境
构造环境的真正优势是你可以创建你所需要的许多不同的构造环境,每一个构造环境对应了一种不同的方式去编译软件的一部分或其他文件。比如,如果我们需要用-O2编译一个程序,编译另一个用-g,我们可以如下做:
opt=Environment(CCFLAGS='-O2')
dbg=Environment(CCFLAGS='-g')
opt.Program('foo','foo.c')
dbg.Program('bar','bar.c')

% scons -Q
cc -o bar.o -c -g bar.c
cc -o bar bar.o
cc -o foo.o -c -O2 foo.c
cc -o foo foo.o
我们甚至可以使用多个构造环境去编译一个程序的多个版本:
opt=Environment(CCFLAGS='-O2')
dbg=Environment(CCFLAGS='-g')
opt.Program('foo','foo.c')
dbg.Program('foo','foo.c')
这个时候SCons会发生错误:
% scons -Q
scons: *** Two environments with different actions were specified for the same target: foo.o
File "/home/my/project/SConstruct", line 6, in
这是因为这两个Program调用都隐式地告诉SCons产生一个叫做foo.o的目标文件。SCons无法决定它们的优先级,所以报错了。为了解决这个问题,我们应该显示指定每个环境将foo.c编译成不同名字的目标文件:
opt=Environment(CCFLAGS='-O2')
dbg=Environment(CCFLAGS='-g')
o=opt.Object('foo-opt','foo.c')
opt.Program(o)
d=dbg.Object('foo-dbg','foo.c')
dbg.Program(d)

7.2.7、拷贝构造环境:Clone方法
有时候你想多于一个构造环境对于一个或多个变量共享相同的值。当你创建每一个构造环境的时候,不是重复设置所有共用的变量,你可以使用Clone方法创建一个构造环境的拷贝。
Environment调用创建一个构造环境,Clone方法通过构造变量赋值,重载拷贝构造环境的值。例如,假设我们想使用gcc创建一个程序的三个版本,一个优化版,一个调试版,一个其他版本。我们可以创建一个基础构造环境设置$CC为gcc,然后创建两个拷贝:
env=Environment(CC='gcc')
opt=env.Clone(CCFLAGS='-O2')
dbg=env.Clone(CCFLAGS='-g')
env.Program('foo','foo.c')
o=opt.Object('foo-opt','foo.c')
opt.Program(o)
d=dbg.Object('foo-dbg','foo.c')
dbg.Program(d)

7.2.8、替换值:Replace方法
你可以使用Replace方法替换已经存在的构造变量:
env=Environment(CCFLAGS='-DDEFINE1');
env.Replace(CCFLAGS='-DDEFINE2');
env.Program('foo.c')
你可以安全地针对那些不存在的构造变量调用Replace方法:
env=Environment()
env.Replace(NEW_VARIABLE='xyzzy')
print "NEW_VARIABLE = ", env['NEW_VARIABLE']
在这个例子中,构造变量被添加到构造环境中去了:
%scons -Q
NEW_VARIABLE = xyzzy
scons: '.' is up to date.
变量不会被扩展知道构造环境真正被用来编译目标文件的时候,同时SCons函数和方法的调用是没有顺序的,最后的替换可能被用来编译所有的目标文件,而不管Replace方法的调用是在编译方法的前后:
env=Environment(CCFLAGS='-DDEFINE1')
print "CCFLAGS = ", env['CCFLAGS']
env.Program("foo.c")
env.Replace(CCFLAGS='-DDEFIN2')
print "CCFLAGS = ", env['CCFLAGS']
env.Program("bar.c")

% scons
scons: Reading SConscript files ...
CCFLAGS = -DDEFINE1
CCFLAGS = -DDEFINE2
scons: done reading SConscript files.
scons: Building targets ...
cc -o bar.o -c -DDEFINE2 bar.c
cc -o bar bar.o
cc -o foo.o -c -DDEFINE2 foo.c
cc -o foo foo.o
scons: done building targets.
因为替换发生在读取SConscript文件的时候,foo.o编译的时候$CCFLAGS变量已经被设置为-DDEFINE2,即使Relapce方法是在SConscript文件后面被调用的。

7.2.9、在没有定义的时候设置值:SetDefault方法
有时候一个构造变量应该被设置为一个值仅仅在构造环境没有定义这个变量的情况下。你可以使用SetDefault方法,这有点类似于Python字典的set_default方法:
env.SetDefault(SPECIAL_FLAG='-extra-option')
当你编写你自己的Tool模块将变量应用到构造环境中的时候非常有用。

7.2.10、追加到值的末尾:Append方法
你可以追加一个值到一个已经存在的构造变量,使用Append方法:
env=Environment(CCFLAGS=['-DMY_VALUE'])
env.Append(CCFLAGS=['-DLAST'])
env.Program('foo.c')
Scons编译目标文件的时候会应用-DMY_VALUE和-DLAST两个标志:
% scons -Q
cc -o foo.o -c -DMY_VALUE -DLAST foo.c
cc -o foo foo.o
如果构造变量不存在,Append方法将会创建它。

7.2.11、追加唯一的值:AppendUnique方法
有时候仅仅只有在已经存在的构造变量没有包含某个值的时候,才会增加这个新值。可以使用AppendUnique方法:
env.AppendUnique(CCFLAGS=['-g'])
上面的例子,仅仅只有在$CCFLAGS没有包含-g值得时候才会增加-g。

7.2.12、在值的开始位置追加值:Prepend方法
对于一个存在的构造变量,你可以使用Prepend方法追加一个值到它的值的开始位置。
env=Environment(CCFLAGS=['-DMY_VALUE'])
env.Prepend(CCFLAGS=['-DFIRST'])
env.Program('foo.c')

% scons -Q
cc -o foo.o -c -DFIRST -DMY_VALUE foo.c
cc -o foo foo.o
如果构造变量不存在,Prepend方法会创建它。

7.2.13、在前面追加唯一值:PrependUnique方法
仅仅在一个构造变量存在的值中没有包含将要增加的值的时候,这个值才被追加到前面,可以使用PrependUnique方法;
env.PrependUnique(CCFLAGS=['-g'])

7.3、控制命令的执行环境
当SCons编译一个目标文件的时候,它不会使用你用来执行SCons的同样的外部环境来执行一些命令。它会使用ENV构造变量作为外部环境来执行命令。 这个行为最重要的体现就是PATH环境变量,它决定了操作系统将去哪里查找命令和工具,与你调用SCons使用的外部环境的不一样。这就意味着SCons将不能找到你在命令行里执行的所有工具。 PATH环境变量的默认值是/usr/local/bin:/bin:/usr/bin。如果你想执行任何命令不在这些默认地方,你需要在你的构造环境中的ENV字典中设置PATH。
最简单的方式就是当你创建构造环境的时候初始化这些值:
path=['/usr/local/bin', '/usr/bin']
env=Environment(ENV={'PATH':PATH})
以这种方式将一个字典赋值给ENV构造变量完全重置了外部环境,所以当外部命令执行的时候,设置的变量仅仅是PATH的值。如果你想使用ENV中其余的值,仅仅只是设置PATH的值,你可以这样做:
env['ENV']['PATH']=['/usr/local/bin','/bin','/usr/bin']
注意SCons允许你用一个字符串定义PATH中的目录,路径用路径分隔符分隔:
env['ENV']['PATH']='/usr/local/bin:/bin:/usr/bin'

7.3.1、从外部环境获得PATH值
你可能想获得外部的PATH来作为命令的执行环境。你可以使用来自os.environ的PATH值来初始化PATH变量:
import os
env=Environment(ENV={'PATH':os.environ['PATH']})
你设置可以设置整个的外部环境:
import os
env=Environment(ENV=os.environ)

7.3.2、在执行环境里增加PATH的值
常见的一个需求就是增加一个或多个自定义的目录到PATH变量中。
env=Environment(ENV=os.environ)
env.PrependENVPath('PATH','/usr/local/bin')
env.AppendENVPath('LIB','/usr/local/lib')

1、将选项合并到环境中:MergeFlags函数

SCons的construction环境有一个MergeFlags方法,此方法将一个值的字典合并到construction环境中。MergeFlags将字典中的每个值看做一个选项列表。如果一个选项已经在construction环境变量中存在了,MergeFlags将不会重复设置这个选项。

当合并选项到任何一个名字在PATH中的变量的时候,MergeFlags保持选项在最左端出现,应为目录路径列表中,第一个出现占主要地位。当合并选项到任何其他变量名的时候,MergeFlags保持选项在右端出现,因为在命令行选项列表中,最后出现的占主要地位。

env = Environment()

env.Append(CCFLAGS = '-option -O3 -O1')

flags = { 'CCFLAGS' : '-whatever -O3' }

env.MergeFlags(flags)

print env['CCFLAGS']

% scons -Q

['-option', '-O1', '-whatever', '-O3']

scons: `.' is up to date.

上面例子中$CCFLAGS的默认值是一个内部SCons对象,会自动将我们指定的选项转换成一个字符串加入到列表中。

env = Environment()

env.Append(CPPPATH = ['/include', '/usr/local/include', '/usr/include'])

flags = { 'CPPPATH' : ['/usr/opt/include', '/usr/local/include'] }

env.MergeFlags(flags)

print env['CPPPATH']

% scons -Q

['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']

scons: `.' is up to date.

上面例子中$CPPPATH的默认值是一个Python列表,所以我们必须在传递给MergeFlags函数的字典中将值指定为一个列表。

如果不是传递一个字典而是其他的东西,会调用ParseFlags方法将其转换为一个字典:

env = Environment()

env.Append(CCFLAGS = '-option -O3 -O1')

env.Append(CPPPATH = ['/include', '/usr/local/include', '/usr/include'])

env.MergeFlags('-whatever -I/usr/opt/include -O3 -I/usr/local/include')

print env['CCFLAGS']

print env['CPPPATH']

% scons -Q

['-option', '-O1', '-whatever', '-O3']

['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']

scons: `.' is up to date.

在上面的例子中,ParseFlags方法已经将选项排序赋值给相对应的变量,然后返回一个字典给MergeFlags方法。

2、将编译参数分离成变量:ParseFlags函数

当编译程序的时候,对于不同类型的选项,SCons有一个令人迷惑的construction变量数组。对于一个特殊的选项,有时候你可能不知道该使用哪一个变量。

SCons construction环境有一个ParseFlags方法,该方法接受一个命令行选项集合,然后将他们分发给合适的construction变量。

ParseFlags返回一个包含了construction变量和值的字典。正常情况下,这个字典将会传递给MergeFlags方法,将选项合并到construction环境中去,但是如果想要提供额外的功能,这个字典可以被编辑。

env = Environment()

d = env.ParseFlags("-I/opt/include -L/opt/lib -lfoo")

for k,v in sorted(d.items()):

if v:

print k, v

env.MergeFlags(d)

env.Program('f1.c')

% scons -Q

CPPPATH ['/opt/include']

LIBPATH ['/opt/lib']

LIBS ['foo']

cc -o f1.o -c -I/opt/include f1.c

cc -o f1 f1.o -L/opt/lib -lfoo

因为假设flags是用于GCC的,那么不认别的flags会被放置到$CCFLAGS中,用于编译C和C++:

env = Environment()

d = env.ParseFlags("-whatever")

for k,v in sorted(d.items()):

if v:

print k, v

env.MergeFlags(d)

env.Program('f1.c')

% scons -Q

CCFLAGS -whatever

cc -o f1.o -c -whatever f1.c

cc -o f1 f1.o

ParseFlags也接受一个字符串列表作为输入:

env = Environment()

d = env.ParseFlags(["-I/opt/include", ["-L/opt/lib", "-lfoo"]])

for k,v in sorted(d.items()):

if v:

print k, v

env.MergeFlags(d)

env.Program('f1.c')

% scons -Q

CPPPATH ['/opt/include']

LIBPATH ['/opt/lib']

LIBS ['foo']

cc -o f1.o -c -I/opt/include f1.c

cc -o f1 f1.o -L/opt/lib -lfoo

如果一个字符串以!开始,这个字符串将被传递给shell执行。命令的输出然后被解析:

env = Environment()

d = env.ParseFlags(["!echo -I/opt/include", "!echo -L/opt/lib", "-lfoo"])

for k,v in sorted(d.items()):

if v:

print k, v

env.MergeFlags(d)

env.Program('f1.c')

% scons -Q

CPPPATH ['/opt/include']

LIBPATH ['/opt/lib']

LIBS ['foo']

cc -o f1.o -c -I/opt/include f1.c

cc -o f1 f1.o -L/opt/lib -lfoo

ParseFlags会定期更新新的选项。

3、查找已经安装的库信息:ParseConfig函数

配置正确的选项来编译引用了其他库的程序,尤其引用的是共享库,是非常复杂的。为了帮助解决这种情况,许多名字以config结尾的工具返回需要使用这些库的命令行选项;例如,使用一个名字为lib的库的命令行选项将会被叫做lib-config的工具找到。

最近的一个惯例是这些选项通过通用的pkg-config工具是可用的,此程序有通用的框架,错误处理,所以所有的包创建者需要做的就是为它的特殊的包提供字符串集合。

SCons construction环境有一个ParseConfig方法,此方法会执行*config工具并且基于特定命令返回的命令行选项配置合适的construction变量。

env = Environment()

env['CPPPATH'] = ['/lib/compat']

env.ParseConfig("pkg-config x11 --cflags --libs")

print env['CPPPATH']

SCons将会执行特定的命令字符串,解析结果flags,然后将flags增加到合适的环境变量:

% scons -Q

['/lib/compat', '/usr/X11/include']

scons: `.' is up to date.

上面的例子中,SCons增加include目录到CPPPATH。

注意到,使用MergeFlags方法将选项同存在的选项合并,每一个选项在construction变量中仅仅出现一次:

env = Environment()

env.ParseConfig("pkg-config x11 --cflags --libs")

env.ParseConfig("pkg-config x11 --cflags --libs")

print env['CPPPATH']

% scons -Q

['/usr/X11/include']

scons: `.' is up to date.

日记本
Gupao