SCons
scons是一个Python写的自动化构建工具,使用SConstruct文件进行构建。
Wiki:https://github.com/SCons/scons/wiki
manual page: SCons 4.6.0
SConstruct文件,类似于Make系统中的Makefile文件,这是SCons读取并控制编译构建的输入文件。 SConstruct和Makefile的最大不同是:SConstruct文件是python脚本,这会大大简化构建的过程。
Scons函数与构建顺序无关
SConstruct有一点和一般python脚本不同,SCons函数的书写调用顺序,并不影响SCons真实的构建顺序,这点和Makefile有点相似。换句话说,当你调用Program构建时(或者其他构建方法),SCons并非在此刻构建可执行文件,相反,这仅仅是告诉SCons你想要获得一个可执行文件的构建结果。举例来说,需要构建hello.c的源文件,SCons也仅仅是获取了构建可执行文件hello和源码hello.c之间的关系。
一、Scons构建方法
1. Program构建二进制文件
如果想构建hello.c程序,新建一个
1 | Program('hello.c') |
在SCons命令行中执行scons
命令进行构建,会有scons会执行以下编译命令,默认得到hello
二进制可执行文件
1 | > scons |
1.1 指定二进制文件名称
如果想要指定二进制文件的文件名,则需要在源文件的前面,指明输出名称即可
1 | Program('new_hello', 'hello.c') |
1.2 简化构建输出
如果想要简化构建时命令行的输出,只看实际执行的编译命令,则在构建时使用-Q选项
1 | > scons -Q |
1.3 编译多个源文件
编译多个源文件,可以采用列表的方式指定多个源文件名
1 | # build hello.c only |
可以用scons内置的Split函数自动分割字符串
1 | # Split function help divide the file string |
1.4 关键字参数
SCons同样支持输入或输出文件的关键字定义。输出文件的关键字是target,输入源文件的关键字是source, 因为关键字已经指明了该参数的含义,因此关键字的顺序不做要求
1 | src_files = Split('main.c file1.c file2.c') |
1.5 构建多个工程
用一个SConstruct文件,构建多个工程,最简单的方式是调用Program多次构建:
1 | Program('foo.c') |
1.6 多工程编译共享中间文件
同一份源码文件对应多个工程,最直接的方式是每个工程都加入该源码:
1 | Program(Split('foo.c common1.c common2.c')) |
当采用这种方式构建时,SCons会意识到common1.c和common2.c的复用,因此会将其仅仅编译一份中间文件,尽管生成的目标文件分别链接到各自的中间文件:
1 | % scons -Q |
1.7 清理构建的工程
采用SCons我们不需要增加特殊的指令在构建后执行清除操作,相反,你可以简单使用-c或者–clean选项,此时SCons会自动删除构建的文件
1 | > scons |
2. Object构建中间文件
Object用于构建中间文件:
1 | Object('hello.c') |
执行scons命令,去构建整个工程,在LINUX工程中这将会仅仅构建hello.o。
1 | > scons |
3. Libary编译库文件
与Program格式相同
3.1 全部通过源文件
来编译库
1 | Library('foo', ['f1.c', 'f2.c', 'f3.c']) |
SCons会自动根据系统来创建合适的库文件前缀和后缀,因此在Linux系统上,上述示例将会构建以下内容:
SCons会自动给库文件加入前缀和后缀。
1 | > scons -Q |
如果不特别指定目标库文件名称,SCons将会从源文件列表中选择第一个作为库文件名称。
3.2 通过中间文件
或源文件和中间文件
来编译库
上述示例介绍了通过源文件列表构建库文件,SCons同样也支持通过中间文件构建,或者源文件和中间文件混在一起构建也可以。
1 | Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o']) |
同时SCons也会意识到,只有源文件才需要进一步构建成中间文件:
1 | > scons -Q |
4. StaticLibrary编译静态库
对于Library和StaticLibrary,他们完全等价,没有任何区别。
1 | StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c']) |
5. SharedLibrary构建动态库文件
1 | SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c']) |
Linux系统上输出为:
1 | > scons -Q |
二、链接库文件
1. LIBS关键字指明库文件
通过指明LIBS变量关键字,来指定需要链接的库文件
1 | Library('foo', ['f1.c', 'f2.c', 'f3.c']) |
不需要特别声明库文件的前缀(如lib),或者后缀(如.a或.lib),SCons会自动根据系统来查找相关前缀或后缀。
1 | > scons -Q |
2.LIBPATH关键字指明库文件的查找路径
通过指明LIBPATH变量关键字,来指定库文件的查找路径
默认情况下,连接器只会在系统路径中查找库文件,SCons可以通过用户指定的LIBPATH变量,来查找用户定义路径:
1 | Program('prog.c', LIBS = 'm', |
Linux 中输出如下
1 | > scons -Q |
这里推荐使用python的列表(list),因为python是跨平台的,这要迁移起来比较方便。当然您也可以将搜索路径放到一个字符串中,采用系统指定的分割符分开,如POSIX系统中采用冒号,Windows系统中采用分号:
1 | # POSIX |
python在Windows路径中要求采用反斜杠转义符
三、节点对象
在SCons内部,所有的文件和路径都被看作是节点(Nodes),这种方式让SConscript脚本更加便于迁移和阅读。
1. 跨平台时出现的问题
1 | Object('hello.c', CCFLAGS='-DHELLO') |
以上代码在linux上是可以正常编译的,但在windows上不可以,因为在Windows平台上,生成的中间文件是hello.obj和goodbye.obj,而非hello.o和goodby.o。
较好的处理方式是将Object的构建输出存入变量中,这样我们可以不断在列表后追加新的内容,将其作为Program的输入:
1 | hello_list = Object('hello.c', CCFLAGS='-DHELLO') |
这样SConstruct脚本的跨平台性可以得到保证,其在Linux平台输出如下:
1 | > scons -Q |
Windows平台输出如下:
1 | C:\>scons -Q |
2. 构建方法返回目标节点列表
所有的构建方法返回一个节点对象列表,这些节点可以被用作其他构建方法的参数。
1 | object_list = Object('hello.c') |
linux平台输出如下
1 | > scons -Q |
3. File创建文件节点 & Dir创建路径节点
SCons明确了文件节点和路径节点的不同,SCons支持File 和Dir两个函数,用于返回文件和路径节点:
1 | hello_c = File('hello.c') |
通常情况下,您不需要手动调用File或Dir,因为调用构建方法时,会自动将输入作为文件或目录的名称,并将其转换为一个Node对象。
有时对象可能是文件也可能是一个路径,此时SCons提供了一个Entry函数,用于返回一个文件节点或者路径节点。
1 | xyzzy = Entry('xyzzy') |
4. 打印节点对象名称
大多数情况下我们需要调用节点去打印其内部的文件名称,但是请注意此时的对象是节点列表,而非文件对象,因此打印的时候需要增加下标访问:
1 | object_list = Object('hello.c') |
在Linux系统输出如下:
1 | > scons -Q |
在上面的示例中,object_list[0]从列表中提取了一个实际的Node对象,Python的print语句将该对象转换为要打印的字符串。
5. 节点对象转换为字符串
如上节介绍所示,我们可以直接打印节点的文件信息,但是如果您想得到一个节点字符串,而非列表,则可以通过python内置的str函数实现。举例而言,如果您希望使用Python的os.path.exists来确认文件是否存在,您可以采用如下方式:
1 | import os.path |
此时,在Linux系统中将会得到如下输出:
1 | > scons -Q |
四、判断是否需要重新编译
SCons很智能,只会编译需要编译的内容。比如我刚执行完scons,再次执行,则会提示scons: . is up to date.
。 那么他是如何做到的呢?也不复杂,依赖一个Decider的方法,以及一个.sconsign.dblite
文件。
默认情况下,如果文件的md5值改变了,才会重新编译。每次编译,SCons都会把md5存起来,再次执行时,如果md5没变,则不需要rebuild。
五、解析命令行选项
1. AddOption
作用:添加一项命令行选项
用法类似python中optparse库解析命令行参数的方法add_option
https://blog.csdn.net/lwnylslwnyls/article/details/8199454
1 | AddOption( |
添加以上命令行选项后,执行scons -h后可以看到添加的选项说明
1 | Local Options: |
2. GetOption
作用:读取相应的命令行选项
通过AddOption得到的命令行参数,可以通过GetOption(‘参数名’)来获得,如上例所示
3. SetOption
作用:设置相应的命令行选项。
通过命令行选项设置的值将优先于使用SetOption设置的值
SetOption允许在脚本中设置项目默认值,并通过命令行临时覆盖它。SetOption调用也可以放在site_init.py文件中。
4. Help
在一个命令不知道怎么使用时,使用 –help 来获取当前命令的选项帮助是最好的方法。SCons 也是提供了 help 选项的编写,可以帮助用户简单的创建 –help 的语句。
1 | Help(""" |
scons -h输出如下
1 | > scons -h |
六、site_scons目录
在scons解析SConscript文件之前,scons将首先在各种系统目录
,含有SConstruct文件的目录
和scons 的命令行选项--site-dir指定的目录
这三种目录下寻找site_scons目录,并将找到的site_scons目录放到python模块的搜索路径中,即sys.path。这样就使得SConscript文件可以引入site_scons目录下的模块并使用。
此外,有的site_scons目录还可能含有 site_init.py
文件和site_tools
目录
1.site_init.py
site_init.py文件将会先于SConscript文件被执行
2. site_tools
site_tools目录的路径会被预先添加到默认工作路径
七、SCons内置函数
1 GetCurrentDir()
获取当前路径。
2 Glob()
获取当前目录下的所有 C 文件。修改参数的值为其他后缀就可以匹配当前目录下的所有某类型的文件。
3 Split(str)
将字符串 str 分割成一个列表 list。
八、环境
环境是可以影响程序执行方式的值的集合,SCons区分了三种不同类型的环境,这些环境都可能会影响SCons本身的行为、它执行的编译器以及其他工具。
1. 外部环境 External Environment
外部环境是用户运行SCons时用户环境中的一组变量。这些变量不是SCons构建的自动组成部分,但在需要时可以进行检查。
外部环境即为Python 中os模块的environ字典,它提供了用户在执行SCons时有效的外部环境变量设置。
1 | import os |
os.environ 是一个字典,是环境变量的字典,环境变量是程序和操作系统之间的通信方式
常见 key 字段
1 | windows: |
2. 构造环境 Construction Environment
构造环境是在SConscript文件中创建的一个不同对象,它包含的值会影响SCons如何决定使用什么操作来构建目标,甚至定义应该从哪些源构建哪些目标。SCons最强大的功能之一是能够创建多个构造环境,包括从现有构造环境克隆新的自定义构造环境的能力。
构造环境是一个对象,它有许多相关的构造变量,每个变量都有一个名称和一个值,就像字典一样。
2.1 设置环境构造变量
1 | # 由Environment()方法创建一个构造环境对象 |
用户已明确指定使用GNU C编译器gcc,并且在编译对象文件时应使用-O2(优化级别2)标志。换句话说,$CC和$CCFLAGS的显式初始化会覆盖新创建的构造环境中的默认值。此时使用scons输出如下
1 | > scons -Q |
2.2 获取环境构造变量的值
Python打印调用将为我们输出CC和LATEX的值
ps:使用.get(xxx, None)方法进行获取意味着,如果未设置变量,我们将返回None,而不是失败
1 | env = Environment() |
构造环境实际上是一个具有关联方法和属性的对象。如果您只想直接访问构造变量的字典,则可以使用env的Dictionary方法获取该字典。
1 | env = Environment(FOO='foo', BAR='bar') |
使用dump()方法格式化输出构造环境的信息
1 | env = Environment() |
subst方法
直接通过字典索引的方式输出环境构造变量的值时,有些环境构造变量不会展开,使得阅读存在困难
1 | env = Environment(CCFLAGS='-DFOO') |
而使用subst方法输出,则会展开全部环境构造变量
1 | env = Environment(CCFLAGS='-DFOO') |
可见,CC被展开为gcc,CCFLAGS被展开为-DFOO
2.3 默认构造环境
如果没有指定构造环境,Program和Library等构造方法都是使用默认构造环境
1 | # 使用默认构造环境 |
而控制默认构造和环境的方法为DefaultEnvironment
DefaultEnvironment函数返回初始化的默认构造环境对象,然后可以像任何其他构造环境一样对其进行操作,例如以下示例指定了默认构造环境使用的编译器为’/usr/local/bin/gcc’
1 | # 方式一 |
2.4 创建多个构造环境
SCons的一大优势是可以创造多个构造环境。
不同的源文件可能需要在命令行上启用不同的选项,或者不同的可执行程序需要与不同的库链接。SCons允许您创建和配置控制软件构建方式的多个构建环境,从而满足这些不同的构建需求。
每个构建环境根据构建某个软件或其他文件的不同方式进行定制。例如,如果我们需要用 -O2标志构建一个程序,用 -g (debug)标志构建另一个程序,我们可以这样做:
1 | opt = Environment(CCFLAGS='-O2') |
甚至可以构建同一个源文件的不同版本程序
1 | opt = Environment(CCFLAGS='-O2') |
但是因为产生了相同名字的不同中间文件而报错,为了避免这个问题,我们必须明确指定每个环境使用object生成器将foo.c编译为一个单独命名的对象文件,如下所示:
1 | opt = Environment(CCFLAGS='-O2') |
2.5 复制构造环境
有时,您希望多个构造环境共享一个或多个变量的相同值。在创建每个构造环境时,不必总是重复所有公共变量,而是可以使用env构造环境的Clone方法来创建构造环境的副本。
与创建构造环境的Environment调用一样,Clone方法接受构造变量赋值,该赋值将覆盖复制的构造环境中的值。
例如,假设我们想使用gcc创建一个程序的三个版本,一个是优化的,一个调试的,还有一个两者都没有。我们可以通过创建一个“基本”构造环境来实现这一点,该环境将CC设置为gcc,然后创建两个副本,一个设置CCFLAGS用于优化,另一个设置CCFLAGS用于调试:
1 | env = Environment(CC='gcc') |
2.6 更改环境构造变量
通过env的Replace方法可以更改env构造环境中环境构造变量的值,如果这个环境构造变量不存在,则新增。
1 | env = Environment(CCFLAGS='-DDEFINE1') |
在构建环境实际用于构建目标之前,变量不会展开,而且SCons函数和方法调用与顺序无关,所以最后一个替换“获胜”并用于构建所有目标,而不管对Replace()的调用与对构建器方法的调用之间的顺序如何
1 | env = Environment(CCFLAGS='-DDEFINE1') |
3. 执行环境 Execution Environment
执行环境是SCons在执行外部命令(如编译器或链接器)以构建一个或多个目标时设置的值。
参考资料: