scons 用法

1. 基本概念

SConstruct 是一个 Python 脚本,作用类似于 Makefile,我们通过它来告诉 Scons 要构建什么东西

2. 常用命令

1
2
3
4
5
6
# 表示读取 SConstruct 脚本文件,开始构建
scons
# -c 选项表示清理现场,将构建出来的东西删除掉
scons -c
# -Q 选项表示安静模式,即不打印构建过程中的提示信息
scons -Q

3. 简单构建

单个源文件

1
2
3
4
5
6
7
8
# Program 用来告知 Scons 要构建可执行文件,参数即是源代码
Program('hello.c')

# 如果要指代可执行文件的名字,可写在第一个参数上
Program('my_hello', 'hello.c')

# Object 表示要构建目标文件
Object('hello.c')

多个源文件

1
2
3
4
5
6
7
8
9
10
# 如果源代码有多个文件,只需将它们放在一个列表中即可
Program(['prog.c', 'file1.c', 'file2.c'])

# Program 默认以列表第一个元素为最终编译结果的可执行文件的文件名,但是可以自定义
Program('my_prog', ['prog.c', 'file1.c', 'file2.c'])

# 如果文件很多个,可以使用关键字和 Glob 命令进行匹配
# 通配符包括星号 *,问号 ?,以及部分关键字如 [abc] 表示任意满足 a, b 或 c 的文件均可
# 也可以使用 [!abc] 进行排除,即不包括以上几个字母的,即是目标
Program('prog', Glob('*.c'))

事实上,源代码文件在 Scons 内部都是当作列表来处理,单个文件的情况,会自动添加方括号而已;为了统一,建议还是都加上方括号比较好,同时也可以避免 Python 解释器报语法错误

自动加引号

当有多个文件时,需要为每个文件打上双引号,如果文件一多,确实工作量不小;Scons 额外提供了一个 Split 名称,可以为文件自动添加引号,使用方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用 Split 函数自动添加引号,文件名之间只需要使用空格分隔即可
Program('prog', Split('main.c file1.c file2.c'))

# 也可以用一个变量来代表多个文件的列表
src_files = Split('main.c file1.c file2.c')
Program('program', src_files)

# 文件名之间有多个空格也没有关系
# 此处用三个引号是为了符合 Python 对多行字符串的格式要求
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
# 此处  common1.c 和 common2.c 这两个文件是共享的
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 函数即可构建库
Library('foo', ['f1.c', 'f2.c', 'f3.c'])

# 除了指定源文件外,也可以在文件列表中加入目标文件
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])

# Library 函数默认构建静态库,同时还可以使用 StaticLibrary 函数显示的指示要构建静态库
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
# 通过在 LIBS 参数中指定库的名称,并在 LIBPATH 指定库的路径,即可完成对库的链接

# 此处先构建一个静态库
Library('foo', ['f1.c', 'f2.c', 'f3.c'])

# 此处告知要链接的静态库名称和路径
# 注意:只需要提供库的名称即可,无须在库的名称前面加上 lib 前缀,或者 .a 后缀什么的
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')


查找库

1
2
3
4
5
6
7
# 通常情况下,链接器只在系统默认的文件中查找库,但是通过 LIBPATH 参数
# 链接器会在指定的文件夹中查找库
Program('prog.c', LIBS = 'm',
LIBPATH = ['/usr/lib', '/usr/local/lib'])

# 不同的路径可以使用逗号分隔,组成列表;也可以使用冒号连接成单个字符串
LIBPATH = '/usr/lib:/usr/local/lib'

5. 节点对象

在内部实现上,Scons 将所有的文件和文件夹都当作一个 NodeObject 节点对象来对待;

构建方法的返回值是节点列表

所有的构建方法都会返回一个节点列表,用来表示将要构建的目标文件或者参与构建的源文件,返回的这个列表可以作为参数,传递给其他构建方法;

1
2
3
4
# 假设我们需要为两个目标文件指定不同的构建参数,因此我们为它们调用了各自的 Object 方法
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
# 创建文件节点和创始目录节点的方法不同
# 创建文件节点使用 File 方法
hello_c = File('hello.c')
Program(hello_c)
# 创建目录节点使用 Dir 方法
classes = Dir('classes')
Java(classes, 'src')

正常情况下,并不需要手动创建节点,因为构建方法会自动帮助创建;仅在需要显式传递节点参数给构建方法时使用;

1
2
# Entry 函数可以根据参数类型,创建文件节点或者目录节点
xyzzy = Entry('xyzzy')

打印节点的文件名称

由于构建方法返回的是一个节点列表,因此如果要打印文件名称,很可能需要遍历它,或者使用索引访问它

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]
# 事实上此处的 object_list 是节点列表,仅仅是 print 函数将节点转成了字符串来代表文件名

获取节点文件名

使用 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 对象,它代表一个环境,在这个环境中,有一个环境变量的 VAR 的值为 value
env=Environment(VAR="value")
# 生成一个文件节点
n=File("foo.c")
# 调用 env 对象的 GetBuildPath 方法,获取节点列表的路径列表
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')
# 使用 timestamp-match 规则进行判断,只要时间戳不吻合,即需要重新构建,不管新旧
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)

scons 用法
https://ccw1078.github.io/2021/01/17/scons 用法/
作者
ccw
发布于
2021年1月17日
许可协议