1. 基本概念
SConstruct 是一个 Python 脚本,作用类似于 Makefile,我们通过它来告诉 Scons 要构建什么东西
2. 常用命令
1 2 3 4 5 6
| scons
scons -c
scons -Q
|
3. 简单构建
单个源文件
1 2 3 4 5 6 7 8
| Program('hello.c')
Program('my_hello', 'hello.c')
Object('hello.c')
|
多个源文件
1 2 3 4 5 6 7 8 9 10
| Program(['prog.c', 'file1.c', 'file2.c'])
Program('my_prog', ['prog.c', 'file1.c', 'file2.c'])
Program('prog', Glob('*.c'))
|
事实上,源代码文件在 Scons 内部都是当作列表来处理,单个文件的情况,会自动添加方括号而已;为了统一,建议还是都加上方括号比较好,同时也可以避免 Python 解释器报语法错误
自动加引号
当有多个文件时,需要为每个文件打上双引号,如果文件一多,确实工作量不小;Scons 额外提供了一个 Split 名称,可以为文件自动添加引号,使用方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| Program('prog', Split('main.c file1.c file2.c'))
src_files = Split('main.c file1.c file2.c') Program('program', src_files)
src_files = Split("""main.c file1.c file2.c""") Program('program', src_files)
|
指定参数名
Program 支持指定参数名
1 2
| src_files = Split('main.c file1.c file2.c') Program(target = 'program', source = src_files)
|
多个编译目标
如果想在同一个 scons 文件中编译多个可执行文件,只需多次调用 Program 函数即可
1 2
| Program('foo.c') Program('bar', ['bar1.c', 'bar2.c'])
|
多目标共享源文件
办法1:只需要将共享的文件放入源文件列表即可
1 2 3 4 5 6 7 8 9 10
| 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)
|
办法2:将共享的文件做为库,由不同的目标进行引号
4. 构建和链接库
构建库
1 2 3 4 5 6 7 8 9 10 11
| Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])
StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
|
链接库
1 2 3 4 5 6 7 8 9 10
|
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')
|
查找库
1 2 3 4 5 6 7
|
Program('prog.c', LIBS = 'm', LIBPATH = ['/usr/lib', '/usr/local/lib'])
LIBPATH = '/usr/lib:/usr/local/lib'
|
5. 节点对象
在内部实现上,Scons 将所有的文件和文件夹都当作一个 NodeObject 节点对象来对待;
构建方法的返回值是节点列表
所有的构建方法都会返回一个节点列表,用来表示将要构建的目标文件或者参与构建的源文件,返回的这个列表可以作为参数,传递给其他构建方法;
1 2 3 4
| hello_list = Object('hello.c', CCFLAGS='-DHELLO') goodbye_list = Object('goodbye.c', CCFLAGS='-DGOODBYE') Program(hello_list + goodbye_list)
|
显式创建文件和目录的节点
1 2 3 4 5 6 7
|
hello_c = File('hello.c') Program(hello_c)
classes = Dir('classes') Java(classes, 'src')
|
正常情况下,并不需要手动创建节点,因为构建方法会自动帮助创建;仅在需要显式传递节点参数给构建方法时使用;
打印节点的文件名称
由于构建方法返回的是一个节点列表,因此如果要打印文件名称,很可能需要遍历它,或者使用索引访问它
1 2 3 4 5
| 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]
|
获取节点文件名
使用 Python 内置的 str 函数即可方便的将一个节点转成一个文件名,例如可以用来判断一个文件是否存在
1 2 3 4 5
| 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!"
|
获取节点路径
env 对象有一个 GetBuildPath方法,可以用来获取单个或多个节点的路径
1 2 3 4 5 6
| env=Environment(VAR="value")
n=File("foo.c")
print env.GetBuildPath([n, "sub/dir/$VAR"])
|
1 2
| # 打印结果为 ['foo.c', 'sub/dir/value']
|
除了使用 env 对象的 GetBuildPath 方法外,也有一个函数版本的 GetBuildPath ,它使用全局环境;
6. 依赖出现更新
判断文件是否更新
如果源文件的内容没有出现更新,是 Scons 不会重复构建已经完成构建的文件,这样可以节省大量的构建时间,不需要每次都从头开始构建每一文件;
使用 MD5 判断
SCons 使用 MD5 来判断某个文件的内容是否发生了更新,当然,也可以另外配置让其使用文件时间戳来判断,甚至可以使用单独的 python 函数来进行各种自定义的判断;
使用 MD5 有一个好处是它只判断内容中的正文部分,同时忽略注释部分,即只要正文内容的构建结果不会出现变化,则 SCons 就不会重现构建它;
使用时间戳判断
如果想使用时间戳来判断文件是否发生更新,则只需要调用 Decider 函数进行设置即可
1 2 3
| Object('hello.c')
Decider('timestamp-newer')
|
普通的时间戳存在一个问题,即某个文件如果从仓库签出了一个旧版本,由于它的时间戳比当前的目标文件更早,所以不会判断为出现更新,导致编译错误;针对这种情况,可以使用 timestamp-match 规则来进行判断
1 2 3
| Object('hello.c')
Decider('timestamp-match')
|
使用混合规则
仅当文件的时间戳出现了变化,再去计算文件的 MD5 值是否发生了变化,这样性能更好;
1 2 3
| Program('hello.c')
Decider('MD5-timestamp')
|
自定义规则
可以自己写一个判断规则的函数,然后传递给 Decider 即可
1 2 3 4 5 6 7 8 9
| 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)
|