BLOG

Record, summarize, and improve.

scons

构建和链接库 节点对象 依赖 确定输入文件何时更改:Decider函数 隐式依赖:$CPPPATH 构造变量 缓存隐式依赖 显式依赖:Depends 函数 来自外部文件的依赖项:ParseDepends 函数 忽略依赖关系:Ignore 函数 Order-Only 依赖:Requires 函数 AlwaysBuild 函数 环境 使用来自外部环境的值 构造环境 控制发出命令的执行环境 使用外部工具的工具路径 自动将命令行选项放入其构造变量中 将选项合并到环境中:MergeFlags函数 在创建环境时合并选项:parse_flags 参数 将编译参数分离到它们的变量中:ParseFlags 函数 查找已安装的库信息:ParseConfig 函数 控制build输出 提供build帮助:help函数 控制 SCons 如何打印构建命令:$*COMSTR 变量 提供build进度输出:Progress函数 打印详细的build状态:GetBuildFailures 函数 从命令行控制构建 命令行选项 命令行 variable=value 构建变量 命令行目标 在其他目录中安装文件:Install Builder 在目录中安装多个文件 以不同的名称安装文件 以不同名称安装多个文件 安装共享库 独立于平台的文件系统操作 复制文件或目录:Copy工厂 删除文件或目录:Delete工厂 移动(重命名)文件或目录:Move工厂 更新文件的修改时间:Touch工厂 创建目录:Mkdir 工厂 更改文件或目录权限:Chmod 工厂 立即执行一个动作:Execute Function 控制目标的移除 在构建过程中防止目标移除:Precious函数 在clean过程中防止目标移除:NoClean功能 在clean过程中删除额外的文件:Clean函数 分层构建 SConscript文件 路径名称是与SConscript目录相关的 辅助SConscript文件中的顶级相关路径名称 绝对路径名称 在SConscript文件之间共享环境(和其他变量) 分离源代码和构建树:变体目录 将变体目录树指定为 SConscript 调用的一部分 为什么 SCons 在变体目录树中复制源文件 告诉 SCons 不要在变体目录树中复制源文件 VariantDir 函数 将 VariantDir 与 SConscript 文件一起使用 将 Glob 与 VariantDir 一起使用 变体build示例 从代码库构建 存储库方法 在存储库中查找源文件 在存储库中查找#include 文件 在存储库中查找 SConstruct 文件 在存储库中查找派生文件 保证文件的本地副本 扩展 SCons:编写自己的构建器 编写执行外部命令的构建器 将构建器附加到构建环境 让 SCons 处理文件后缀 执行 Python 函数的构建器 使用生成器创建操作的构建器 使用发射器修改目标或源列表的构建器 通过添加发射器修改构建器 将自定义生成器和工具放在哪里 不编写生成器:命令生成器 扩展 SCons:伪构建器和 AddMethod 函数 扩展 SCons:编写你自己的扫描器 一个简单的扫描仪示例 将搜索路径添加到扫描仪:FindPathDirs 将扫描仪与构建器一起使用 多平台配置(Autoconf 功能) 配置上下文 检查头文件是否存在 检查功能的可用性 检查库的可用性 检查 typedef 的可用性 检查数据类型的大小 检查程序的存在 扩展 SCons:添加您自己的自定义检查 清理目标时不配置 缓存内置文件 指定派生文件缓存目录 保持构建输出一致 不对特定文件使用派生文件缓存 禁用派生文件缓存 使用已构建的文件填充派生文件缓存 最小化缓存争用:—random 选项 使用自定义 CacheDir 类

包含的文件:

  1. SConstruct 文件
    这是 SCons 的主配置文件,相当于 Makefile,在项目的根目录下。它定义了整个构建过程,包括构建目标、构建依赖和构建参数等。
  2. SConscript 文件
    这些文件通常存在于项目的子目录中,被 SConstruct 文件调用。SConscript 文件用于定义子目录中的构建规则,可以分散和模块化构建逻辑。

YAML 文件不是 SCons 必须的。SCons 使用 Python 脚本作为其构建脚本(SConstruct 和 SConscript 文件),并且自身不依赖于 YAML。

然而,在一些项目中,可能会用 YAML 文件来存储一些配置信息,然后在 SCons 脚本中通过 Python 读取这些 YAML 文件来获取构建配置的参数。这是项目开发者为了方便配置管理可能采用的一种做法,而不是 SCons 内建的特性。如果你的项目中使用了 YAML 文件,你需要在 Python 脚本中使用如 PyYAML 这样的库来解析 YAML 内容。

关键字:

Program 是一个构建器方法,一个 Python 调用,它告诉 SCons 你想要构建一个可执行程序

Program('hello.c')

另一个是 Object builder 方法,它告诉 SCons 从指定的源文件构建一个目标文件

Object('hello.c')

与程序和对象构建方法不同的是,Java构建方法要求你指定一个目标目录的名称,你希望将类文件放在这个目录中,然后是.java文件所在的源目录

Java('classes', 'src')

只需在调用SCons时使用-c或--clean选项,SCons就会删除相应的构建文件

scons -c

SCons工作顺序:首先读取和执行所有配置文件(称为SConscript文件),然后才构建目标文件

专注于SCons实际执行的操作,-Q选项从示例的输出中删除冗余消息

目标文件名在源文件名的左边,输出new_hello

Program('new_hello', 'hello.c')

编译多个文件

Program(['prog.c', 'file1.c', 'file2.c'])

指定输出程序名字

Program('program', ['prog.c', 'file1.c', 'file2.c'])

使⽤ Glob 函数查找与特定模板匹配的所有⽂件

Program('program', Glob('*.c'))

common_sources = ['file1.c', 'file2.c']
# error
Program('program1', common_sources + 'program1.c')
# 正常执行
Program('program2', common_sources + ['program2.c'])

使用 Split 函数,该函数采⽤带引号的⽂件名列表,名称由空格或其他空⽩字符分隔,并将其转成文件名列表

src_files = Split('main.c file1.c file2.c')
Program('program', src_files)

关键字参数,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')

重用代码

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)

构建和链接库

从源文件构建库

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'])

通过在$LIBS构造变量中指定库,并在$LIBPATH构造变量中指定库所在的目录,可以将库与程序链接起来

Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')

节点对象

所有构建器⽅法都返回⼀个 Node 对象列表,这些对象标识将构建⼀个或多个目标文件

SCons将所有知道的文件和目录都表示为节点,将多个构建器的文件链接成一个程序(好处是可以跨平台使用)

hello_list = Object('hello.c', CCFLAGS='-DHELLO')
goodbye_list = Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(hello_list + goodbye_list)

返回一个文件节点和一个目录节点

hello_c = File('hello.c')
Program(hello_c)
classes = Dir('classes')
Java(classes, 'src')

Entry函数,它返回一个Node,可以代表一个文件或一个目录

xyzzy = Entry('xyzzy')

通过使用内置的 Python str 函数来获取字符串

import os.path
program_list = Program('hello.c')
program_name = str(program_list[0])
if not os.path.exists(program_name):
 print("%s does not exist!"%program_name)

env.GetBuildPath(file_or_list)返回一个Node的路径或代表路径的字符串。它也可以接受一个Node和/或字符串的列表,并返回路径的列表,字符串可以有内嵌的结构变量,像往常一样,使用调用Environment的变量集来展开它们。路径可以是文件或目录,而且不一定要存在

env=Environment(VAR="value")
n=File("foo.c")
print(env.GetBuildPath([n, "sub/dir/$VAR"]))

GetBuildPath的函数版本,可以在没有Environment的情况下调用;它使用默认的SCons Environment来对任何字符串参数进行替换

依赖

确定输入文件何时更改:Decider函数

SCons 使用文件内容的加密哈希而不是文件的修改时间来确定文件是否已更改。这意味着如果您习惯于通过更新文件的修改时间(例如使用 touch 命令)强制重建的 Make 约定,您可能会对默认的 SCons 行为感到惊讶

指定内容签名默认行为,仅当文件的时间戳发生变化时才读取这些内容

Program('hello.c')
Decider('content')

在调用Decider函数时,你也可以使用字符串'MD5'作为'content'的同义词,SCons现在支持多种哈希函数的选择,而不仅仅是MD5函数

时间戳方式

Object('hello.c')
Decider('timestamp-newer')

也可以使用字符串 'make' 作为 'timestamp-newer' 的同义词

检查源文件时间戳的精确匹配来处理输入文件的修改时间比目标文件早,目标文件不会被重建这种情况

Object('hello.c')
Decider('timestamp-match')

仅当文件的时间戳发生变化时才读取这些内容

Program('hello.c')
Decider('content-timestamp')

自定义Decider函数

Program('hello.c')
def decide_if_changed(dependency, target, prev_ni, repo_node=None):
    if dependency.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)

第三个参数 prev_ni 是一个对象,它包含上次构建目标时记录的有关依赖项的内容签名和/或时间戳信息。 prev_ni 对象可以保存不同的信息,这取决于依赖参数表示的事物的类型。对于普通文件,prev_ni 对象具有以下属性:

csig 内容签名:上次构建目标时依赖文件的文件内容的加密哈希或校验和。

size 上次构建目标时依赖文件的大小(以字节为单位)。

timestamp 上次构建目标时依赖文件的修改时间

这些属性在第一次运行时可能不存在。在没有任何先前构建的情况下,没有创建任何目标,也不存在 .sconsign 数据库文件。因此,您应该始终检查有问题的 prev_ni 属性是否可用(使用 Python hasattr 方法或 try-except 块)。

第四个参数 repo_node 是在比较 BuildInfo 时如果它不是 None 时要使用的节点。这通常仅在目标节点仅存在于存储库中时才设置 注意忽略自定义 Decider 函数中的某些参数是完全正常的事情,如果它们不影响您想要决定依赖文件的方式已经改变。

我们最后展示了一个基于 csig 的决策函数的小例子。请注意依赖文件的签名信息必须如何在每次函数调用期间通过 get_csig 进行初始化(这是强制性的!)

env = Environment()

def config_file_decider(dependency, target, prev_ni, repo_node=None):
    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 not prev_ni.hasattr("csig"):
        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():
    with open("test.txt", "a") as f:
        f.write("some line\\n")
update_file()
# Activate our own decider function
env.Decider(config_file_decider)
env.Install("install", "test.txt")

如果我们任意想构建一个使用内容签名的程序,另一个使用来自同一源的文件修改时间,我们可以这样配置

env1 = Environment(CPPPATH = ['.'])
env2 = env1.Clone()
env2.Decider('timestamp-match')
env1.Program('prog-content', 'program1.c')
env2.Program('prog-timestamp', 'program2.c')

如果两个程序都包含相同的 inc.h 文件,那么更新 inc.h 的修改时间(使用 touch 命令)将导致只重建 prog-timestamp

隐式依赖:$CPPPATH 构造变量

如果 hello.h 文件的内容发生变化,则必须重新编译 hello 程序。为此,我们需要像这样修改 SConstruct 文件

Program('hello.c', CPPPATH='.')

$CPPPATH 值告诉 SCons 在当前目录 ('.') 中查找 C 源文件(.c 或 .h 文件)包含的任何文件

与 $LIBPATH 变量一样,$CPPPATH 变量可以是目录列表,或由系统特定路径分隔符分隔的字符串(在 POSIX/Linux 上为“:”,在 Windows 上为“;”)。无论哪种方式,SCons 都会创建正确的命令行选项,以便下面的示例

Program('hello.c', CPPPATH = ['include', '/home/project/inc'])
缓存隐式依赖

SCons 允许您缓存其扫描器找到的隐式依赖项,以供以后构建使用。您可以通过在命令行上指定 --implicit-cache 选项来执行此操作

% scons -Q --implicit-cache hello

如果您不想每次都在命令行上指定 --implicit-cache,您可以通过在 SConscript 文件中设置 implicit_cache 选项使其成为构建的默认行为

SetOption('implicit_cache', 1)
  • 当使用--implicit-cache 时,SCons 将忽略可能对搜索路径所做的任何更改(如$CPPPATH 或$LIBPATH)。如果对 $CPPPATH 的更改通常会导致使用来自不同目录的不同的同名文件,这可能会导致 SCons 不重建文件。
  • 当使用--implicit-cache 时,SCons 将不会检测同名文件是否已添加到搜索路径中比上次找到该文件的目录更早的目录中

使用缓存的隐式依赖项时,有时您希望“重新开始”并让 SCons 重新扫描之前缓存依赖项的文件。例如,如果您最近安装了用于编译的新版本外部代码,则外部头文件将发生更改,并且先前缓存的隐式依赖项将过时。您可以通过使用 --implicit-deps-changed 选项运行 SCons 来更新它们

scons -Q --implicit-deps-changed hello

在这种情况下,SCons 将重新扫描所有隐式依赖项并缓存信息的更新副本

强制 SCons 使用缓存的隐式依赖项,即使源文件已更改。这可以加快构建速度,例如,当您更改了源文件但知道您没有更改任何#include 行时。在这种情况下,您可以使用 --implicitdeps-unchanged 选项

% scons -Q --implicit-deps-unchanged hello

在这种情况下,SCons 将假定缓存的隐式依赖项是正确的,并且不会费心重新扫描更改的文件。对于对源文件进行小的增量更改后的典型构建,节省的空间可能不是很大,但有时性能的每一点改进都很重要

显式依赖:Depends 函数

有时一个文件依赖于 SCons 扫描器未检测到的另一个文件。对于这种情况,SCons 允许您明确指定一个文件依赖于另一个文件,并且必须在该文件更改时重建。这是使用 Depends 方法指定的:

hello = Program('hello.c')
Depends(hello, 'other_file')

请注意,依赖项(Depends 的第二个参数)也可能是 Node 对象的列表(例如,通过调用 Builder 返回)

hello = Program('hello.c')
goodbye = Program('goodbye.c')
Depends(hello, goodbye)

在这种情况下,依赖项或依赖项将在目标之前构建

来自外部文件的依赖项:ParseDepends 函数

SCons 内置了多种语言的扫描器。有时,由于扫描器实现的限制,这些扫描器无法提取某些隐式依赖项。

以下示例说明了内置 C 扫描器无法提取对头文件的隐式依赖项的情况

#define FOO_HEADER <foo.h>
#include FOO_HEADER
int main() {
    return FOO;
}

显然,扫描器不知道标头依赖性。不是成熟的 C 预处理器,扫描器不会扩展宏。

在这些情况下,您还可以使用编译器来提取隐式依赖项。 ParseDepends 可以以 Make 的方式解析编译器输出的内容,并显式建立所有列出的依赖项。

以下示例使用 ParseDepends 来处理编译器生成的依赖文件,该文件是在编译目标文件期间作为副作用生成的

obj = Object('hello.c', CCFLAGS='-MD -MF hello.d', CPPPATH='.')
SideEffect('hello.d', obj)
ParseDepends('hello.d')
Program('hello', obj)

从编译器生成的 .d 文件解析依赖项存在先有鸡还是先有蛋的问题,会导致不必要的重建

在第一遍中,在编译目标文件时生成依赖文件。那时,SCons 并不知道对 foo.h 的依赖。在第二遍中,目标文件被重新生成,因为 foo.h 被检测为新的依赖项。

ParseDepends 在调用时立即读取指定的文件,如果文件不存在则返回。构建过程中生成的依赖文件不会自动再次解析。因此,在同一构建过程中,编译器提取的依赖项不会存储在签名数据库中。 ParseDepends 的这种限制会导致不必要的重新编译。因此,ParseDepends 只应在扫描器对所用语言不可用或功能不足以完成特定任务时使用

忽略依赖关系:Ignore 函数

有时不重建程序是有意义的,即使依赖文件发生变化。在这种情况下,您可以使用 Ignore 函数专门告诉 SCons 忽略依赖项,如下所示

hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore(hello_obj, 'hello.h')

一个更现实的例子可能是,如果 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])
Order-Only 依赖:Requires 函数

有时,指定某个文件或目录必须(如有必要)在构建其他目标之前构建或创建可能很有用,但对该文件或目录的更改不需要重建目标本身。

一种关系被称为 order-only 依赖关系,因为它只影响构建事物的顺序——目标之前的依赖关系——但它不是严格的依赖关系,因为目标不应该随着目标的变化而改变依赖文件。

例如,假设您希望在每次运行构建时创建一个文件,该文件标识执行构建的时间、版本号等,并且包含在您构建的每个程序中。版本文件的内容将在每次构建时发生变化。如果您指定一个正常的依赖关系,那么在您每次运行 SCons 时,每个依赖该文件的程序都会被重建。例如,我们可以在 SConstruct 文件中使用一些 Python 代码来创建一个新的 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 列为实际源文件,那么每次运行 SCons 时都会重建 version.o 文件(因为 SConstruct 文件本身会更改 version.c 的内容)并且 hello 可执行文件将重新构建每次都链接(因为version.o文件变了)

(请注意,为了使上面的示例正常工作,我们在每次运行之间休眠一秒钟,以便 SConstruct 文件将创建一个 version.c 文件,其时间字符串比上一次运行晚一秒钟。)一个解决方案是使用 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 程序的来源之一,所以我们必须找到其他方法将其放入链接命令行。对于这个例子,我们有点作弊并将对象文件名(从对象生成器调用返回的 version_obj 列表中提取)填充到 $LINKFLAGS 变量中,因为 $LINKFLAGS 已经包含在 $LINKCOM 命令行中。

通过这些更改,我们得到了仅在 hello.c 发生更改时才重新链接 hello 可执行文件的预期行为,即使重建了 version.o(因为 SConstruct 文件仍会在每次运行时直接更改 version.c 的内容)

AlwaysBuild 函数

SCons 处理依赖关系的方式也会受到 AlwaysBuild 方法的影响。当一个文件被传递给 AlwaysBuild 方法时,像这样

hello = Program('hello.c')
AlwaysBuild(hello)

然后指定的目标文件(在我们的示例中为 hello)将始终被视为过时的并在遍历依赖关系图时评估目标文件时重建:

环境

SCons 区分三种不同类型的环境,这些环境会影响 SCons 本身的行为(取决于 SConscript 文件中的配置),以及它执行的编译器和其他工具:

外部环境

用户运行 SCons 时的用户环境。这些变量不会自动成为 SCons 构建的一部分,但可以在需要时进行检查。请参阅下面的第 7.1 节,“使用来自外部环境的值”。

构造环境

构造环境是在 SConscript 文件中创建的一个独特对象,它包含影响 SCons 如何决定使用什么操作来构建目标的值,甚至可以定义应该从哪些源构建哪些目标。 SCons 最强大的功能之一是能够创建多个构建环境,包括能够从现有构建环境克隆新的定制构建环境。请参阅下面的第 7.2 节“构建环境”。

执行环境

执行环境是 SCons 在执行外部命令(例如编译器或链接器)以构建一个或多个目标时设置的值。请注意,这与外部环境不同(见上文)。请参阅下面的第 7.3 节“控制已发出命令的执行环境”。

与 Make 不同,SCons 不会在不同环境之间自动复制或导入值(构建环境的显式克隆除外,它们从其父级继承值)。这是一个深思熟虑的设计选择,以确保构建在默认情况下是可重复的,而不管用户外部环境中的值如何。

这避免了开发人员本地构建工作的构建问题,因为自定义变量设置导致使用不同的编译器或构建选项,但签入的更改破坏了官方构建,因为它使用了不同的环境变量设置。

使用来自外部环境的值

用户在执行 SConx 时生效的外部环境变量设置在 Python os.environ 字典中可用。该语法表示 os 模块的 environ 属性。在 Python 中,要访问模块的内容,您必须首先导入它 - 因此您可以将 import os 语句包含到任何要在其中使用用户外部环境值的 SConscript 文件

import os
print("Shell is", os.environ['SHELL'])

更有用的是,您可以在 SConscript 文件中使用 os.environ 字典,使用来自用户外部环境的值来初始化构造环境

构造环境

一个大型复杂系统中的所有软件都需要以完全相同的方式构建的情况很少见。例如,不同的源文件可能需要在命令行上启用不同的选项,或者不同的可执行程序需要链接不同的库。 SCons 通过允许您创建和配置多个控制软件构建方式的构建环境来满足这些不同的构建要求。构造环境是一个对象,它有许多关联的构造变量,每个变量都有一个名称和一个值,就像字典一样。

(构建环境也有一组附加的 Builder 方法,我们稍后会详细了解。)

构造环境由 Environment 方法创建

env = Environment()

默认情况下,SCons 根据它在您的系统上找到的工具以及使用这些工具所需的默认构建器方法集,使用一组构建变量初始化每个新的构建环境。构造变量使用描述 C 编译器、Fortran 编译器、链接器等的值以及调用它们的命令行进行初始化。

初始化构建环境时,您可以设置环境的构建变量的值来控制程序的构建方式。例如:

env = Environment(CC='gcc', CCFLAGS='-O2')
env.Program('foo.c')

本例中的构造环境仍然使用相同的默认构造变量值进行初始化,除了用户已明确指定使用 GNU C 编译器 gcc,并且在编译对象时应使用 -O2(优化级别二级)标志文件。换句话说, $CC 和 $CCFLAGS 的显式初始化覆盖了新建构造环境中的默认值。所以这个例子的运行看起来像

gcc -o foo.o -c -O2 foo.c

您可以使用用于访问 Python 字典中单个命名项的相同语法来获取单个值,称为构造变量

env = Environment()
print("CC is: %s" % env['CC'])
print("LATEX is: %s" % env.get('LATEX', None))

这个示例 SConstruct 文件不包含构建任何目标的说明,但因为它仍然是一个有效的 SConstruct,它将被评估并且 Python 打印调用将为我们输出 $CC 和 $LATEX 的值

构造环境实际上是一个具有关联方法和属性的对象。如果你只想直接访问构造变量的字典,你可以使用 env.Dictionary 方法获取它

env = Environment(FOO='foo', BAR='bar')
cvars = env.Dictionary()
for key in ['OBJSUFFIX', 'LIBSUFFIX', 'PROGSUFFIX']:
    print("key = %s, value = %s" % (key, cvars[key]))

如果你想循环并打印构造环境中所有构造变量的值,按排序顺序执行此操作的 Python 代码可能类似于

env = Environment()
for item in sorted(env.Dictionary().items()):
    print("construction variable = '%s', value = '%s'" % item)

应该注意的是,对于前面的例子,实际上有一个构建环境的方法可以更简单地做同样的事情,并试图很好地格式化输出

env = Environment()
print(env.Dump())

从构建环境获取信息的另一种方法是对包含构建变量名称的 $ 扩展的字符串使用 subst 方法。作为一个简单的例子,上一节中使用 env['CC'] 获取 $CC 值的例子也可以写成

env = Environment()
print("CC is: %s" % env.subst('$CC'))

使用 subst 扩展字符串的优点之一是结果中的构造变量会被重新扩展,直到字符串中没有剩余的扩展。所以一个简单的获取像 $CCCOM 这样的值

env = Environment(CCFLAGS='-DFOO')
print("CCCOM is: %s" % env['CCCOM'])

但是,在 $CCOM 上调用 subst 方法

env = Environment(CCFLAGS='-DFOO')
print("CCCOM is: %s" % env.subst('$CCCOM'))

将递归展开所有以 $(美元符号)为前缀的构造变量,向我们展示最终输出

请注意,因为我们没有在构建内容的上下文中扩展它,所以没有要扩展的 $TARGET 和 $SOURCES 的目标或源文件

如果扩展构造变量时出现问题,默认扩展为''(空字符串),不会导致scons失败

env = Environment()
print("value is: %s"%env.subst( '->$MISSING<-' ))

可以使用 AllowSubstExceptions 函数更改此默认行为。当变量扩展出现问题时,它会生成一个异常,并且 AllowSubstExceptions 函数控制这些异常中哪些实际上是致命的,哪些允许安全地发生。默认情况下,NameError 和 IndexError 是允许发生的两个异常:因此不是导致 scons 失败,而是捕获它们,变量扩展为 '' 并且 scons 继续执行。要要求所有构造变量名称都存在,并且不允许超出范围的索引,请调用不带额外参数的 AllowSubstExceptions

AllowSubstExceptions()
env = Environment()
print("value is: %s"%env.subst( '->$MISSING<-' ))

这也可以用于允许可能发生的其他异常,最有用的是 ${...} 构造变量语法。例如,除了允许的默认异常之外,这将允许在变量扩展中发生零除法

AllowSubstExceptions(IndexError, NameError, ZeroDivisionError)
env = Environment()
print("value is: %s"%env.subst( '->${1 / 0}<-' ))

如果多次调用 AllowSubstExceptions,则每次调用都会完全覆盖之前的允许异常列表

到目前为止,我们介绍的所有 Builder 功能,如程序和库,都使用一个构建环境,其中包含 SCons 默认配置的各种编译器和其他工具的设置,或者以其他方式了解并在您的系统上发现。如果不作为特定构造环境的方法调用,它们将使用默认构造环境。默认构造环境的目标是使许多配置“正常工作”,以使用现成的工具以最少的配置更改来构建软件。

如果需要,您可以通过使用 DefaultEnvironment 函数来控制默认构造环境,通过将它们作为关键字参数传递来初始化各种设置

DefaultEnvironment(CC='/usr/local/bin/gcc')

当如上配置时,对程序或对象生成器的所有调用都将使用 /usr/local/bin/gcc 编译器构建对象文件。

DefaultEnvironment 函数返回初始化的默认构造环境对象,然后可以像任何其他构造环境一样操作它(请注意,默认环境像单例一样工作 - 它只能有一个实例 - 所以关键字参数仅在第一次调用时处理. 在任何后续调用中返回现有对象)。所以下面的例子等同于前面的例子,将 $CC 变量设置为 /usr/local/bin/gcc 但作为默认构建环境初始化后的一个单独步骤:

def_env = DefaultEnvironment()
def_env['CC'] = '/usr/local/bin/gcc'

DefaultEnvironment 函数的一个非常常见的用途是加速 SCons 初始化。作为尝试使大多数默认配置“正常工作”的一部分,SCons 实际上会在本地系统中搜索已安装的编译器和其他实用程序。此搜索可能需要一些时间,尤其是在具有慢速文件系统或联网文件系统的系统上。如果您知道要配置哪些编译器和/或其他实用程序,则可以通过指定一些用于初始化默认构造环境的特定工具模块来控制 SCons 执行的搜索:

def_env = DefaultEnvironment(tools=['gcc', 'gnulink'], CC='/usr/local/bin/gcc')

所以上面的例子会告诉 SCons 显式配置默认环境以使用其正常的 GNU 编译器和 GNU 链接器设置(无需搜索它们或任何其他实用程序),特别是使用 /usr/local/bin/gcc

构造环境的真正优势在于,您可以根据需要创建尽可能多的不同环境,每个环境都适合不同的方式来构建某些软件或其他文件。例如,如果我们需要构建一个带有 -O2 标志的程序和另一个带有 -g(调试)标志的程序,我们将这样做

opt = Environment(CCFLAGS='-O2')
dbg = Environment(CCFLAGS='-g')
opt.Program('foo', 'foo.c')
dbg.Program('bar', 'bar.c')

我们甚至可以使用多个构建环境来构建单个程序的多个版本。但是,如果您通过简单地尝试在两种环境中使用程序构建器来做到这一点,就像这样

opt = Environment(CCFLAGS='-O2')
dbg = Environment(CCFLAGS='-g')
opt.Program('foo', 'foo.c')
dbg.Program('foo', 'foo.c')

scons: *** Two environments with different actions were specified for the same target: foo.o File "/home/my/project/SConstruct", line 6, in <module>

这是因为两个 Program 调用都隐含地告诉 SCons 生成一个名为 foo.o 的目标文件,一个的 $CCFLAGS 值为 -O2,另一个的 $CCFLAGS 值为 -g。 SCons 不能仅仅决定其中一个应该优先于另一个,所以它会产生错误。为避免此问题,我们必须明确指定每个环境使用 Object builder 将 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)

请注意,每次调用对象生成器都会返回一个值,一个表示将要生成的对象文件的内部 SCons 对象。然后我们将该对象用作程序构建器的输入。这避免了必须在多个地方明确指定目标文件名,并生成一个紧凑、可读的 SConstruct 文件。

有时您希望多个构造环境共享一个或多个变量的相同值。

在创建每个构建环境时不必总是重复所有公共变量,您可以使用 env.Clone 方法创建构建环境的副本。

与创建构造环境的 Environment 调用一样,Clone 方法接受构造变量赋值,这将覆盖复制的构造环境中的值。例如,假设我们要使用 gcc 创建一个程序的三个版本,一个是优化的,一个是调试的,一个两者都没有。为此,我们可以创建一个将 $CC 设置为 gcc 的“基本”构造环境,然后创建两个副本,一个设置 $CCFLAGS 用于优化,另一个设置 $CCFLAGS 用于调试:

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)

您可以使用 env.Replace 方法替换现有的构造变量值:

env = Environment(CCFLAGS='-DDEFINE1')
env.Replace(CCFLAGS='-DDEFINE2')
env.Program('foo.c')

替换值(上例中的-DDEFINE2)完全替换构造环境中的值

您可以安全地为构造环境中不存在的构造变量调用 Replace

env = Environment()
env.Replace(NEW_VARIABLE='xyzzy')
print("NEW_VARIABLE = %s" % env['NEW_VARIABLE'])

在这种情况下,构造变量只是简单地添加到构造环境中

因为变量在构建环境实际用于构建目标之前不会扩展,并且因为 SCons 函数和方法调用是顺序无关的,所以最后一个替换“获胜”并用于构建所有目标,而不管顺序如何其中对 Replace() 的调用穿插在对构建器方法的调用中

env = Environment(CCFLAGS='-DDEFINE1')
print("CCFLAGS = %s" % env['CCFLAGS'])
env.Program('foo.c')
env.Replace(CCFLAGS='-DDEFINE2')
print("CCFLAGS = %s" % env['CCFLAGS'])
env.Program('bar.c')

如果我们在没有 -Q 选项的情况下运行 scons,那么相对于构建目标的时间,替换实际发生的时间变得很明显

因为替换是在读取 SConscript 文件时发生的,所以在构建 foo.o 目标时 $CCFLAGS 变量已经设置为 -DDEFINE2,即使 Replace 方法的调用直到稍后在 SConscript 中才发生文件

有时,仅当构造环境尚未定义该变量时,才能够指定构造变量应设置为一个值很有用。您可以使用 env.SetDefault 方法执行此操作,其行为类似于 Python 的 setdefault 方法字典对象

env.SetDefault(SPECIAL_FLAG='-extra-option')

这在编写自己的工具模块以将变量应用于构造环境时特别有用

您可以使用 env.Append 方法将值附加到现有构造变量:

env = Environment(CPPDEFINES=['MY_VALUE'])
env.Append(CPPDEFINES=['LAST'])
env.Program('foo.c')

注意 $CPPDEFINES 是设置预处理器定义的首选方式,因为 SCons 将使用正确的平台前缀/后缀生成命令行参数,从而使使用具有可移植性。如果您使用 $CCFLAGS 和 $SHCCFLAGS,您需要将它们包含在它们的最终形式中,这种形式的可移植性较差。

如果构造变量尚不存在,Append 方法将创建它

env = Environment()
env.Append(NEW_VARIABLE = 'added')
print("NEW_VARIABLE = %s"%env['NEW_VARIABLE'])

请注意,Append 函数试图“智能”地了解如何将新值附加到旧值。如果两者都是字符串,则将之前的字符串和新的字符串简单地连接起来。类似地,如果两者都是列表,则将列表连接起来。但是,如果一个是字符串而另一个是列表,则该字符串将作为新元素添加到列表中

有时,仅当现有构造变量尚未包含该值时,添加新值才有用。

这可以使用 env.AppendUnique 方法完成

env.AppendUnique(CCFLAGS=['-g'])

在上面的示例中,仅当 $CCFLAGS 变量尚未包含 -g 值时才会添加 -g

您可以使用 env.Prepend 方法将值添加到现有构造变量的开头:

env = Environment(CPPDEFINES=['MY_VALUE'])
env.Prepend(CPPDEFINES=['FIRST'])
env.Program('foo.c')

然后,SCons 会根据具有正确前缀/后缀的 CPPDEFINES 值生成预处理器定义参数。

例如在 Linux 或 POSIX 上,将生成以下参数:-DFIRST 和 -DMY_VALUE

如果构造变量尚不存在,则 Prepend 方法将创建它

env = Environment()
env.Prepend(NEW_VARIABLE='added')
print("NEW_VARIABLE = %s" % env['NEW_VARIABLE'])

与 Append 函数一样,Prepend 函数尝试“智能”地了解如何将新值附加到旧值。如果两者都是字符串,则将之前的字符串和新的字符串简单地连接起来。类似地,如果两者都是列表,则将列表连接起来。但是,如果一个是字符串,另一个是列表,则该字符串作为新元素添加到列表中

有时,仅当现有值尚未包含要添加的值时,将新值添加到构造变量的开头才有用。这可以使用 env.PrependUnique 方法来完成:

env.PrependUnique(CCFLAGS=['-g'])

在上面的示例中,仅当 $CCFLAGS 变量尚未包含 -g 值时才会添加 -g

您可以在调用构建器方法时通过将构造变量作为关键字参数传递来覆盖或添加构造变量,而不是为特定任务创建克隆构造环境。这些覆盖或添加的变量的值只会在构建该目标时生效,不会影响构建的其他部分。例如,如果您只想为一个程序添加额外的库

env.Program('hello', 'hello.c', LIBS=['gl', 'glut'])

或者生成一个带有非标准后缀的共享库

env.SharedLibrary(
    target='word',
    source='word.cpp',
    SHLIBSUFFIX='.ocx',
    LIBSUFFIXES=['.ocx'],
)

以这种方式覆盖时,构建器调用中的 Python 关键字参数表示“设置为此值”。如果您希望覆盖增加现有值,则必须采取一些额外的步骤。在构建器调用中,可以使用包含以美元符号 ($) 开头的变量名的字符串来替换现有值

env = Environment(CPPDEFINES="FOO")
env.Object(target="foo1.o", source="foo.c")
env.Object(target="foo2.o", source="foo.c", CPPDEFINES="BAR")
env.Object(target="foo3.o", source="foo.c", CPPDEFINES=["BAR", "$CPPDEFINES"])

也可以在覆盖中使用 parse_flags 关键字参数将命令行样式参数合并到适当的构造变量中

此示例将“include”添加到 $CPPPATH,将“EBUG”添加到 $CPPDEFINES,将“m”添加到 $LIBS

env = Environment()
env.Program('hello', 'hello.c', parse_flags='-Iinclude -DEBUG -lm')

以这种方式使用临时覆盖比创建完整的构造环境更轻量,因此它可以帮助在需要设置许多特殊情况值的大型项目中提高性能。但是,请记住,这仅在目标唯一时才有效

控制发出命令的执行环境

当 SCons 构建目标文件时,它不会使用您用于执行 SCons 的外部环境执行命令。相反,它根据存储在 $ENV 构造变量中的值构建一个执行环境,并将其用于执行命令。

这种行为最重要的后果是 PATH 环境变量,它控制操作系统将在何处查找命令和实用程序,几乎肯定与您调用 SCons 的外部环境不同。这意味着 SCons 不一定能找到您可以从命令行成功执行的所有工具

POSIX 系统上 PATH 环境变量的默认值为 /usr/local/bin:/opt/bin:/bin:/usr/bin:/snap/bin。 Windows 系统上 PATH 环境变量的默认值来自命令解释器的 Windows 注册表值。如果要执行任何不在这些默认位置的命令——编译器、链接器等,则需要在构建环境的 $ENV 字典中设置 PATH 值。

最简单的方法是在创建构造环境时显式初始化该值;这是一种方法

path = ['/usr/local/bin', '/bin', '/usr/bin']
env = Environment(ENV={'PATH': path})

以这种方式将字典分配给 $ENV 构造变量会完全重置执行环境,因此在执行外部命令时将设置的唯一变量将是 PATH 值。如果你想使用 $ENV 中的其余值并且只设置 PATH 的值,你可以只为该变量赋值

env['ENV']['PATH'] = ['/usr/local/bin', '/bin', '/usr/bin']

增加可移植性

import os
env['ENV']['PATH'] = os.pathsep.join(['/usr/local/bin', '/bin', '/usr/bin'])

您可能希望将外部环境 PATH 传播到命令的执行环境。为此,您可以使用 os.environ 字典中的 PATH 值初始化 PATH 变量,这是 Python 让您获取外部环境的方式

import os
env = Environment(ENV={'PATH': os.environ['PATH']})

或者,您可能会发现将整个外部环境传播到命令的执行环境会更容易。这比显式选择 PATH 值更容易编码

import os
env = Environment(ENV=os.environ.copy())

这些中的任何一个都将保证 SCons 能够执行您可以从命令行执行的任何命令。缺点是,如果构建由在其环境中具有不同 PATH 值的人运行,则构建的行为可能会有所不同——例如,如果 /bin 和 /usr/local/bin 目录都有不同的 cc 命令,那么将使用哪一个编译程序将取决于用户的 PATH 变量中首先列出的目录。

在执行环境中操作变量的最常见要求之一是将一个或多个自定义目录添加到路径搜索变量,例如 Linux 或 POSIX 系统上的 PATH,或 Windows 上的 %PATH%,以便本地安装的编译器或当 SCons 尝试执行它以更新目标时,可以找到其他实用程序。 SCons 提供了 env.PrependENVPath 和 env.AppendENVPath 函数来方便地向执行变量添加东西。您可以通过指定要将值添加到的变量,然后指定值本身来调用这些函数。因此,要将一些 /usr/local 目录添加到 $PATH 和 $LIB 变量中,您可以

env = Environment(ENV=os.environ.copy())
env.PrependENVPath('PATH', '/usr/local/bin')
env.AppendENVPath('LIB', '/usr/local/lib')

请注意,添加的值是字符串,如果要将多个目录添加到 $PATH 之类的变量中,则必须在字符串中包含路径分隔符(:在 Linux 或 POSIX 上,; 在 Windows 上,或使用 os.pathsep可移植性)。

使用外部工具的工具路径

通常在构造环境中使用工具时,默认情况下会检查几个不同的搜索位置。这包括作为 scons 发行版一部分的 SCons/Tools/ 目录和相对于根 SConstruct 文件的目录 site_scons/site_tools

# Builtin tool or tool located within site_tools
env = Environment(tools=['SomeTool'])
env.SomeTool(targets, sources)
# The search locations would include by default
SCons/Tool/SomeTool.py
SCons/Tool/SomeTool/__init__.py
./site_scons/site_tools/SomeTool.py
./site_scons/site_tools/SomeTool/__init__.py

在某些情况下,您可能希望指定不同的位置来搜索工具。 Environment 函数包含一个称为工具路径的选项这可用于添加其他搜索目录

# Tool located within the toolpath directory option
env = Environment(
    tools=['SomeTool'],
    toolpath=['/opt/SomeToolPath', '/opt/SomeToolPath2']
)
env.SomeTool(targets, sources)
# The search locations in this example would include:
/opt/SomeToolPath/SomeTool.py
/opt/SomeToolPath/SomeTool/__init__.py
/opt/SomeToolPath2/SomeTool.py
/opt/SomeToolPath2/SomeTool/__init__.py
SCons/Tool/SomeTool.py
SCons/Tool/SomeTool/__init__.py
./site_scons/site_tools/SomeTool.py
./site_scons/site_tools/SomeTool/__init__.py

从 SCons 3.0 开始,Builder 可能位于工具路径的子目录/子包中。这类似于 Python 中的命名空间。对于嵌套或命名空间工具,我们可以使用点符号来指定工具所在的子目录

# namespaced target
env = Environment(
    tools=['SubDir1.SubDir2.SomeTool'],
    toolpath=['/opt/SomeToolPath']
)
env.SomeTool(targets, sources)
# With this example the search locations would include
/opt/SomeToolPath/SubDir1/SubDir2/SomeTool.py
/opt/SomeToolPath/SubDir1/SubDir2/SomeTool/__init__.py
SCons/Tool/SubDir1/SubDir2/SomeTool.py
SCons/Tool/SubDir1/SubDir2/SomeTool/__init__.py
./site_scons/site_tools/SubDir1/SubDir2/SomeTool.py
./site_scons/site_tools/SubDir1/SubDir2/SomeTool/__init__.py

如果我们想访问可通过 sys.path 找到的 scons 外部的工具(例如,通过 Python 的 pip 包管理器安装的工具),可以将 sys.path 与工具路径一起使用。使用这种方法需要注意的一件事是 sys.path 有时可能包含 .egg 文件的路径而不是目录。所以我们需要用这种方法过滤掉那些

# namespaced target using sys.path within toolpath
searchpaths = []
for item in sys.path:
    if os.path.isdir(item):
        searchpaths.append(item)
env = Environment(
    tools=['someinstalledpackage.SomeTool'],
    toolpath=searchpaths
)
env.SomeTool(targets, sources)

在某些情况下,您可能希望使用位于已安装的外部 pip 包中的工具。这可以通过将 sys.path 与刀具路径一起使用来实现。但是,在这种情况下,您需要为工具名提供前缀以指示它在 sys.path 中的位置

searchpaths = []
for item in sys.path:
    if os.path.isdir(item):
        searchpaths.append(item)
env = Environment(
    tools=['tools_example.subdir1.subdir2.SomeTool'],
    toolpath=searchpaths
)
env.SomeTool(targets, sources)

为了避免在工具名称中使用前缀或过滤目录的 sys.path,我们可以使用 PyPackageDir 函数来定位 python 包的目录。 PyPackageDir 返回一个 Dir 对象,它表示指定为参数的 python 包/模块的目录路径

# namespaced target using sys.path
env = Environment(
    tools=['SomeTool'],
    toolpath=[PyPackageDir('tools_example.subdir1.subdir2')]
)
env.SomeTool(targets, sources)

自动将命令行选项放入其构造变量中

本章描述了构造环境的MergeFlagsParseFlagsParseConfig方法,以及对构造环境的方法的parse_flags关键字参数

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

SCons 构造环境有一个 MergeFlags 方法,可以将传入参数的值合并到构造环境中。如果参数是字典,MergeFlags 会将字典中的每个值视为将传递给命令(例如编译器或链接器)的选项列表。如果一个选项已经存在于构造变量中,MergeFlags 将不会复制它。如果参数是一个字符串,MergeFlags 调用 ParseFlags 方法首先将其爆破到字典中,然后对结果进行操作。

MergeFlags 试图智能地合并选项,知道不同的构造变量可能有不同的需求。将选项合并到名称以 PATH 结尾的任何变量时,MergeFlags 保留选项最左边的匹配项,因为在典型的目录路径列表中,第一个匹配项“获胜”。将选项合并到任何其他变量名称时,MergeFlags 会保留最右边出现的选项,因为在典型的命令行选项列表中,最后出现的“获胜”。

env = Environment()
env.Append(CCFLAGS='-option -O3 -O1')
flags = {'CCFLAGS': '-whatever -O3'}
env.MergeFlags(flags)
print("CCFLAGS:", env['CCFLAGS'])

$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("CPPPATH:", env['CPPPATH'])

请注意 $CPPPATH 的默认值是一个普通的 Python 列表,因此您应该将其值作为传递给 MergeFlags 函数的字典中的列表

如果 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("CCFLAGS:", env['CCFLAGS'])
print("CPPPATH:", env['CPPPATH'])

在上面的组合示例中,ParseFlags 已经将选项排序到它们对应的变量中,并返回一个字典供 MergeFlags 应用于指定构造环境中的构造变量

在创建环境时合并选项:parse_flags 参数

也可以合并来自给定环境调用本身的参数的构造变量值。如果给出了 parse_flags 关键字参数,它的值将以与 MergeFlags 方法相同的方式分配给新环境中的构造变量。这在调用 env.Clone 以及覆盖构建器方法时也有效

env = Environment(parse_flags="-I/opt/include -L/opt/lib -lfoo")
for k in ('CPPPATH', 'LIBPATH', 'LIBS'):
    print("%s:" % k, env.get(k))
env.Program("f1.c")
将编译参数分离到它们的变量中:ParseFlags 函数

SCons 在构建程序时针对不同类型的选项有一个令人眼花缭乱的构造变量数组。

有时您可能不确切知道应该为特定选项使用哪个变量。

SCons 构造环境有一个 ParseFlags 方法,它接受一组典型的命令行选项并将它们分配到适当的构造变量中历史上,它是为了支持 ParseConfig 方法而创建的,因此它专注于 GNU 编译器集合 (GCC) 使用的选项对于 C 和 C++ 工具链。

ParseFlags 返回一个字典,其中包含分布到各自构造变量中的选项。

通常,此字典随后将传递给 MergeFlags 以将选项合并到构造环境中,但如果需要可以编辑字典以提供其他功能。 (请注意,如果不打算编辑标志,则直接使用选项调用 MergeFlags 将避免额外的步骤。)

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")

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")

如果字符串以感叹号 (!) 开头,则该字符串将传递给 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")
查找已安装的库信息:ParseConfig 函数

配置正确的选项来构建程序以使用 POSIX 系统上可用的库(尤其是共享库)可能很复杂。为了帮助解决这种情况,各种名称以 config 结尾的实用程序返回构建和链接这些库所需的 GNU 编译器集合 (GCC) 的命令行选项;例如,可以通过调用名为 lib-config 的实用程序找到使用名为 lib 的库的命令行选项。

最近的约定是这些选项可通过通用 pkg-config 程序获得,提供通用框架、错误处理等,因此包创建者所要做的就是为他的特定包提供字符串集。

SCons 构造变量有一个 ParseConfig 方法,它要求主机系统执行命令,然后根据该命令的输出配置适当的构造变量。这使您可以运行诸如 pkg-config 之类的程序或更具体的实用程序来帮助设置您的构建。

env = Environment()
env['CPPPATH'] = ['/lib/compat']
env.ParseConfig("pkg-config x11 --cflags --libs")
print("CPPPATH:", env['CPPPATH'])

SCons 将执行指定的命令字符串,解析结果标志,并将标志添加到适当的环境变量中

在上面的示例中,SCons 已将包含目录添加到 $CPPPATH(取决于 pkg-config 命令发出的其他标志,其他变量也可能已扩展。)请注意,选项与现有选项合并使用MergeFlags 方法,使每个选项在构造变量中只出现一次

env = Environment()
env.ParseConfig("pkg-config x11 --cflags --libs")
env.ParseConfig("pkg-config x11 --cflags --libs")
print("CPPPATH:", "CPPPATH:", env['CPPPATH'])

控制build输出

创建可用构建配置的一个关键方面是从构建中提供有用的输出,以便其用户可以轻松了解构建正在做什么并获得有关如何控制构建的信息。 SCons 提供了多种控制构建配置输出的方法,以帮助使构建更有用和易于理解

提供build帮助:help函数

能够为用户提供一些描述可用于构建的特定目标、构建选项等的帮助通常非常有用。 SCons 提供了 Help 功能让你指定这个帮助文本

Help("""
Type: 'scons program' to build the production program,
      'scons debug' to build the debug version.
""")

可选地,可以指定附加标志

Help("""
Type: 'scons program' to build the production program,
      'scons debug' to build the debug version.
""", append=True)

(请注意上面使用的 Python 三引号语法,这对于指定多行字符串(如帮助文本)非常方便。)当 SConstruct 或 SConscript 文件包含对 Help 函数的此类调用时,指定的帮助文本将是响应 SCons -h 选项显示

SConscript 文件可能包含对 Help 函数的多次调用,在这种情况下,指定的文本将在显示时连接起来。这允许您将帮助文本拆分到多个 SConscript 文件中。在这种情况下,调用 SConscript 文件的顺序将决定调用帮助函数的顺序,这将决定连接各个文本位的顺序。

当与 AddOption Help("text", append=False) 一起使用时,将破坏与 AddOption() 关联的任何帮助输出。

要保留 AddOption() 的帮助输出,请设置 append=True。

另一种用途是使帮助文本以某个变量为条件。例如,假设当实际在 Windows 上运行时,您只想显示一行关于构建仅 Windows 版本的程序。以下 SConstruct 文件:

env = Environment()
Help("\\nType: 'scons program' to build the production program.\\n")
if env['PLATFORM'] == 'win32':
    Help("\\nType: 'scons windebug' to build the Windows debug version.\\n")

如果 SConstruct 或 SConscript 文件中没有帮助文本,SCons 将恢复显示其描述 SCons 命令行选项的标准列表。只要使用 -H 选项,这个列表也总是显示

控制 SCons 如何打印构建命令:$*COMSTR 变量

有时,为编译目标文件或链接程序(或构建其他目标)而执行的命令可能会变得很长,足以让用户难以区分错误消息或其他重要的构建输出与命令本身。指定用于构建各种类型的目标文件的命令行的所有默认 $*COM 变量都有一个相应的 $*COMSTR 变量,可以将其设置为将在构建目标时显示的替代字符串。

例如,假设您希望 SCons 在编译目标文件时显示“正在编译”消息,在链接可执行文件时显示“正在链接”消息。你可以写一个看起来像的 SConstruct 文件

env = Environment(CCCOMSTR = "Compiling $TARGET",
                  LINKCOMSTR = "Linking $TARGET")
env.Program('foo.c')

SCons 对 $*COMSTR 变量执行完整的变量替换,因此它们可以访问所有标准变量,如 $TARGET $SOURCES 等,以及恰好在用于构建特定的构造环境中配置的任何构造变量目标。

当然,有时能够看到 SCons 将执行构建目标的确切命令仍然很重要。

例如,您可能只需要验证 SCons 是否配置为向编译器提供正确的选项,或者开发人员可能希望剪切并粘贴编译命令以添加一些用于自定义测试的选项。

让用户控制 SCons 是否应打印实际命令行或简短的配置摘要的一种常见方法是在 SConstruct 文件中添加对 VERBOSE 命令行变量的支持。一个简单的配置可能如下所示:

env = Environment()
if ARGUMENTS.get('VERBOSE') != '1':
    env['CCCOMSTR'] = "Compiling $TARGET"
    env['LINKCOMSTR'] = "Linking $TARGET"
env.Program('foo.c')

如果用户在命令行上指定 VERBOSE=1,则通过仅设置适当的 $*COMSTR 变量,用户可以控制 SCons 如何显示这些特定的命令行

% scons -Q VERBOSE=1

在此温馨提醒:许多用于构建的命令成对出现,具体取决于意图是否构建用于共享库的对象。命令字符串反映了这一点,因此可能需要同时设置 CCCOMSTR 和 SHCCCOMSTR 以获得所需的结果。

提供build进度输出:Progress函数

提供良好构建输出的另一个方面是向用户提供有关 SCons 正在做什么的反馈,即使此时没有构建任何内容。当大多数目标已经是最新的时,对于大型构建尤其如此。由于 SCons 可能需要很长时间才能绝对确定每个目标实际上对于许多依赖文件都是最新的,因此用户很容易错误地得出 SCons 已挂起或有一些依赖文件的结论构建的其他问题。

处理这种感觉的一种方法是配置 SCons 打印一些东西让用户知道它在“想什么”。 Progress 函数允许您指定一个字符串,该字符串将为 SCons 在遍历依赖关系图以确定哪些目标是最新的或不是最新的时正在“考虑”的每个文件打印。

Progress('Evaluating $TARGET\\n')
Program('f1.c')
Program('f2.c')

当然,通常您不想将所有这些额外的行都添加到您的构建输出中,因为这会使用户难以发现错误或其他重要消息。显示此进度的更有用的方法可能是将文件名直接打印到用户屏幕,而不是打印构建输出的同一标准输出流,并使用回车符 (\r) 以便每个文件名称在同一行上重新打印。这样的配置看起来像:

Progress('$TARGET\\r',
         file=open('/dev/tty', 'w'),
         overwrite=True)
Program('f1.c')
Program('f2.c')

请注意,我们还为 Progress 函数指定了 overwrite=True 参数,这会导致 SCons 在打印下一个 Progress 字符串之前用空格字符“清除”前一个字符串

如果没有 overwrite=True 参数,较短的文件名不会覆盖它前面较长文件名中的所有字符,因此很难判断输出中的实际文件名是什么.另请注意,我们打开了 /dev/tty 文件以直接访问(在 POSIX 上)用户屏幕。在 Windows 上,等效项是打开 con: 文件名。

另外,重要的是要知道,虽然您可以使用 $TARGET 来替换字符串中的节点名称,但 Progress 函数不会执行一般变量替换(因为不一定像源文件那样在评估节点时涉及构造环境, 例如)。

您还可以为 Progress 函数指定一个字符串列表,在这种情况下,SCons 将依次显示每个字符串。

这可用于通过让 SCons 循环遍历一系列字符串来实现“微调器”

Progress(['-\\r', '\\\\\\r', '|\\r', '/\\r'], interval=5)
Program('f1.c')
Program('f2.c')

请注意,这里我们还使用了 interval= 关键字参数,让 SCons 每五个评估节点仅打印一次新的“spinner”字符串。使用 interval= count,即使像我们上面的例子那样使用 $TARGET 的字符串,也是减少 SCons 打印 Progress 字符串的工作的好方法,同时仍然给用户反馈表明 SCons 仍在评估构建.

最后,您可以通过将 Python 函数(或其他 Python 可调用函数)传递给 Progress 函数来直接控制如何打印每个评估的节点。将为每个评估的节点调用您的函数,从而允许您实现更复杂的逻辑,例如添加计数器:

screen = open('/dev/tty', 'w')
count = 0
def progress_function(node)
    count += 1
    screen.write('Node %4d: %s\\r' % (count, node))
Progress(progress_function)

当然,如果你愿意,你可以完全忽略函数的节点参数,只打印一个计数,或者任何你想要的。

(请注意,这里有一个明显的后续问题:您如何找到将被评估的节点总数,以便您可以告诉用户构建离完成有多近?不幸的是,在一般情况下,没有一个很好的方法,除了让 SCons 评估它的依赖图两次,第一次计算总数,第二次实际构建目标。这是必要的,因为你无法提前知道哪个目标用户实际请求构建。例如,整个构建可能包含数千个节点,但用户可能特别要求只构建一个目标文件。)

打印详细的build状态:GetBuildFailures 函数

与大多数构建工具一样,SCons 在成功时向 shell 返回零状态,在失败时返回非零状态。有时在运行结束时提供有关构建状态的更多信息很有用,例如打印信息性消息、发送电子邮件或呼叫破坏构建的可怜的懒汉

SCons 提供了一个 GetBuildFailures 方法,您可以在 python atexit 函数中使用该方法来获取描述在尝试构建目标时失败的操作的对象列表。如果您使用 -j,则可以有多个。

这是一个简单的例子

import atexit
def print_build_failures():
    from SCons.Script import GetBuildFailures
    for bf in GetBuildFailures():
        print("%s failed: %s" % (bf.node, bf.errstr))
atexit.register(print_build_failures)

atexit.register 调用将 print_build_failures 注册为 atexit 回调,在 SCons 退出之前调用。调用该函数时,它会调用 GetBuildFailures 来获取失败对象的列表。返回对象的详细内容见man page;一些更有用的属性是 .node、.errstr、.filename 和 .command。文件名不一定是与节点相同的文件;节点是错误发生时正在构建的目标,而文件名是实际导致错误的文件或目录。注意:只在构建结束时调用 GetBuildFailures;在任何其他时间调用它是未定义的。

下面是一个更完整的示例,展示了如何将 GetBuildFailures 的每个元素转换为字符串:

# Make the build fail if we pass fail=1 on the command line
if ARGUMENTS.get('fail', 0):
    Command('target', 'source', ['/bin/false'])
def bf_to_str(bf):
    """Convert an element of GetBuildFailures() to a string
    in a useful way."""
    import SCons.Errors
    if bf is None: # unknown targets product None in list
        return '(unknown tgt)'
    elif isinstance(bf, SCons.Errors.StopError):
        return str(bf)
    elif bf.node:
        return str(bf.node) + ': ' + bf.errstr
    elif bf.filename:
        return bf.filename + ': ' + bf.errstr
    return 'unknown failure: ' + bf.errstr
import atexit
def build_status():
    """Convert the build status to a 2-tuple, (status, msg)."""
    from SCons.Script import GetBuildFailures
    bf = GetBuildFailures()
    if bf:
        # bf is normally a list of build failures; if an element is None,
        # it's because of a target that scons doesn't know anything about.
        status = 'failed'
        failures_message = "\\n".join(["Failed building %s" % bf_to_str(x)
                           for x in bf if x is not None])
    else:
        # if bf is None, the build completed successfully.
				status = 'ok'
        failures_message = ''
    return (status, failures_message)
def display_build_status():
    """Display the build status.  Called by atexit.
    Here you could do all kinds of complicated things."""
    status, failures_message = build_status()
    if status == 'failed':
       print("FAILED!!!!")  # could display alert, ring bell, etc.
    elif status == 'ok':
       print("Build succeeded.")
    print(failures_message)
atexit.register(display_build_status)

从命令行控制构建

作为 SConscript 文件的编写者,SCons 为您提供了多种方式,使您(和您的用户)能够控制构建执行。可以在命令行上指定的参数分为三种类型:

选项 Options

命令行选项总是以一个或两个 -(连字符)字符开头。 SCons 为您提供了从 SConscript 文件中检查和设置选项值的方法,以及定义您自己的自定义选项的能力。请参阅下面的第 10.1 节,“命令行选项”。

变量 Variables

任何包含 =(等号)的命令行参数都被视为变量设置,其形式为 variable=value。 SCons 提供对所有命令行变量设置的直接访问、将命令行变量设置应用于构造环境的能力,以及用于配置特定类型变量(布尔值、路径名等)的功能,并自动验证指定的值。请参阅下面的第 10.2 节,“命令行变量=值构建变量”。

目标 Targets

任何不是选项或变量设置的命令行参数(不以连字符开头且不包含等号)都被视为您告诉 SCons 构建的目标。 SCons 提供对指定目标列表的访问,以及从 SConscript 文件中设置默认目标列表的方法。

命令行选项

SCons 有许多控制其行为的命令行选项。 SCons 命令行选项始终以一个或两个连字符 (-) 字符开头

不必每次都指定命令行选项:SCONSFLAGS 环境变量

您可能会发现自己每次运行 SCons 时都使用相同的命令行选项。例如,您可能会发现指定 -j 2 让 SCons 并行运行最多两个构建命令可以节省时间。为避免每次都手动键入 j 2,您可以将外部环境变量 SCONSFLAGS 设置为包含 -j 2 的字符串,以及您希望 SCons 始终使用的任何其他命令行选项。 SCONSFLAGS 是 SCons 本身避免从您正在运行的 shell 中查看环境变量的通常规则的例外。

例如,如果您使用的是 bash 或 zsh 等 POSIX shell,并且您始终希望 SCons 使用 -Q 选项,则可以按如下方式设置 SCONSFLAGS 环境

对于 POSIX 系统上的 csh 样式的 shell,您可以按如下方式设置 SCONSFLAGS 环境变量:

$ setenv SCONSFLAGS "-Q”

要更永久地设置 SCONSFLAGS,您可以将设置添加到 POSIX 系统上的 shell 启动文件中

获取由命令行选项设置的值:GetOption 函数

SCons 提供了 GetOption 函数来获取各种命令行选项设置的值。

GetOption 的一个用例是检查是否指定了 -h 或 --help 选项。通常,SCons 在读取所有 SConscript 文件之前不会打印其帮助文本,因为帮助文本可能已被源代码树层次结构深处的某个附属 SConscript 文件添加。当然,阅读所有 SConscript 文件需要额外的时间。如果您知道您的配置没有在附属 SConscript 文件中定义任何额外的帮助文本,您可以通过使用 GetOption 函数加载附属 SConscript 文件来加快显示命令行帮助,仅当 -h 或 --help 选项具有没有像这样指定

if not GetOption('help'):
    SConscript('src/SConscript', export='env')

通常,传递给 GetOption 函数以获取命令行选项设置值的字符串与“最常见”的长选项名称(以两个连字符开头)相同,但也有一些例外。 SCons 命令行选项列表和用于获取它们的 GetOption 字符串可在下面的第 10.1.4 节“用于获取或设置 SCons 命令行选项值的字符串”部分中找到。

GetOption 可用于检索通过调用 AddOption 定义的选项值。 GetOption 调用必须出现在该选项的 AddOption 调用之后。如果 AddOption 调用提供了一个 dest 关键字参数,一个具有该名称的字符串作为参数传递给 GetOption 的内容,否则它是第一个长选项名称的(可能修改过的)版本

设置命令行选项的值:SetOption 函数

您还可以使用 SetOption 函数从 SConscript 文件中设置 SCons 命令行选项的值。用于设置 SCons 命令行选项值的字符串在下面的第 10.1.4 节“用于获取或设置 SCons 命令行选项值的字符串”部分中可用。

SetOption 函数的一种用途是为 -j 或 --jobs 选项指定一个值,这样您就可以提高并行构建的性能,而无需手动指定该选项。一个复杂的因素是 -j 选项的良好值在某种程度上取决于系统。一个粗略的准则是,您的系统拥有的处理器越多,您希望将 -j 值设置得越高,以便利用 CPU 的数量。

例如,假设您的开发系统的管理员已标准化将 NUM_CPU 环境变量设置为每个系统上的处理器数量。一些用于访问环境变量的 Python 代码和 SetOption 函数提供了适当的灵活性:

import os
num_cpu = int(os.environ.get('NUM_CPU', 2))
SetOption('num_jobs', num_cpu)
print("running with -j %s" % GetOption('num_jobs'))

上面的代码片段将 --jobs 选项的值设置为 NUM_CPU 环境变量中指定的值。 (这是字符串与 from 命令行选项的拼写不同的例外情况之一。

由于历史原因,用于获取或设置 --jobs 值的字符串是 num_jobs。)此示例中的代码出于说明目的打印 num_jobs 值。它使用默认值 2 来提供一些最小的并行性,即使在单处理器系统上也是如此:

但是,如果设置了 NUM_CPU 环境变量,则将其用于默认的作业数

% export NUM_CPU="4" % scons -Q

无论是否设置了 NUM_CPU 环境变量,都会首先使用您在命令行中指定的任何显式 -j 或 --jobs 值

用于获取或设置 SCons 命令行选项值的字符串

您可以传递给 GetOption 和 SetOption 函数的字符串通常对应于第一个长格式选项名称(即名称以两个连字符开头:--),在用下划线替换所有剩余的连字符之后。

当前不支持使用 AddOption 添加的选项使用 SetOption。

完整的字符串列表及其对应的变量如下

String for GetOption and SetOption Command-Line Option(s)
cache_debug --cache-debug
cache_disable --cache-disable
cache_force --cache-force
cache_show --cache-show
clean -c, --clean, --remove
config --config
directory -C, --directory
diskcheck --diskcheck
duplicate --duplicate
file -f, --file, --makefile , --sconstruct
help -h, --help
ignore_errors --ignore-errors
implicit_cache --implicit-cache
implicit_deps_changed --implicit-deps-changed
implicit_deps_unchanged --implicit-deps-unchanged
interactive --interact, --interactive
keep_going -k, --keep-going
max_drift --max-drift
no_exec -n, --no-exec, --just-print, --dry-run, --
recon
no_site_dir --no-site-dir
num_jobs -j, --jobs
profile_file --profile
question -q, --question
random --random
repository -Y, --repository, --srcdir
silent -s, --silent, --quiet
site_dir --site-dir

stack_size --stack-size
taskmastertrace_file --taskmastertrace
warn --warn --warning

添加自定义命令行选项:AddOption 函数

SCons 还允许您使用 AddOption 函数定义自己的命令行选项。 AddOption 函数采用与标准 Python 库模块 optparse 中的 add_option 方法相同的参数。使用 AddOption 函数添加自定义命令行选项后,选项的值(如果有)可立即使用标准 GetOption 函数获得。 GetOption 的参数必须是保存选项的变量的名称。如果指定了 AddOption 的 dest 关键字参数,则该值为变量名。给出。如果未给出,则它是在用下划线替换任何剩余的连字符后给予 AddOption 的第一个长选项名称的名称(不带前导连字符),因为连字符在 Python 标识符名称中是不合法的。

当前不支持使用 AddOption 添加的选项使用 SetOption。

使用此功能的一个有用示例是提供一个 --prefix 来帮助描述安装文件的位置:

AddOption(
    '--prefix',
    dest='prefix',
    type='string',
    nargs=1,
    action='store',
    metavar='DIR',
    help='installation prefix',
)
env = Environment(PREFIX=GetOption('prefix'))
installed_foo = env.Install('$PREFIX/usr/bin', 'foo.in')
Default(installed_foo)

上面的代码使用 GetOption 函数将 $PREFIX 构造变量设置为您使用命令行选项 --prefix 指定的值。因为 $PREFIX 在未初始化时会扩展为空字符串,所以在不带 --prefix 选项的情况下运行 SCons 会将文件安装在 /usr/bin/ 目录中

但是在命令行上指定 --prefix=/tmp/install 会导致文件安装在 /tmp/install/usr/bin/ 目录中

注意

SCons 无法正确解析由空格而不是 = 与长选项分隔的选项参数。虽然 --input=ARG 显然是 opt 后跟 arg,但对于 --input ARG,如果没有说明,则无法判断 ARG 是属于输入选项的参数还是位置参数。 SCons 将位置参数视为可在 SConscript 中使用的命令行构建选项或命令行目标(有关详细信息,请参阅紧随其后的部分)。

因此,必须在 SConscript 处理发生之前收集它们。由于提供解决任何歧义的处理指令的 AddOption 调用发生在 SConscript 中,SCons 无法及时知道以这种方式添加的选项,并且会发生意想不到的事情,例如分配为目标的选项参数和/或由于缺少选项参数。

因此,在调用 scons 时应避免这种使用方式。对于单参数选项,请在命令行中使用 --input=ARG 形式。对于多参数选项(nargs 大于一个),在 AddOption 调用中将 nargs 设置为一个,并且:将选项参数组合成一个带有分隔符的单词,并在您自己的代码中解析结果(请参阅内置 -调试选项,它允许将多个参数指定为单个逗号分隔的单词,例如此类用法);或者通过设置 action='append' 允许多次指定该选项。可以同时支持这两种方法

命令行 variable=value 构建变量

您可能希望通过允许在命令行上指定 variable=value 值来控制构建的各个方面。例如,假设您希望能够通过如下运行 SCons 来构建程序的调试版本

% scons -Q debug=1

SCons 提供了一个 ARGUMENTS 字典,用于存储来自命令行的所有 variable=value 赋值。这允许您根据命令行上的规范修改构建的各个方面。 (请注意,除非您希望始终指定变量,否则您可能希望使用 Python 字典 get 方法,如果命令行上没有指定,它允许您指定要使用的默认值。)以下代码集$CCFLAGS 构造变量以响应在 ARGUMENTS 字典中设置的调试标志

env = Environment()
debug = ARGUMENTS.get('debug', 0)
if int(debug):
    env.Append(CCFLAGS='-g')
env.Program('prog.c')

这导致在命令行上使用 debug=1 时使用 -g 编译器选项

SCons 跟踪用于构建每个目标文件的精确命令行,因此可以确定当调试参数的值发生变化时需要重建目标文件和可执行文件。

ARGUMENTS 字典有两个小缺点。首先,因为它是一本字典,它只能为每个指定的关键字存储一个值,因此只能“记住”命令行上每个关键字的最后设置。如果您希望允许在命令行上为给定关键字指定多个值,这会使 ARGUMENTS 字典不太理想。其次,它不保留指定变量设置的顺序,如果您希望配置以不同的方式响应在命令行中指定构建变量设置的顺序,这是一个问题。

为了满足这些要求,SCons 提供了一个 ARGLIST 变量,让您可以在命令行上直接访问 variable=value 设置,按照它们被指定的确切顺序,并且不会删除任何重复的设置。 ARGLIST 变量中的每个元素本身都是一个包含关键字和设置值的双元素列表,您必须循环遍历或从 ARGLIST 的元素中选择,以以任何适当的方式处理您想要的特定设置为您的配置。例如,以下代码允许您通过在命令行上指定多个 define= 设置来添加到 CPPDEFINES 构造变量:

cppdefines = []
for key, value in ARGLIST:
    if key == 'define':
        cppdefines.append(value)
env = Environment(CPPDEFINES=cppdefines)
env.Object('prog.c')

请注意,ARGLIST 和 ARGUMENTS 变量不会相互干扰,而是提供略微不同的视图来了解您如何在命令行上指定 variable=value 设置。您可以在同一个 SCons 配置中使用这两个变量。一般来说,ARGUMENTS 字典使用起来更方便(因为您可以通过 Python 字典访问来获取变量设置),而 ARGLIST 列表更灵活(因为您可以检查命令行变量设置的特定顺序)给定

控制命令行构建变量

能够使用像 debug=1 这样的命令行构建变量很方便,但是编写特定的 Python 代码来识别每个这样的变量、检查错误并提供适当的消息并将值应用于构造变量可能是一件苦差事.为了帮助解决这个问题,SCons 提供了一个变量类来轻松定义此类构建变量,以及一种将构建变量应用于构建环境的机制。这允许您控制构建变量如何影响构建环境。

例如,假设您希望在构建发布程序时在命令行上设置一个 RELEASE 构造变量,并且应该将此变量的值添加到命令行中适当定义以将值传递给 C 编译器。以下是您可以通过在字典中为 $CPPDEFINES 构造变量设置适当的值来做到这一点

vars = Variables(None, ARGUMENTS)
vars.Add('RELEASE', default=0)
env = Environment(variables=vars, CPPDEFINES={'RELEASE_BUILD': '${RELEASE}'})
env.Program(['foo.c', 'bar.c'])

此 SConstruct 文件首先创建一个 Variables 对象,该对象使用命令行选项字典 ARGUMENTS(vars=Variables(None, ARGUMENTS) 调用)中的值。然后它使用对象的 Add 方法指示可以在命令行上设置 RELEASE 变量,如果不设置默认值为 0。新创建的 Variables 对象被传递给用于创建构造环境的 Environment 调用变量关键字参数。然后,您可以在命令行上设置 RELEASE 构建变量,并在用于从 C 源文件构建每个对象的命令行中显示该变量

历史记录:在旧的 SCons(0.98.1 之前)中,这些构建变量被称为“命令行构建选项”。当时,类被命名为 Options,构造选项的预定义函数被命名为 BoolOption、EnumOption、ListOption、PathOption、PackageOption 和 AddOptions(与下面第 10.2.4 节“预定义构建变量函数”中的当前名称对比) ).您可能会在旧的 SConscript 文件、wiki 页面、博客条目、StackExchange 文章等中遇到这些名称。这些旧名称不再有效,但是用“Variable”代替“Option”允许概念转移到当前的使用模型

为命令行构建变量提供帮助

为了使命令行构建变量最有用,您最好在寻求帮助(运行 scons -h)时提供一些帮助文本来描述可用的变量。您可以手写此文本,但 SCons 提供了一些帮助。变量对象提供了一个 GenerateHelpText 方法来生成描述已添加到其中的各种变量的文本。默认文本包括帮助字符串本身以及其他信息,例如允许的值。 (也可以通过替换 FormatVariableHelpText 方法来自定义生成的文本)

vars = Variables(None, ARGUMENTS)
vars.Add('RELEASE', help='Set to 1 to build for release', default=0)
env = Environment(variables=vars)
Help(vars.GenerateHelpText(env))

当使用 -h 选项时,SCons 现在会显示一些有用的文本

% scons -Q -h

您可以看到帮助输出显示了默认值以及构建变量的当前实际值。

从文件中读取构建变量

能够在命令行上指定构建变量的值很有用,但如果每次运行 SCons 时都必须指定变量,仍然会变得乏味。为了使这更容易,您可以通过在创建变量对象时提供文件名来在本地文件中提供自定义的构建变量设置:

vars = Variables('custom.py')
vars.Add('RELEASE', help='Set to 1 to build for release', default=0)
env = Environment(variables=vars, CPPDEFINES={'RELEASE_BUILD': '${RELEASE}'})
env.Program(['foo.c', 'bar.c'])
Help(vars.GenerateHelpText(env))

然后,您可以通过在 custom.py 文件中设置 RELEASE 变量来控制它

RELEASE = 1

请注意,这个文件实际上是像 Python 脚本一样执行的。现在当你运行 SCons

如果您将 custom.py 的内容更改为RELEASE = 0

vars = Variables('custom.py', ARGUMENTS

选项文件 custom.py 中的值被命令行中指定的值覆盖。

预定义构建变量函数

SCons 提供了许多方便的函数,为各种类型的命令行构建变量提供现成的行为。这些函数都返回一个元组,该元组已准备好传递给 Add 或 AddVariables 方法调用。您当然也可以自由定义自己的行为

True/False 值:BoolVariable 构建变量函数

能够指定一个变量来控制具有 true 或 false 值的简单布尔变量通常很方便。

适应如何表示真值或假值的不同偏好会更加方便。 BoolVariable 函数可以很容易地适应这些常见的 true 或 false 表示。

BoolVariable 函数采用三个参数:构建变量的名称、构建变量的默认值和变量的帮助字符串。然后它返回适当的信息以传递给 Variables 对象的 Add 方法,如下所示

vars = Variables('custom.py')
vars.Add(BoolVariable('RELEASE', help='Set to build for release', default=0))
env = Environment(variables=vars, CPPDEFINES={'RELEASE_BUILD': '${RELEASE}'})
env.Program('foo.c')

有了这个构建变量,现在可以通过将其设置为值 yes 或 t 来启用 RELEASE 变量

% scons -Q RELEASE=yes foo.o

其他等于 true 的值包括 y、1、on 和 all。

相反,现在可以通过将 RELEASE 设置为 no 或 f 来为其赋予假值

% scons -Q RELEASE=no foo.o

选择的单个值:EnumVariable 构建变量函数

假设您希望允许设置一个 COLOR 变量来选择应用程序显示的背景颜色,但您希望将选择限制为一组特定的允许颜色。您可以使用 EnumVariable 函数非常轻松地进行设置,该函数除了变量名、默认值和帮助文本参数外还接受一个 allowed_values 列表

vars = Variables('custom.py')
vars.Add(
    EnumVariable(
        'COLOR',
        help='Set background color',
        default='red',
        allowed_values=('red', 'green', 'blue'),
    )
)
env = Environment(variables=vars, CPPDEFINES={'COLOR': '"${COLOR}"'})
env.Program('foo.c')
Help(vars.GenerateHelpText(env))

您现在可以将 COLOR 构建变量显式设置为任何指定的允许值

但是,重要的是,尝试将 COLOR 设置为不在列表中的值会生成一条错误消息

此示例还可以进一步说明帮助生成:此处的帮助消息不仅采用帮助文本,而且使用从 allowed_values 和 default 收集的信息对其进行扩充

EnumVariable 函数还提供了一种将备用名称映射到允许值的方法。例如,假设您希望将海军一词用作蓝色的同义词。为此,您可以添加一个映射字典,将其键值映射到所需的允许值

vars = Variables('custom.py')
vars.Add(
    EnumVariable(
        'COLOR',
        help='Set background color',
        default='red',
        allowed_values=('red', 'green', 'blue'),
        map={'navy': 'blue'},
    )
)
env = Environment(variables=vars, CPPDEFINES={'COLOR': '"${COLOR}"'})
env.Program('foo.c')

默认情况下,使用 EnumVariable 函数时,允许的值区分大小写

EnumVariable 函数可以接受一个额外的 ignorecase 关键字参数,当设置为 1 时,告诉 SCons 在指定值时允许大小写差异

请注意,值为 1 的 ignorecase 保留提供的大小写拼写,仅忽略匹配的大小写。

如果您希望 SCons 将名称转换为小写,而不考虑用户使用的大小写,请指定 ignorecase 值 2

vars = Variables('custom.py')
vars.Add(
    EnumVariable(
        'COLOR',
        help='Set background color',
        default='red',
        allowed_values=('red', 'green', 'blue'),
        map={'navy': 'blue'},
        ignorecase=2,
    )
)
env = Environment(variables=vars, CPPDEFINES={'COLOR': '"${COLOR}"'})
env.Program('foo.c')

列表中的多个值:ListVariable 构建变量函数

您可能想要控制构建变量的另一种方法是指定一个允许值列表,可以从中选择一个或多个(其中 EnumVariable 只允许选择一个值)。 SCons 通过 ListVariable 函数提供此功能。例如,如果您希望能够将 COLORS 变量设置为一个或多个允许的值:

vars = Variables('custom.py')
vars.Add(
    ListVariable(
        'COLORS', help='List of colors', default=0, names=['red', 'green', 'blue']
    )
)
env = Environment(variables=vars, CPPDEFINES={'COLORS': '"${COLORS}"'})
env.Program('foo.c')

您现在可以指定一个以逗号分隔的允许值列表,这些值将被转换为以空格分隔的列表以传递给构建命令

% scons -Q COLORS=red,blue foo.o

此外,ListVariable 函数允许您指定显式关键字 all 或 none 以分别选择所有允许的值或不选择任何值

当然,非法值仍会生成错误消息

您可以使用最后一个特征作为一种方式来强制至少选择一个有效选项,方法是使用 names 参数指定有效值,然后给出一个不在该列表中的值作为默认参数 - 如果没有给出值,则采用这种方式在命令行上,选择默认值,SCons 出错,因为这是无效的。实际上,该示例是通过使用 0 作为默认值来设置的

您可以使用最后一个特征作为一种方式来强制至少选择一个有效选项,方法是使用 names 参数指定有效值,然后给出一个不在该列表中的值作为默认参数 - 如果没有给出值,则采用这种方式在命令行上,选择默认值,SCons 出错,因为这是无效的。实际上,该示例是通过使用 0 作为默认值来设置的

此技术也适用于 EnumVariable

路径名称:PathVariable 构建变量函数

SCons 提供了一个 PathVariable 函数,可以很容易地创建一个构建变量来控制预期的路径名。例如,如果您需要定义一个预处理器宏来控制配置文件的位置

vars = Variables('custom.py')
vars.Add(
    PathVariable(
        'CONFIG', help='Path to configuration file', default='/etc/my_config'
    )
)
env = Environment(variables=vars, CPPDEFINES={'CONFIG_FILE': '"$CONFIG"'})
env.Program('foo.c')

这允许您根据需要在命令行上覆盖 CONFIG 构建变量

% scons -Q CONFIG=/usr/local/etc/other_config foo.o

默认情况下,PathVariable 检查以确保指定的路径存在,如果不存在则生成错误

PathVariable 提供了许多可用于更改此行为的方法。如果要确保任何指定的路径实际上是文件而不是目录,请使用 PathVariable.PathIsFile 方法作为验证函数:

vars = Variables('custom.py')
vars.Add(
    PathVariable(
        'CONFIG',
        help='Path to configuration file',
        default='/etc/my_config',
        validator=PathVariable.PathIsFile,
    )
)
env = Environment(variables=vars, CPPDEFINES={'CONFIG_FILE': '"$CONFIG"'})
env.Program('foo.c')

相反,要确保任何指定的路径都是目录而不是文件,请使用 PathVariable.PathIsDir 方法作为验证函数

vars = Variables('custom.py')
vars.Add(
    PathVariable(
        'DBDIR',
        help='Path to database directory',
        default='/var/my_dbdir',
        validator=PathVariable.PathIsDir,
    )
)
env = Environment(variables=vars, CPPDEFINES={'DBDIR': '"$DBDIR"'})
env.Program('foo.c')

如果您想确保任何指定的路径都是目录,并且您希望创建目录(如果它不存在),请使用 PathVariable.PathIsDirCreate 方法作为验证函数

vars = Variables('custom.py')
vars.Add(
    PathVariable(
        'DBDIR',
        help='Path to database directory',
        default='/var/my_dbdir',
        validator=PathVariable.PathIsDirCreate,
    )
)
env = Environment(variables=vars, CPPDEFINES={'DBDIR': '"$DBDIR"'})
env.Program('foo.c')

最后,如果您不关心路径是否存在、是文件还是目录,请使用 PathVariable.PathAccept 方法接受您提供的任何路径

vars = Variables('custom.py')
vars.Add(
    PathVariable(
        'OUTPUT',
        help='Path to output file or directory',
        default=None,
        validator=PathVariable.PathAccept,
    )
)
env = Environment(variables=vars, CPPDEFINES={'OUTPUT': '"$OUTPUT"'})
env.Program('foo.c')

启用/禁用路径名称:PackageVariable 构建变量函数

有时您希望对路径名变量提供更多控制,允许使用 yes 或 no 关键字显式启用或禁用它们,此外还允许提供显式路径名。 SCons 提供了 PackageVariable 函数来支持这一点:

vars = Variables("custom.py")
vars.Add(
    PackageVariable("PACKAGE", help="Location package", default="/opt/location")
)
env = Environment(variables=vars, CPPDEFINES={"PACKAGE": '"$PACKAGE"'})
env.Program("foo.c")

当 SConscript 文件使用 PackageVariable 函数时,您仍然可以使用默认值或提供覆盖路径名,但您现在可以将指定变量显式设置为指示应启用包的值(在这种情况下,应使用默认值) 或禁用

一次添加多个命令行构建变量

最后,SCons 提供了一种将多个构建变量一次添加到 Variables 对象的方法。不必多次调用 Add 方法,您可以使用要添加到对象的构建变量调用 AddVariables 方法。每个构建变量都被指定为一个参数元组,或者被指定为对预打包命令行构建变量的预定义函数之一的调用,它返回这样一个元组。请注意,单个元组不能像调用 Add 或其中一个构建变量函数那样采用关键字参数。赋予 AddVariables 的变量顺序无关紧要

vars = Variables()
vars.AddVariables(
    ('RELEASE', 'Set to 1 to build for release', 0),
    ('CONFIG', 'Configuration file', '/etc/my_config'),
    BoolVariable('warnings', help='compilation with -Wall and similiar', default=1),
    EnumVariable(
        'debug',
        help='debug output and symbols',
        default='no',
        allowed_values=('yes', 'no', 'full'),
        map={},
        ignorecase=0,
    ),
    ListVariable(
        'shared',
        help='libraries to build as shared libraries',
        default='all',
        names=list_of_libs,
    ),
    PackageVariable(
        'x11', help='use X11 installed here (yes = search some places)', default='yes'
    ),
    PathVariable('qtdir', help='where the root of Qt is installed', default=qtdir),
)

处理未知的命令行构建变量:UnknownVariables 函数

当然,人类偶尔会在命令行设置中拼错变量名。 SCons 不会为命令行中指定的任何未知变量生成错误或警告,因为它无法可靠地判断给定的“拼写错误”变量是否真的未知以及是否存在潜在问题。毕竟,您可能会直接使用 ARGUMENTS 或 ARGLIST 和 SConscript 文件中的一些 Python 代码来处理参数。

但是,如果您正在使用 Variables 对象来定义一组您希望能够设置的特定命令行构建变量,那么如果指定了一个变量设置,您可能希望提供一条错误消息或您自己的警告不在 Variables 对象已知的已定义变量名列表中。您可以通过调用 Variables 对象的 UnknownVariables 方法来获取 Variables 无法识别的设置

vars = Variables(None)
vars.Add('RELEASE', help='Set to 1 to build for release', default=0)
env = Environment(variables=vars, CPPDEFINES={'RELEASE_BUILD': '${RELEASE}'})
unknown = vars.UnknownVariables()
if unknown:
    print("Unknown variables: %s" % " ".join(unknown.keys()))
    Exit(1)
env.Program('foo.c')

UnknownVariables 方法返回一个字典,其中包含命令行上指定的任何变量的关键字和值,这些变量不在 Variables 对象已知的变量中(从已指定使用 Variables 对象的 Add 方法)。上面的示例检查 UnknownVariables 返回的字典是否为非空,如果是,则打印包含未知变量名称的 Python 列表,然后调用 Exit 函数终止 SCons

当然,您可以以适合您的构建配置的任何方式处理 UnknownVariables 函数返回的字典中的项目,包括仅打印警告消息但不退出、在某处记录错误等。

请注意,您必须延迟 UnknownVariables 的调用,直到您使用 variables= Environment 调用的关键字参数将 Variables 对象应用到构造环境之后:在这种情况发生之前,对象中的变量不会被完全处理

命令行目标

获取命令行目标:COMMAND_LINE_TARGETS 变量

SCons 提供了一个 COMMAND_LINE_TARGETS 变量,可让您获取在命令行上指定的目标列表。您可以使用目标以您希望的任何方式操纵构建。举个简单的例子,假设您想要在构建特定程序时打印提醒。您可以通过检查 COMMAND_LINE_TARGETS 列表中的目标来执行此操作

if 'bar' in COMMAND_LINE_TARGETS:
    print("Don't forget to copy `bar' to the archive!")
Default(Program('foo.c'))
Program('bar.c')

现在,使用默认目标运行 SCons 可以像往常一样工作,但是在命令行上明确指定 bar 目标会生成警告消息

COMMAND_LINE_TARGETS 变量的另一个实际用途可能是通过仅在请求特定目标时读取某些附属 SConscript 文件来加速构建

控制默认目标:Default函数

您可以控制 SCons 默认构建哪些目标——也就是说,当命令行上没有指定目标时。

如前所述,除非您在命令行上明确指定一个或多个目标,否则 SCons 通常会在当前目录中或当前目录下构建每个目标。但是,有时您可能希望指定默认只构建某些程序或某些目录中的程序。您可以使用Default函数执行此操作:

env = Environment()
hello = env.Program('hello.c')
env.Program('goodbye.c')
Default(hello)

这个 SConstruct 文件知道如何构建 hello 和 goodbye 两个程序,但默认只构建 hello 程序

请注意,即使您在 SConstruct 文件中使用 Default 函数,您仍然可以在命令行上显式指定当前目录 (.) 以告诉 SCons 在当前目录中(或以下)构建所有内容

您还可以多次调用 Default 函数,在这种情况下,每次调用都会添加到默认构建的目标列表中

env = Environment()
prog1 = env.Program('prog1.c')
Default(prog1)
prog2 = env.Program('prog2.c')
prog3 = env.Program('prog3.c')
Default(prog3)

或者您可以在对 Default 函数的单次调用中指定多个目标

env = Environment()
prog1 = env.Program('prog1.c')
prog2 = env.Program('prog2.c')
prog3 = env.Program('prog3.c')
Default(prog1, prog3)

默认情况下,最后两个示例中的任何一个都只构建 prog1 和 prog3 程序

您可以将目录列为 Default 的参数

在这种情况下,默认情况下仅构建该目录中的目标

最后,如果由于某种原因你不想默认构建任何目标,你可以使用 Python None 变量:

env = Environment()
prog1 = env.Program('prog1.c')
prog2 = env.Program('prog2.c')
Default(None)

获取默认目标列表:DEFAULT_TARGETS 变量

SCons 提供了一个 DEFAULT_TARGETS 变量,让您可以获取通过调用 Default 函数或方法指定的当前默认目标列表。 DEFAULT_TARGETS 变量与 COMMAND_LINE_TARGETS 变量有两个重要区别。首先,DEFAULT_TARGETS 变量是内部 SCons 节点的列表,因此如果要打印列表元素或查找特定目标名称,则需要将列表元素转换为字符串。您可以通过在列表理解中的元素上调用 str 轻松地做到这一点:

prog1 = Program('prog1.c')
Default(prog1)
print("DEFAULT_TARGETS is %s" % [str(t) for t in DEFAULT_TARGETS])

请记住,对 DEFAULT_TARGETS 列表的所有操作都发生在 SCons 读取 SConscript 文件的第一阶段,如果您在运行 SCons 时不使用 -Q 标志,这一点很明显

其次,DEFAULT_TARGETS 列表的内容会随着对 Default 函数的调用而发生变化,正如您可以从以下 SConstruct 文件中看到的那样

prog1 = Program('prog1.c')
Default(prog1)
print("DEFAULT_TARGETS is now %s" % [str(t) for t in DEFAULT_TARGETS])
prog2 = Program('prog2.c')
Default(prog2)
print("DEFAULT_TARGETS is now %s" % [str(t) for t in DEFAULT_TARGETS])

实际上,这只是意味着您需要注意调用默认函数和引用 DEFAULT_TARGETS 列表的顺序,以确保在添加您期望的默认目标之前不检查列表在其中找到

获取构建目标列表,无论来源如何:BUILD_TARGETS 变量

您已经看到了 COMMAND_LINE_TARGETS 变量,它包含在命令行上指定的目标列表,以及 DEFAULT_TARGETS 变量,它包含通过调用默认方法或函数指定的目标列表。然而,有时您想要一个 SCons 尝试构建的任何目标的列表,而不管这些目标是来自命令行还是来自 Default 调用。您可以手动编写代码,如下所示

if COMMAND_LINE_TARGETS:
    targets = COMMAND_LINE_TARGETS
else:
    targets = DEFAULT_TARGETS

然而,SCons 提供了一个方便的 BUILD_TARGETS 变量,消除了这种手动操作的需要。本质上,BUILD_TARGETS 变量包含命令行目标列表(如果指定了任何命令行目标),如果未指定命令行目标,则它包含通过默认方法或函数指定的目标列表。

因为 BUILD_TARGETS 可能包含 SCons 节点列表,所以如果要打印它们或查找特定目标名称,则必须将列表元素转换为字符串,就像 DEFAULT_TARGETS 列表一样

prog1 = Program('prog1.c')
Program('prog2.c')
Default(prog1)
print("BUILD_TARGETS is %s" % [str(t) for t in BUILD_TARGETS])

请注意 BUILD_TARGETS 的值如何根据是否在命令行上指定目标而变化 - BUILD_TARGETS 仅在没有 COMMAND_LINE_TARGETS 时从 DEFAULT_TARGETS 获取

在其他目录中安装文件:Install Builder

一旦构建了一个程序,将它安装在另一个目录中以供公共使用通常是合适的。您使用 Install 方法安排将程序或任何其他文件复制到目标目录中

env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)

但是请注意,安装文件仍被视为一种文件“构建”。当您记住 SCons 的默认行为是在当前目录中或当前目录下构建文件时,这一点很重要。如果,如上例所示,您要在顶级 SConstruct 文件的目录树之外的目录中安装文件,则必须指定该目录(或更高目录,例如 /),以便在该目录中安装任何内容

但是,记住(并键入)应安装程序(或其他文件)的特定目标目录可能会很麻烦。调用 Default 可用于将目录添加到默认目标列表,无需键入它,但有时您不想在每次构建时都安装。这是别名功能派上用场的地方,例如,允许您创建一个名为 install 的伪目标,它可以扩展到指定的目标目录

env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)
env.Alias('install', '/usr/bin')

然后,这会产生更自然的能力,将程序作为单独的调用安装在其目标位置

在目录中安装多个文件

只需多次调用 Install 函数,即可将多个文件安装到一个目录中

env = Environment()
hello = env.Program('hello.c')
goodbye = env.Program('goodbye.c')
env.Install('/usr/bin', hello)
env.Install('/usr/bin', goodbye)
env.Alias('install', '/usr/bin')

或者,更简洁地,在列表中列出多个输入文件(就像您可以对任何其他构建器所做的那样)

env = Environment()
hello = env.Program('hello.c')
goodbye = env.Program('goodbye.c')
env.Install('/usr/bin', [hello, goodbye])
env.Alias('install', '/usr/bin')

这两个示例中的任何一个都会产生

以不同的名称安装文件

Install 方法在将文件复制到目标目录时保留文件名。如果在复制文件时需要更改文件名,请使用 InstallAs 函数

env = Environment()
hello = env.Program('hello.c')
env.InstallAs('/usr/bin/hello-new', hello)
env.Alias('install', '/usr/bin')
以不同名称安装多个文件

如果您有多个文件都需要使用不同的文件名安装,您可以多次调用 InstallAs 函数,或者作为简写,您可以为目标和源参数提供相同长度的列表

env = Environment()
hello = env.Program('hello.c')
goodbye = env.Program('goodbye.c')
env.InstallAs(['/usr/bin/hello-new',
               '/usr/bin/goodbye-new'],
               [hello, goodbye])
env.Alias('install', '/usr/bin')

在这种情况下,InstallAs 函数同时循环遍历两个列表,并将每个源文件复制到其对应的目标文件名中

安装共享库

如果使用 $SHLIBVERSION 变量集创建共享库,scons 将根据该变量根据需要创建符号链接。要正确安装包含符号链接的此类库,请使用 InstallVersionedLib 函数。

例如,在 Linux 系统上,这条指令

foo = env.SharedLibrary(target="foo", source="foo.c", SHLIBVERSION="1.2.3”

将生成共享库 libfoo.so.1.2.3 以及指向 libfoo.so.1.2.3 的符号链接 libfoo.so 和 libfoo.so.1。您可以使用 SharedLibrary 构建器返回的节点来一次性安装库及其符号链接,而无需单独列出它们

env.InstallVersionedLib(target="lib", source=foo)

在希望安装共享库的系统上,共享库既有指示版本的名称,用于运行时解析,也有普通名称,用于链接时解析,可以使用 InstallVersionedLib 函数。

将根据源库的符号链接生成适用于系统类型的符号链接。

独立于平台的文件系统操作

SCons 提供了许多独立于平台的函数,称为工厂,它们执行常见的文件系统操作,如复制、移动或删除文件和目录,或创建目录。这些函数是工厂,因为它们在调用时不执行操作,它们各自返回一个可以在适当时间执行的 Action 对象

复制文件或目录:Copy工厂

假设您想要安排制作一个文件的副本,并且没有合适的预先存在的构建器。一种方法是将复制操作工厂与命令生成器结合使用

Command("file.out", "[file.in](<http://file.in/>)", Copy("$TARGET", "$SOURCE"))

请注意,Copy 工厂返回的操作将在构建 file.out 时扩展 $TARGET 和 $SOURCE 字符串,并且参数的顺序与构建器本身的顺序相同——即首先是目标,其次是来源

当然,您可以显式命名文件,而不是使用 $TARGET 或 $SOURCE

Command("file.out", [], Copy("$TARGET", "[file.in](<http://file.in/>)"))

当您在传递给命令构建器的操作列表中使用复制工厂时,它的用处将变得更加明显。例如,假设您需要通过一个实用程序运行一个文件,该实用程序只能就地修改文件,并且不能将输入“管道化”到输出。一种解决方案是将源文件复制到一个临时文件名,运行该实用程序,然后将修改后的临时文件复制到目标,复制工厂使此操作变得非常容易

Command(
    "file.out",
    "file.in",
    action=[
        Copy("tempfile", "$SOURCE"),
        "modify tempfile",
        Copy("$TARGET", "tempfile"),
    ],
)

Copy 工厂有第三个可选参数,它控制如何复制符号链接

# Symbolic link shallow copied as a new symbolic link:
Command("LinkIn", "LinkOut", Copy("$TARGET", "$SOURCE"[, True]))
# Symbolic link target copied as a file or directory:
Command("LinkIn", "FileOrDirectoryOut", Copy("$TARGET", "$SOURCE", False))
删除文件或目录:Delete工厂

如果您需要删除一个文件,那么删除工厂的使用方式与复制工厂的使用方式大致相同。例如,如果我们想在复制之前确保上一个示例中的临时文件不存在,我们可以将 Delete 添加到命令列表的开头

Command(
    "file.out",
    "file.in",
    action=[
        Delete("tempfile"),
        Copy("tempfile", "$SOURCE"),
        "modify tempfile",
        Copy("$TARGET", "tempfile"),
    ],
)

当然,与所有这些 Action 工厂一样,Delete 工厂也适当地扩展了 $TARGET 和 $SOURCE 变量。例如

Command(
    "file.out",
    "file.in",
    action=[
        Delete("$TARGET"),
        Copy("$TARGET", "$SOURCE"),
    ],
)

但是请注意,您通常不需要以这种方式显式调用 Delete 工厂;默认情况下,SCons 会在执行任何操作之前为您删除其目标。

关于使用 Delete 工厂的一个警告:它具有与任何其他工厂相同的可用变量扩展,包括 $SOURCE 变量。指定 Delete("$SOURCE") 不是您通常想要做的事情!

移动(重命名)文件或目录:Move工厂

Move 工厂允许您重命名文件或目录。例如,如果我们不想复制临时文件,我们可以使用

Command(
    "file.out",
    "file.in",
    action=[
        Copy("tempfile", "$SOURCE"),
        "modify tempfile",
        Move("$TARGET", "tempfile"),
    ],
)
更新文件的修改时间:Touch工厂

如果你只是需要更新一个文件的记录修改时间,使用 Touch factory

Command(
    "file.out",
    "file.in",
    action=[
        Copy("$TARGET", "$SOURCE"),
        Touch("$TARGET"),
    ]
)
创建目录:Mkdir 工厂

如果需要创建目录,请使用 Mkdir 工厂。例如,如果我们需要处理一个临时目录中的文件,处理工具将在其中创建我们不关心的其他文件,您可以使用

Command(
    "file.out",
    "file.in",
    action=[
        Delete("tempdir"),
        Mkdir("tempdir"),
        Copy("tempdir/${SOURCE.file}", "$SOURCE"),
        "process tempdir",
        Move("$TARGET", "tempdir/output_file"),
        Delete("tempdir"),
    ],
)
更改文件或目录权限:Chmod 工厂

要更改文件或目录的权限,请使用 Chmod 工厂。权限参数使用 POSIX 风格的权限位,通常应表示为八进制数,而不是十进制数

Command(
    "file.out",
    "file.in",
    action=[
        Copy("$TARGET", "$SOURCE"),
        Chmod("$TARGET", 0o755),
    ]
)
立即执行一个动作:Execute Function

我们一直在向您展示如何在 Command 函数中使用 Action 工厂。您还可以在使用 Execute 函数读取 SConscript 文件时执行工厂返回的 Action(或者实际上是任何 Action)。例如,如果我们需要在构建任何目标之前确保目录存在

Execute(Mkdir('/tmp/my_temp_directory'))

请注意,这将在读取 SConscript 文件时创建目录

如果您熟悉 Python,您可能想知道为什么要使用它而不是仅仅调用本机 Python os.mkdir() 函数。这里的优点是如果用户指定 SCons 的 -n 或 -q 选项——也就是说,当指定 -n 时它将打印操作但不实际创建目录,或者当指定 -q 时创建目录但不打印操作。

Execute 函数返回正在执行的基础操作的退出状态或返回值。如果操作失败并返回非零值,它还会打印一条错误消息。但是,如果操作失败,SCons 不会真正停止构建。如果您希望构建停止以响应由 Execute 调用的操作失败,您必须通过显式检查返回值并调用 Exit 函数(或 Python 等效函数)来实现:

if Execute(Mkdir('/tmp/my_temp_directory')):
    # A problem occurred while making the temp directory.
    Exit(1)

控制目标的移除

有两种情况下,SCons会默认删除目标文件。

第一种是当SCons确定一个目标文件需要重建,并在执行前删除现有的目标版本时。这些行为可以分别用Precious和NoClean函数来抑制

在构建过程中防止目标移除:Precious函数

默认情况下,SCons在构建目标之前会将其删除。然而,有时候,这并不是你想要的。例如,你可能想渐进地更新一个库,而不是让它被删除,然后从所有的组成对象文件中重建。在这种情况下,你可以使用Precious方法来防止SCons在构建目标之前将其删除。

env = Environment(RANLIBCOM='')
  lib = env.Library('foo', ['f1.c', 'f2.c', 'f3.c'])
  env.Precious(lib)

虽然输出结果看起来没有什么不同,但事实上,SCons在重建目标库之前并没有删除它

在clean过程中防止目标移除:NoClean功能

默认情况下,SCons在调用-c选项来清理源码树上的构建目标时,会删除所有的构建目标。

然而,有时候,这并不是你想要的。例如,你可能想只删除中间生成的文件(如对象文件),但不触动最终目标(库)。在这种情况下,你可以使用NoClean方法来防止SCons在清理时删除目标

env = Environment(RANLIBCOM='')
lib = env.Library('foo', ['f1.c', 'f2.c', 'f3.c'])
env.NoClean(lib

注意,libfoo.a没有被列为已删除的文件

在clean过程中删除额外的文件:Clean函数

在使用-c选项时,可能会有一些你想删除的额外文件,但SCons并不知道,因为它们不是正常的目标文件。例如,也许你调用的一个命令创建了一个日志文件,作为建立你想要的目标文件的一部分。你希望日志文件被清理掉,但你不想让SCons知道这个命令 "建立 "了两个文件。

你可以使用Clean函数来安排在使用-c选项时删除其他文件。然而,请注意,Clean函数需要两个参数,第二个参数是你想要清理的额外文件的名称(本例中是foo.log)

t = Command('foo.out', 'foo.in', 'build -o $TARGET $SOURCE')
Clean(t, 'foo.log')

第一个参数是你希望这个额外文件的清洗与之相关的目标。在上面的例子中,我们使用了Command函数的返回值,它代表foo.out目标。现在,每当foo.out目标被-c选项清理时,foo.log文件也将被删除

分层构建

大型软件项目的源代码很少停留在一个单一的目录中,而是几乎总是被划分为一个层次的目录。使用SCons来组织一个大型软件的构建,需要使用SConscript函数创建一个构建脚本的层次结构

SConscript文件

正如我们已经看到的,位于树顶的构建脚本被称为SConstruct。顶层的 SConstruct 文件可以使用 SConscript 函数在构建过程中包含其他的附属脚本。这些附属脚本又可以使用 SConscript 函数在构建中包含其他脚本。按照惯例,这些附属脚本通常被命名为 SConscript。例如,一个顶级的SConstruct文件可能会安排四个附属脚本加入到构建中,如下所示

SConscript(['drivers/display/SConscript',
            'drivers/mouse/SConscript',
            'parser/SConscript',
            'utilities/SConscript'])

在这种情况下,SConstruct文件明确列出了构建中的所有SConscript文件。(然而,请注意,并不是树中的每个目录都有一个SConscript文件)。另外,驱动子目录可能包含一个中间的SConscript文件,在这种情况下,顶层SConstruct文件中的SConscript调用将看起来像

SConscript(['drivers/SConscript',
            'parser/SConscript',
            'utilities/SConscript'])

而驱动程序子目录中的附属SConscript文件将看起来像

SConscript(['display/SConscript',
            'mouse/SConscript'])

你是在顶层SConstruct文件中列出所有的SConscript文件,还是在中间的目录中放置一个附属的SConscript文件,或者使用这两种方案的某种混合,都取决于你和你的软件的需要

路径名称是与SConscript目录相关的

辅助SConscript文件使得创建一个构建层次很容易,因为辅助SConscript文件中的所有文件和目录名称都是相对于SConscript文件所在的目录解释的。通常情况下,这允许包含构建目标文件的指令的SConscript文件与构建目标文件的源文件生活在同一目录下,这样就很容易在添加或删除文件(或进行其他更改)时更新软件的构建方式。

例如,假设我们想在两个独立的目录中构建两个程序prog1和prog2,其名称与程序相同。一个典型的方法是用一个顶层的SConstruct文件来做这件事,比如说

SConscript(['prog1/SConscript', 'prog2/SConscript'])

还有附属的SConscript文件,看起来像这样的

env = Environment()
env.Program('prog1', ['main.c', 'foo1.c', 'foo2.c'])

而这

env = Environment()
env.Program('prog2', ['main.c', 'bar1.c', 'bar2.c'])

然后,当我们在顶层目录中运行SCons时,我们的构建看起来像是

% scons -Q
cc -o prog1/foo1.o -c prog1/foo1.c
cc -o prog1/foo2.o -c prog1/foo2.c
cc -o prog1/main.o -c prog1/main.c
cc -o prog1/prog1 prog1/main.o prog1/foo1.o prog1/foo2.o
cc -o prog2/bar1.o -c prog2/bar1.c
cc -o prog2/bar2.o -c prog2/bar2.c
cc -o prog2/main.o -c prog2/main.c
cc -o prog2/prog2 prog2/main.o prog2/bar1.o prog2/bar2.o

注意以下几点。首先,你可以在多个目录下有相同名称的文件,如上例中的main.c。第二,与Make的标准递归使用不同,SCons停留在顶层目录(SConstruct文件所在的位置),并发出命令,使用从顶层目录到层次结构中的目标和源文件的路径名称

辅助SConscript文件中的顶级相关路径名称

如果您需要使用另一个目录中的文件,有时从顶级 SConstruct 目录指定另一个目录中文件的路径会更方便,即使您在子目录的附属 SConscript 文件中使用该文件也是如此。 您可以告诉 SCons 将路径名解释为相对于顶级 SConstruct 目录,而不是 SConscript 文件的本地目录,方法是在路径名前添加#(井号):

env = Environment()
env.Program('prog', ['main.c', '#lib/foo1.c', 'foo2.c'])

在这个例子中,lib目录位于顶级SConstruct目录的正下方。如果上述SConscript文件在名为src/prog的子目录下,输出结果会是这样的

(注意,lib/foo1.o 对象文件与它的源文件被构建在同一个目录下。参见第15章,分离源文件和构建树。关于如何在不同的子目录下构建对象文件的信息,请参见下面的第15章 分离源文件和构建树:变量目录)

关于顶级相对路径的一些注意事项:

  1. SCons 不关心你在#后面有没有加斜杠。 有些人认为“#/lib/foo1.c”比“#lib/foo1.c”更具可读性,但它们在功能上是等效的。
  2. top-relative语法只被SCons评估,Python语言本身并不理解它。 如果你喜欢使用 print 进行调试,或者编写一个 Python 函数想要
    评估路径。 您可以通过从中创建一个 Node 对象来强制 SCons 评估顶级相对路径:
path = "#/include"
print("path =", path)
print("force-interpreted path =", Entry(path))
绝对路径名称

当然,你总是可以为一个文件指定一个绝对路径名称--例如

env = Environment()
env.Program('prog', ['main.c', '/usr/joe/lib/foo1.c', 'foo2.c'])

(就像与顶部相关的路径名称一样,注意/usr/joe/lib/foo1.o对象文件是在与其源文件相同的目录下构建的。参见第15章,分离源代码和构建树。关于如何在不同的子目录下构建对象文件的信息,请参见下面的第15章,分离源文件和构建树:变量目录

在SConscript文件之间共享环境(和其他变量)

在前面的例子中,每个附属SConscript文件都通过单独调用Environment来创建自己的构建环境。这显然是可行的,但如果每个程序都必须用相同的构造变量来构建,那么在每个附属的SConscript文件中以同样的方式反复初始化独立的构造环境是很麻烦和容易出错的。

SCons支持从SConscript文件中导出变量的能力,这样它们就可以被其他SConscript文件导入,从而允许你在整个构建层次结构中共享共同的初始化值

导出变量

有两种方法可以从SConscript文件中导出变量。第一种方法是调用Export函数。

导出是相当灵活的--在最简单的形式下,你传递给它一个代表变量名称的字符串,然后导出与它的值一起存储

env = Environment()
Export('env')

你可以一次导出一个以上的变量名称

env = Environment()
debug = ARGUMENTS['debug']
Export('env', 'debug')

因为Python标识符不能包含空格,Export假定包含空格的字符串是多个变量名导出的快捷方式,并为你拆分了它

env = Environment()
debug = ARGUMENTS['debug']
Export('env debug')

你也可以传递Export一个值的字典。这种形式允许以不同的名字从当前范围导出一个变量--在这个例子中,foo的值以 "bar "的名字导出

env = Environment()
foo = "FOO"
args = {"env": env, "bar": foo}
Export(args)

Export也将接受关键字风格的参数。这种形式增加了创建导出变量的能力,这些变量实际上并没有在SConscript文件中进行本地设置。当以这种方式使用时,关键是预期的变量名称,而不是像其他形式那样用字符串表示

Export(MODE="DEBUG", TARGET="arm")

这些样式可以混合使用,尽管Python函数的调用语法要求所有非关键字参数在调用中都要在任何关键字参数之前。

Export 函数将变量添加到一个全局位置,其他 SConscript 文件可以从中导入。对Export的调用是累积的。当你调用 Export 时,你实际上是在更新一个 Python 字典,所以导出一个你已经导出的变量是可以的,但是当这样做时,之前的值会丢失。

另一种导出方式是你可以指定一个变量列表作为SConscript函数调用的第二个参数

SConscript('src/SConscript', 'env')

或者(最好是为了可读性)使用出口关键字参数

SConscript('src/SConscript', exports='env')

这些调用只将指定的变量导出到列出的SConscript文件中。你可以在一个列表中指定一个以上的SConscript文件

SConscript(['src1/SConscript',
            'src2/SConscript'], exports='env')

这在功能上等同于用相同的出口参数多次调用SConscript函数,每个SConscript文件一次

导入变量

一旦一个变量从调用的SConscript文件中导出,它就可以通过调用导入函数在其他SConscript文件中使用

Import('env')
env.Program('prog', ['prog.c'])

导入调用使先前定义的env变量在SConscript文件中可用。假设env是一个构建环境,在导入后它可以被用来构建程序、库等。传递构建环境的用例在较大的scons构建中极为常见

与导出函数一样,导入函数可以用多个变量名来调用

Import('env', 'debug')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

在这个例子中,我们拉入了普通的构造环境env,并使用调试变量的值,通过传递给Clone调用来制作一个修改过的副本。

Import函数将(像Export一样)把一个包含白字的字符串分割成独立的变量名

Import('env debug')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

与全局定义相比,导入更倾向于局部定义,因此,如果有一个foo的全局导出,而调用的SConscript已经将foo导出到这个SConscript,导入将找到导出到这个SConscript的foo。

最后,作为一种特殊情况,你可以通过向Import函数提供一个星号来导入所有已经导出的变量

Import('*')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

如果你要处理大量的SConscript文件,这可能比在每个文件中保持任意的导入变量列表要简单得多

从SConscript文件中返回值

有时,你希望能够以某种方式使用附属SConscript文件的信息。例如,假设你想从几个附属SConscript文件建立的对象文件中创建一个库。

你可以通过使用Return函数从附属SConscript文件向调用文件返回数值来实现这一目的。像导入和导出一样,Return接收变量名称的字符串表示,而不是变量名称本身。

例如,如果我们有两个子目录foo和bar,它们应该各自为一个库贡献一个对象文件,我们希望能够这样做,从附属的SConscript调用中收集对象文件。

env = Environment()
Export('env')
objs = []
for subdir in ['foo', 'bar']:
    o = SConscript('%s/SConscript' % subdir)
    objs.append(o)
env.Library('prog', objs)

我们可以通过在foo/SConscript文件中使用Return函数来做到这一点,就像这样

Import('env')
obj = env.Object('foo.c')
Return('obj')

(相应的bar/SConscript文件应该很明显。)然后当我们运行SCons时,来自附属子目录的对象文件都被正确地归档到所需的库中

分离源代码和构建树:变体目录

将任何构建的文件与源文件完全分开通常很有用。考虑一下您是否有一个为各种不同的控制器硬件构建软件的项目。这些板能够共享大量代码,因此将它们保存在同一源代码树中是有意义的,但源代码和头文件中的某些构建选项不同。如果您首先构建“控制器 A”,然后是“控制器 B”,则在“控制器 B”构建上,所有内容都必须重新构建,因为 SCons 认识到构建指令与“控制器 A”构建中使用的指令不同target - 构建指令是 SCons 过时计算的一部分。现在,当您返回并为“控制器 A”构建时,出于同样的原因,必须再次从头开始重建。但是,如果您可以分隔输出文件的位置,则可以避免此问题。您甚至可以设置为在一次 SCons 调用中执行两个构建。

您可以通过建立一个或多个用于执行构建的变体目录树来启用这种分离,从而为目标文件、库和可执行程序等提供一个独特的家,用于构建的特定风格或变体. SCons 通过路径跟踪目标,因此当包含变体目录时,属于“控制器 A”的对象可以具有与属于“控制器 B”的对象不同的构建指令,而不会触发乒乓重建。

SCons 提供了两种方式来做到这一点,一种是通过我们已经看到的 SConscript 函数,另一种是通过更灵活的 VariantDir 函数。

历史记录:VariantDir 函数过去称为 BuildDir,该名称已被删除,因为 SCons 功能不同于由其他构建系统(如 GNU Autotools)实现的熟悉的“构建目录”模型。您可能仍会在 Internet 上的有关 SCons 的帖子中找到对旧名称的引用,但它不再有效。

将变体目录树指定为 SConscript 调用的一部分

建立变体目录树的最直接方法依赖于这样一个事实,即建立构建层次结构的通常方法是在源子目录中有一个 SConscript 文件。如果将 variant_dir 参数传递给 SConscript 函数调用:

SConscript('src/SConscript', variant_dir='build')

然后 SCons 将构建构建子目录中的所有文件:

src里面没有build文件,他们去到build目录。构建输出可能有点令人惊讶:目标文件 build/hello.o 和可执行文件 build/hello 是在 build 子目录中构建的,正如预期的那样。但是即使我们的 hello.c 文件位于 src 子目录中,SCons 实际上已经编译了一个 build/hello.c 文件来创建目标文件,并且该文件现在可以在 build 中看到。

发生的事情是 SCons 将 hello.c 文件从 src 子目录复制到 build 子目录,并从那里构建程序(它也复制了 SConscript)。下一节将解释 SCons 这样做的原因。

为什么 SCons 在变体目录树中复制源文件

需要了解的重要一点是,当您设置变体目录时,SCons 会在该目录中执行构建。

事实证明,通过就地构建最容易确保构建产品的最终位置。由于构建发生在与源所在位置不同的地方,因此保证正确构建的最直接方法是让 SCons 将它们复制到那里。

在变体目录中复制源文件的最直接原因很简单,一些工具(主要是旧版本)被编写为仅在与源文件相同的目录中构建它们的输出文件。在这种情况下,选择要么在源目录中构建输出文件并将其移动到变体目录,要么在变体目录中复制源文件。

此外,如果我们不只是在变体目录中复制源文件的层次结构,文件之间的相对引用可能会导致问题。您可以在使用带有双引号而不是尖括号的 C 预处理器 #include 机制时看到这一点:

#include "file.h”

在这种情况下,大多数 C 编译器的实际标准行为是首先查找与包含 #include 行的源文件相同的目录,然后查找预处理器搜索路径中的目录。此外,SCons 实现对代码存储库的支持(如下所述)意味着并非所有文件都可以在同一目录层次结构中找到,确保找到正确包含文件的最简单方法是复制源代码文件放入变体目录,无论源文件的原始位置如何,它都提供正确的构建。

尽管源文件复制即使在这些最终情况下也能保证正确构建,但通常可以安全地禁用它。

下一节将介绍如何禁用变体目录中源文件的复制

告诉 SCons 不要在变体目录树中复制源文件

在大多数情况下,对于大多数工具集,SCons 可以将其目标文件放在构建子目录中,而无需复制源文件,一切都会正常进行。您可以通过在调用 SConscript 函数时指定 duplicate=False 来禁用默认的 SCons 行为

SConscript('src/SConscript', variant_dir='build', duplicate=False)

指定此标志后,SCons 会像大多数人期望的那样使用变体目录——也就是说,输出文件放在变体目录中,而源文件留在源目录中

VariantDir 函数

使用 VariantDir 函数确定目标文件应与源文件建立在不同的目录中

VariantDir('build', 'src')
env = Environment()
env.Program('build/hello.c')

请注意,当您不使用 src 子目录中的 SConscript 文件时,您实际上必须指定该程序必须从 SCons 将在构建子目录中复制的 build/hello.c 文件构建。

直接使用VariantDir函数时,SCons默认还是会复制variant目录下的源文件

您可以指定与 SConscript 调用相同的 duplicate=False 参数:

VariantDir('build', 'src', duplicate=False)
env = Environment()
env.Program('build/hello.c')

在这种情况下,SCons 将禁用源文件的复制

将 VariantDir 与 SConscript 文件一起使用

即使在使用 VariantDir 函数时,将它与附属的 SConscript 文件一起使用也更自然,因为这样您就不必调整您的个人构建指令来使用变体目录路径。例如,如果 src/SConscript 看起来像这样

env = Environment()
env.Program('hello.c')

然后我们的 SConstruct 文件看起来像

VariantDir('build', 'src')
SConscript('build/SConscript')

请注意,这完全等同于我们在上一节中了解的 SConscript 的使用

将 Glob 与 VariantDir 一起使用

Glob 文件名模式匹配函数在使用 VariantDir 时与往常一样工作。例如,如果 src/SConscript 看起来像这样

env = Environment()
env.Program('hello', Glob('*.c'))

然后使用与上一节相同的 SConstruct 文件,以及 src 中的源文件 f1.c 和 f2.c,我们将看到以下输出

正如您所期望的,Glob 函数返回 build/ 树中的节点

变体build示例

SConscript 函数的 variant_dir 关键字参数提供了我们需要的一切来展示使用 SCons 创建变体构建是多么容易。例如,假设我们想要为 Windows 和 Linux 平台构建一个程序,但是我们想要在网络共享目录中构建它,并为该程序的 Windows 和 Linux 版本提供单独的并排构建目录.我们必须做一些工作来构建路径,以确保不需要的位置依赖性不会蔓延。顶级相对路径引用在这里很有用。为了避免编写基于平台的条件代码,我们可以动态构建 variant_dir 路径:

platform = ARGUMENTS.get('OS', Platform())
include = "#export/$PLATFORM/include"
lib = "#export/$PLATFORM/lib"
bin = "#export/$PLATFORM/bin"
env = Environment(
    PLATFORM=platform,
    BINDIR=bin,
    INCDIR=include,
    LIBDIR=lib,
    CPPPATH=[include],
    LIBPATH=[lib],
    LIBS='world',
)
Export('env')
env.SConscript('src/SConscript', variant_dir='build/$PLATFORM')

当在 Linux 系统上运行时,这个 SConstruct 文件产生

% scons -Q OS=linux

为了在使用 SConscript 的 variant_dir 参数时一次构建多个变体,您可以重复调用该函数 - 此示例在循环中这样做。请注意,传递脚本文件列表或源目录列表的 SConscript 技巧不适用于 variant_dir,如果使用 variant_dir,SCons 只允许给出一个 SConscript

env = Environment(OS=ARGUMENTS.get('OS'))
for os in ['newell', 'post']:
    SConscript('src/SConscript', variant_dir='build/' + os)

从代码库构建

通常,一个软件项目会有一个或多个中央存储库、包含源代码或派生文件或两者的目录树。通过让 SCons 使用来自一个或多个代码存储库的文件在本地构建树中构建文件,您可以消除额外的不必要的文件重建

存储库方法

允许多个程序员在一个项目上工作以从存储在可集中访问的存储库(源代码树的目录副本)中的源文件和/或派生文件构建软件通常很有用。 (请注意,这不是由 BitKeeper、CVS 或 Subversion 等源代码管理系统维护的那种存储库。)您使用 Repository 方法告诉 SCons 搜索一个或多个中央代码存储库(按顺序)以查找任何源文件以及本地构建树中不存在的派生文件

env = Environment()
env.Program('hello.c')
Repository('/usr/repository1', '/usr/repository2')

多次调用 Repository 方法只会将存储库添加到 SCons 维护的全局列表中,但 SCons 会自动从列表中删除当前目录和任何不存在的目录

在存储库中查找源文件

上面的示例指定 SCons 将首先搜索 /usr/repository1 树下的文件,然后搜索 /usr/repository2 树下的文件。 SCons 期望它搜索的任何文件都可以在相对于顶级目录的相同位置找到。在上面的示例中,如果在本地构建树中找不到 hello.c 文件,SCons 将首先搜索 /usr/repository1/hello.c 文件,然后搜索 /usr/repository2/hello.c 文件以使用在它的地方。

所以给定上面的 SConstruct 文件,如果本地构建目录中存在 hello.c 文件,SCons 将正常重建 hello 程序

然而,如果本地没有 hello.c 文件,但存在于 /usr/repository1 中,SCons 将从它在存储库中找到的源文件重新编译 hello 程序

在存储库中查找#include 文件

我们已经看到 SCons 将扫描源文件的内容以查找 #include 文件名,并意识到从该源文件构建的目标也依赖于 #include 文件。对于 $CPPPATH 列表中的每个目录,SCons 实际上会在任何存储库树中搜索相应的目录,并在存储库目录中找到的任何 #include 文件上建立正确的依赖关系。

但是,除非 C 编译器也知道存储库树中的这些目录,否则它将无法找到 #include 文件。例如,如果我们前面示例中的 hello.c 文件在其当前目录中包含 hello.h,并且 hello.h 仅存在于存储库中:

为了通知 C 编译器有关存储库的信息,SCons 将为 $CPPPATH 列表中的每个目录的编译命令添加适当的 -I 标志。那么如果我们像这样把当前目录添加到构建环境$CPPPATH中

env = Environment(CPPPATH = ['.'])
env.Program('hello.c')
Repository('/usr/repository1')
  • I 选项的顺序为 C 预处理器复制了 SCons 用于其自身依赖性分析的相同存储库目录搜索路径。如果有多个存储库和多个 $CPPPATH 目录,SCons 会将存储库目录添加到每个 $CPPPATH 目录的开头,快速增加 -I 标志的数量。例如,如果 $CPPPATH 包含三个目录(以及更短的存储库路径名!):
env = Environment(CPPPATH = ['dir1', 'dir2', 'dir3'])
env.Program('hello.c')
Repository('/r1', '/r2')

然后我们将在命令行上得到九个 -I 选项,三个(对于每个 $CPPPATH 目录)乘以三(对于本地目录加上两个存储库)

存储库中#include 文件的限制

SCons 依赖于 C 编译器的 -I 选项来控制预处理器在存储库目录中搜索 #include 文件的顺序。但是,这会导致 C 预处理器如何处理带有包含在双引号中的文件名的#include 行的问题。

正如我们所见,如果本地目录中不存在 hello.c 文件,SCons 将从存储库中编译该文件。但是,如果存储库中的 hello.c 文件包含一个带有双引号的文件名的#include 行

#include "hello.h"
int
main(int argc, char *argv[])
{
    printf(HELLO_MESSAGE);
    return (0);
}

然后 C 预处理器将始终首先使用存储库目录中的 hello.h 文件,即使本地目录中有 hello.h 文件,尽管命令行指定 -I 作为第一个选项

C 预处理器的这种行为——总是首先在与源文件相同的目录中搜索双引号中的#include 文件,然后才搜索-I——一般情况下,无法更改。换句话说,如果您想以这种方式使用代码存储库,这是必须忍受的限制。您可以通过三种方式解决此 C 预处理器行为:

  1. 某些现代版本的 C 编译器确实具有禁用或控制此行为的选项。如果是这样,请将该选项添加到构造环境中的 $CFLAGS(或 $CXXFLAGS 或两者)。确保该选项用于所有使用 C 预处理的构建环境!
  2. 将所有出现的#include "file.h" 更改为#include <file.h>。使用带尖括号的#include 不具有相同的行为——首先在-I 目录中搜索#include 文件——这使SCons 可以直接控制C 预处理器将搜索的目录列表。
  3. 要求从存储库进行编译的每个人都检查并处理整个文件目录,而不是单个文件。 (如果您在源代码控制系统的命令周围使用本地包装器脚本,您可以添加逻辑以在那里强制执行此限制。
在存储库中查找 SConstruct 文件

SCons 还将在存储库中搜索 SConstruct 文件和任何指定的 SConscript 文件。但是,这带来了一个问题:如果 SConstruct 文件本身包含有关存储库路径名的信息,SCons 如何在存储库树中搜索 SConstruct 文件?为了解决这个问题,SCons 允许您使用 -Y 选项在命令行上指定存储库目录

在查找源文件或派生文件时,SCons 会先搜索命令行指定的仓库,然后再搜索 SConstruct 或 SConscript 文件中指定的仓库

在存储库中查找派生文件

如果存储库不仅包含源文件,还包含派生文件(例如目标文件、库或可执行文件),SCons 将执行其正常的 MD5 签名计算以确定存储库中的派生文件是否是最新的,或者派生文件必须在本地构建目录中重建。为了使 SCons 签名计算正常工作,存储库树必须包含 SCons 用来跟踪签名信息的 .sconsign 文件。

通常,这将由构建集成商完成,他将在存储库中运行 SCons 以创建其所有派生文件和 .sconsign 文件,或者谁将在单独的构建目录中运行 SCons 并将生成的树复制到所需的存储库

(请注意,即使 SConstruct 文件将 /usr/repository1 列为存储库,这也是安全的,因为 SCons 将从该调用的存储库列表中删除当前构建目录。)现在,填充存储库后,我们只需要创建一个我们目前感兴趣的本地源文件,并使用 -Y 选项告诉 SCons 从存储库中获取它需要的任何其他文件

请注意,SCons 意识到它不需要重建本地副本 file1.o 和 file2.o 文件,而是使用存储库中已编译的文件

保证文件的本地副本

如果存储库树包含构建的完整结果,并且我们尝试从存储库构建而本地树中没有任何文件,则会发生一些令人惊讶的事情

为什么本地build目录下没有hello程序,SCons却说hello程序是最新的?因为存储库(不是本地目录)包含最新的 hello 程序,并且 SCons 正确地确定不需要执行任何操作来重建该文件的最新副本。

然而,很多时候您希望确保文件的本地副本始终存在。例如,打包或测试脚本可能假设某些生成的文件存在于本地。要告诉 SCons 在本地构建目录中复制任何最新的存储库文件,请使用 Local 函数

env = Environment()
hello = env.Program('hello.c')
Local(hello)

如果我们然后运行相同的命令,SCons 将从存储库副本中制作程序的本地副本,并告诉您它正在这样做

扩展 SCons:编写自己的构建器

尽管 SCons 为构建通用软件产品(程序、库、文档等)提供了许多有用的方法,但您经常希望能够构建 SCons 不直接支持的其他类型的文件。

幸运的是,SCons 可以很容易地为您想要构建的任何自定义文件类型定义您自己的 Builder 对象。 (事实上,用于创建 Builder 对象的 SCons 接口非常灵活且易于使用,所有 SCons 内置 Builder 对象都是使用本节中描述的机制创建的)

编写执行外部命令的构建器

要创建的最简单的构建器是执行外部命令的构建器。例如,如果我们想通过名为 foobuild 的命令运行输入文件的内容来构建输出文件,创建该 Builder 可能看起来像

bld = Builder(action='foobuild < $SOURCE > $TARGET')

将构建器附加到构建环境

Builder 对象在附加到构建环境之前是没有用的,这样我们就可以调用它来安排要构建的文件。这是通过环境中的 $BUILDERS 构造变量完成的。 $BUILDERS 变量是一个 Python 字典,它将您要用来调用各种 Builder 对象的名称映射到对象本身。例如,如果我们想调用我们刚刚定义的名为 Foo 的 Builder,我们的 SConstruct 文件可能如下所示

bld = Builder(action='foobuild < $SOURCE > $TARGET')
env = Environment(BUILDERS={'Foo': bld})

将 Builder 附加到我们的名为 Foo 的构建环境中,我们现在可以这样称呼它

env.Foo('file.foo', 'file.input')

但是请注意,构建环境中默认的 $BUILDERS 变量带有一组默认的 Builder 对象,这些对象已经定义:Program、Library 等。而当我们在创建构建环境时显式设置 $BUILDERS 变量时,默认的 Builders不再是环境的一部分

bld = Builder(action='foobuild < $SOURCE > $TARGET')
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file.foo', 'file.input')
env.Program('hello.c')

为了能够在同一构造环境中同时使用我们自己定义的 Builder 对象和默认的 Builder 对象,您可以使用 Append 函数添加到 $BUILDERS 变量:

env = Environment()
bld = Builder(action='foobuild < $SOURCE > $TARGET')
env.Append(BUILDERS={'Foo': bld})
env.Foo('file.foo', 'file.input')
env.Program('hello.c')

或者您可以在 $BUILDERS 字典中明确设置适当命名的键:

env = Environment()
bld = Builder(action='foobuild < $SOURCE > $TARGET')
env['BUILDERS']['Foo'] = bld
env.Foo('file.foo', 'file.input')
env.Program('hello.c')

无论哪种方式,相同的构建环境都可以使用新定义的 Foo Builder 和默认的 Program Builder

让 SCons 处理文件后缀

通过在创建构建器时提供附加信息,您可以让 SCons 为目标和/或源文件添加适当的文件后缀。例如,不必明确指定您希望 Foo Builder 从 file.input 源文件构建 file.foo 目标文件,您可以为 Builder 提供 .foo 和 .input 后缀,从而使更紧凑和对 Foo Builder 的可读调用:

bld = Builder(
    action='foobuild < $SOURCE > $TARGET',
    suffix='.foo',
    src_suffix='.input',
)
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file1')
env.Foo('file2')

如果让 SCons 将前缀附加到目标文件名的开头是合适的,您还可以提供前缀关键字参数

执行 Python 函数的构建器

在 SCons 中,您不必调用外部命令来构建文件。相反,您可以定义一个 Python 函数,Builder 对象可以调用该函数来构建您的目标文件(或多个文件)。这样的构建器函数定义如下:

def build_function(target, source, env):
    # Code to build "target" from "source"
    return None

builder 函数的参数是:

target

一个 Node 对象的列表,表示一个或多个目标将由此函数构建。可以使用 Python str 函数提取这些目标的文件名。

source

一个 Node 对象列表,表示此函数用于构建目标的源。可以使用 Python str 函数提取这些源文件的文件名。

env

用于构建目标的构建环境。该函数可以以任何方式使用任何环境的构造变量来影响它构建目标的方式。

该函数将构造为 SCons FunctionAction,如果目标构建成功,则必须返回 0 或 None 值。该函数可能会引发异常或返回任何非零值以指示构建不成功。

一旦定义了将构建目标文件的 Python 函数,为其定义一个 Builder 对象就像指定函数名称一样简单,而不是外部命令,作为 Builder 的操作参数:

def build_function(target, source, env):
    # Code to build "target" from "source"
    return None
bld = Builder(
    action=build_function,
    suffix='.foo',
    src_suffix='.input',
)
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file')

并注意输出略有变化,反映了现在调用 Python 函数而不是外部命令来构建目标文件的事实

使用生成器创建操作的构建器

SCons Builder 对象可以使用称为生成器的函数“即时”创建操作。 (注意:这与 PEP 255 [https://www.python.org/dev/peps/pep-0255/] 中描述的 Python 生成器函数不同)这提供了很大的灵活性来构造仅用于构建目标的正确命令列表。生成器看起来像:

def generate_actions(source, target, env, for_signature):
    return 'foobuild < %s > %s' % (target[0], source[0])

生成器的参数是:

source

一个 Node 对象列表,表示要由该函数生成的命令或其他操作构建的源。可以使用 Python str 函数提取这些源文件的文件名。

target

一个 Node 对象列表,表示一个或多个目标,这些目标将由命令或此函数生成的其他操作构建。可以使用 Python str 函数提取这些目标的文件名。

env

用于构建目标的构建环境。生成器可以以任何方式使用任何环境的构造变量来确定返回什么命令或其他操作。

for_signature

一个标志,指定生成器是否被调用以贡献构建签名,而不是实际执行命令。

生成器必须返回将用于从指定源构建指定目标的命令字符串或其他操作。

一旦你定义了一个生成器,你就可以创建一个生成器来通过指定生成器关键字参数而不是动作来使用它

def generate_actions(source, target, env, for_signature):
    return 'foobuild < %s > %s' % (source[0], target[0])
bld = Builder(
    generator=generate_actions,
    suffix='.foo',
    src_suffix='.input',
)
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file')
使用发射器修改目标或源列表的构建器

SCons 支持构建器从指定源修改目标列表的能力。为此,您可以定义一个发射器函数,该函数将传递给构建器的目标列表、传递给构建器的源列表以及构造环境作为参数。发射器函数应该返回修改后的应该构建的目标列表和构建目标的来源。

例如,假设您要定义一个始终调用 foobuild 程序的构建器,并且您希望在调用时自动添加一个名为 new_target 的新目标文件和一个名为 new_source 的新源文件。 SConstruct 文件可能如下所示

def modify_targets(target, source, env):
    target.append('new_target')
    source.append('new_source')
    return target, source
bld = Builder(
    action='foobuild $TARGETS - $SOURCES',
    suffix='.foo',
    src_suffix='.input',
    emitter=modify_targets,
)
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file')

您可以做的一件非常灵活的事情是使用构造变量为不同的构造环境指定不同的发射器函数。为此,在调用 Builder 函数时指定一个包含构造变量 expansion 的字符串作为发射器,并在不同的构造环境中将该构造变量设置为所需的发射器函数

bld = Builder(
    action='./my_command $SOURCES > $TARGET',
    suffix='.foo',
    src_suffix='.input',
    emitter='$MY_EMITTER',
)
def modify1(target, source, env):
    return target, source + ['modify1.in']
def modify2(target, source, env):
    return target, source + ['modify2.in']
env1 = Environment(BUILDERS={'Foo': bld}, MY_EMITTER=modify1)
env2 = Environment(BUILDERS={'Foo': bld}, MY_EMITTER=modify2)
env1.Foo('file1')
env2.Foo('file2')

在此示例中,modify1.inmodify2.in 文件被添加到不同命令的源列表中

通过添加发射器修改构建器

定义发射器以与自定义构建器一起使用是一个强大的概念,但有时您真正想要的只是能够使用现有构建器但改变其创建目标的概念。在这种情况下,尝试重新创建现有 Builder 的逻辑以提供特殊发射器可能需要大量工作。典型的情况是当您想要使用导致生成其他文件的编译器标志时。例如,GNU 链接器接受一个选项 Map,它输出一个链接映射到选项参数指定的文件。如果仅将此选项提供给构建,SCons 将不会将链接映射文件视为跟踪目标,这会产生各种不良影响。

为了帮助解决这个问题,SCons 提供了对应于一些标准构建器的构造变量:$PROGEMITTER 用于程序; $LIBEMITTER 用于图书馆; SharedLibrary 的 $SHLIBEMITTER 和 LoadableModule 的 $LDMODULEEMITTER;。将发射器添加到其中之一将导致除了相应构建器的任何现有发射器之外还调用它。

此示例将映射创建添加为链接器标志,并修改标准程序发射器以了解映射生成是一个副作用:

env = Environment()
map_filename = "${TARGET.name}.map"
def map_emitter(target, source, env):
    target.append(map_filename)
    return target, source
env.Append(LINKFLAGS="-Wl,-Map={},--cref".format(map_filename))
env.Append(PROGEMITTER=map_emitter)
env.Program('hello.c')

如果你运行这个例子,添加一个选项来告诉 SCons 转储一些它知道的依赖项的信息,它会显示正在使用的映射文件选项,并且 SCons 确实知道映射文件,这不仅仅是无声的副作用编译器

将自定义生成器和工具放在哪里

site_scons 目录为您提供了一个放置 Python 模块和包的地方,您可以将这些模块和包导入到您的 SConscript 文件中(在顶层),可以集成到 SCons 中的附加工具(在 site_tools 子目录中),以及 site_scons/site_init.py在任何 SConstruct 或 SConscript 文件之前读取的文件,允许您更改 SCons 的默认行为。

每种系统类型(Windows、Mac、Linux 等)都在一组规范的目录中搜索 site_scons;有关详细信息,请参见手册页。顶层 SConstruct 的 site_scons 目录(即项目中的目录)总是最后搜索,并且它的目录位于工具路径中的第一位,因此它会覆盖所有其他目录。

如果您从某个地方(例如 SCons wiki 或第三方)获得了一个工具并且您想在您的项目中使用它,那么 site_scons 目录是放置它的最简单的地方。工具有两种类型;可以是在环境上运行的 Python 函数,也可以是包含两个函数 exists() 和 generate() 的 Python 模块或包。

一个单一功能的工具可以只包含在你的 site_scons/site_init.py 文件中,它将被解析并可供使用。例如,你可以有一个像这样的 site_scons/site_init.py 文件:

def TOOL_ADD_HEADER(env):
    """A Tool to add a header from $HEADER to the source file"""
    add_header = Builder(
        action=['echo "$HEADER" > $TARGET', 'cat $SOURCE >> $TARGET']
    )
    env.Append(BUILDERS={'AddHeader': add_header})
		env['HEADER'] = ''  # set default value

和这样的 SConstruct

# Use TOOL_ADD_HEADER from site_scons/site_init.py
env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====")
env.AddHeader('tgt', 'src')

将调用 TOOL_ADD_HEADER 工具方法以将 AddHeader 工具添加到环境中。

具有 exists() 和 generate() 方法的更成熟的工具可以作为文件 site_scons/site_tools/toolname.py 中的模块或作为目录 site_scons/site_tools/toolname 中的包安装。在使用包的情况下,exists() 和 generate() 位于文件 site_scons/site_tools/toolname/init.py 中。 (在所有上述情况下,工具名都被工具的名称替换。)由于 site_scons/site_tools 自动添加到工具搜索路径的头部,因此在那里找到的任何工具都将可用于所有环境。此外,在那里找到的工具将覆盖同名的内置工具,因此如果您需要更改内置工具的行为,site_scons 会为您提供所需的挂钩。

许多人都有一组实用的 Python 函数,他们希望将它们包含在他们的 SConscript 文件中:只需将它们放在 site_scons/my_utils.py 或您选择的任何有效的 Python 模块名称中。例如,您可以在 site_scons/my_utils.py 中执行类似的操作来添加 build_id 和 MakeWorkDir 函数:

from SCons.Script import *  # for Execute and Mkdir
def build_id():
    """Return a build ID (stub version)"""
    return "100"
def MakeWorkDir(workdir):
    """Create the specified dir immediately"""
    Execute(Mkdir(workdir))

然后在构建中任何地方的 SConscript 或任何子 SConscript 中,您可以导入 my_utils 并使用它

import my_utils
print("build_id=" + my_utils.build_id())
my_utils.MakeWorkDir('/tmp/work')

您可以将此集合放在 site_scons 中它自己的模块中,并像示例中那样导入它,或者您可以将它包含在 site_scons/site_init.py 中,它会自动导入(除非您禁用站点目录)。请注意,为了在除 SConstruct 或 SConscript 之外的任何文件中引用 SCons 命名空间中的对象,例如 Environment 或 Mkdir 或 Execute,您总是需要这样做

from SCons.Script import *

您可以使用任何用户或机器范围的站点目录,例如 ~/.scons/site_scons 而不是 ./site_scons,或者使用 --site-dir 选项指向您自己的目录。 site_init.py 和 site_tools 将位于该目录下。要完全避免使用 site_scons 目录,即使它存在,请使用 --nosite-dir 选项

不编写生成器:命令生成器

当您想要重用操作来构建多个相同类型的文件时,创建构建器并将其附加到构建环境可以提供很大的灵活性。但是,如果您只需要执行一个特定命令来构建单个文件(或一组文件),这可能会很麻烦。对于这些情况,SCons 支持一个命令构建器,它安排执行特定操作以构建一个或多个特定文件。这看起来很像其他构建器(如程序、对象等),但将构建文件要执行的命令作为附加参数

env = Environment()
env.Command('foo.out', 'foo.in', "sed 's/x/y/' < $SOURCE > $TARGET")

执行时,SCons 运行指定的命令,按预期替换 $SOURCE 和 $TARGET

这通常比创建一个 Builder 对象并将其添加到构造环境的 $BUILDERS 变量中更方便。

请注意,您指定给 Command Builder 的操作可以是任何合法的 SCons 操作,例如 Python 函数

env = Environment()
def build(target, source, env):
    # Whatever it takes to build
    return None
env.Command('foo.out', 'foo.in', build)

请注意,$SOURCE 和 $TARGET 也在源和目标中展开,因此您可以编写

env.Command('${SOURCE.basename}.out', '[foo.in](<http://foo.in/>)', build)

它与前面的示例做同样的事情,但可以避免重复自己。

使用 action 关键字来指定操作可能会有所帮助,这是否会让读者更清楚:

env.Command('${SOURCE.basename}.out', '[foo.in](<http://foo.in/>)', action=build)

第 9.2 节“控制 SCons 如何打印构建命令:$*COMSTR 变量”中描述的用于控制构建输出的方法在与预定义的构建器一起使用时效果很好,这些构建器具有用于此目的的预定义 *COMSTR 变量,但事实并非如此调用 Command 时的情况,其中 SCons 事先没有具体的操作知识。如果 Command 的操作参数还不是一个 Action 对象,它会为您构造一个具有合适默认值的对象,其中包括基于操作类型的消息。但是,您也可以自己构建 Action 对象以传递给 Command,这样您就可以进行更多控制。这是上面示例的演变,显示了这种方法

env = Environment()
def build(target, source, env):
    # Whatever it takes to build
    return None
act = Action(build, cmdstr="Building ${TARGET}")
env.Command('foo.out', 'foo.in', action=act)

扩展 SCons:伪构建器和 AddMethod 函数

AddMethod 函数用于向环境添加方法。它通常用于添加一个“伪构建器”,一个看起来像构建器但在调用一个或多个构建器之前包装对多个其他构建器的调用或以其他方式处理其参数的函数。在下面的示例中,我们希望将程序安装到标准的 /usr/bin 目录层次结构中,但也将其复制到本地 install/bin 目录中,可以从中构建包

def install_in_bin_dirs(env, source):
    """Install source in both bin dirs"""
    i1 = env.Install("$BIN", source)
    i2 = env.Install("$LOCALBIN", source)
    return [i1[0], i2[0]] # Return a list, like a normal builder
env = Environment(BIN='/usr/bin', LOCALBIN='#install/bin')
env.AddMethod(install_in_bin_dirs, "InstallInBinDirs")
env.InstallInBinDirs(Program('hello.c')) # installs hello in both bin dirs

如前所述,伪构建器在解析参数方面也比使用构建器更灵活。

下一个示例显示了一个伪构建器,它带有一个修改文件名的命名参数,以及一个用于资源文件的单独参数(而不是让构建器通过文件扩展名来计算)。此示例还演示了使用全局 AddMethod 函数向全局 Environment 类添加一个方法,因此它将在所有后续创建的环境中使用

def BuildTestProg(env, testfile, resourcefile, testdir="tests"):
    """Build the test program;
    prepends "test_" to src and target,
    and puts target into testdir."""
    srcfile = "test_%s.c" % testfile
    target = "%s/test_%s" % (testdir, testfile)
    if env['PLATFORM'] == 'win32':
        resfile = env.RES(resourcefile)
        p = env.Program(target, [srcfile, resfile])
    else:
        p = env.Program(target, srcfile)
    return p
AddMethod(Environment, BuildTestProg)
env = Environment()
env.BuildTestProg('stuff', resourcefile='res.rc')

使用 AddMethod 比仅仅将实例方法添加到构造环境要好,因为它作为正确的方法被调用,并且因为 AddMethod 提供了将方法复制到构造环境实例的任何克隆的功能

扩展 SCons:编写你自己的扫描器

SCons 有内置的扫描器,知道如何在 C/C++、Fortran、D、IDL、LaTeX、Python 和 SWIG 源文件中查找有关从这些文件构建的目标所依赖的其他文件的信息——例如,在这种情况下使用 C 预处理器的文件,在源代码中使用 #include 行指定的 .h 文件。您可以使用 SCons 用于创建其内置扫描器的相同机制来为 SCons 不知道如何“开箱即用”扫描的文件类型编写您自己的扫描器

一个简单的扫描仪示例

例如,假设我们要为 .foo 文件创建一个简单的扫描器。 .foo 文件包含一些将被处理的文本,并且可以在以 include 开头后跟文件名的行中包含其他文件

include filename.foo

扫描文件将由您必须提供的 Python 函数处理。这是一个函数,它将使用 Python re 模块来扫描我们示例中的包含行:

import re
include_re = re.compile(r'înclude\\s+(\\S+)$', re.M)
def kfile_scan(node, env, path, arg):
    contents = node.get_text_contents()
    return env.File(include_re.findall(contents))

重要的是要注意,您必须从扫描仪函数返回一个文件节点列表,文件名的简单字符串是不行的。正如我们在此处展示的示例中一样,您可以使用当前构造环境的文件功能,以便根据具有相对路径的一系列文件名动态创建节点。

扫描器函数必须接受四个指定的参数并返回一个隐式依赖列表。据推测,这些将是通过检查文件内容找到的依赖项,尽管该函数可以执行任何操作来生成依赖项列表

node

表示正在扫描的文件的 SCons 节点对象。可以通过使用 str() 函数将节点转换为字符串来使用文件的路径名,或者可以使用内部 SCons get_text_contents() 对象方法来获取内容。

env

对该扫描有效的构造环境。扫描仪功能可以选择使用来自此环境的构造变量来影响其行为。

path

构成此扫描程序包含文件的搜索路径的目录列表。这就是 SCons 处理 $CPPPATH 和 $LIBPATH 变量的方式。

arg

一个可选参数,您可以选择由各种扫描器实例传递给此扫描器函数。

Scanner 对象是使用 Scanner 函数创建的,该函数通常采用 skeys 参数将文件后缀与该扫描仪相关联。然后 Scanner 对象必须与当前构造环境中的 $SCANNERS 构造变量相关联,通常使用 Append 方法

kscan = Scanner(function=kfile_scan, skeys=['.k'])
env.Append(SCANNERS=kscan)

当我们把它们放在一起时,它看起来像

import re
include_re = re.compile(r'înclude\\s+(\\S+)$', re.M)
def kfile_scan(node, env, path):
    contents = node.get_text_contents()
    includes = include_re.findall(contents)
    return env.File(includes)
kscan = Scanner(function=kfile_scan, skeys=['.k'])
env = Environment(ENV={'PATH': '/usr/local/bin'})
env.Append(SCANNERS=kscan)
env.Command('foo', 'foo.k', 'kprocess < $SOURCES > $TARGET')
将搜索路径添加到扫描仪:FindPathDirs

如果有问题的构建工具将使用路径变量来搜索包含的文件或其他依赖项,则扫描程序也需要考虑该路径变量 - 例如,以这种方式使用 $CPPPATH 和 $LIBPATH。搜索路径作为路径参数传递给您的扫描仪。路径变量可以是节点列表、以分号分隔的字符串,甚至可以包含需要扩展的构造变量。 SCons 提供 FindPathDirs 函数,该函数返回一个可调用对象以扩展给定路径(作为 SCons 构造变量给出名称)到调用扫描器时的路径列表。将评估推迟到那个点允许,例如,包含 $TARGET 引用的路径对于每个扫描的文件都不同。

使用 FindPathDirs 非常简单。继续上面的示例,使用 KPATH 作为带有搜索路径的构造变量(类似于 $CPPPATH),我们只需修改对 Scanner 工厂函数的调用以包含路径关键字 arg:

kscan = Scanner(function=kfile_scan, skeys=['.k'], path_function=FindPathDirs('KPATH'))

FindPathDirs 返回一个可调用对象,当调用该对象时,它将实质上扩展 env['KPATH'] 中的元素并告诉扫描器在这些目录中进行搜索。它还会将相关的存储库和变体目录正确添加到搜索列表中。作为旁注,返回的方法以有效的方式存储路径,因此即使可能需要变量替换,查找也很快。这很重要,因为在典型构建中会扫描许多文件。

将扫描仪与构建器一起使用

将扫描仪引入构建的一种方法是与构建器结合使用。创建构建器时,我们可以使用两个相关的可选参数:source_scanner 和 target_scanner。 source_scanner用于扫描源文件,target_scanner用于扫描生成的目标

import re
include_re = re.compile(r'înclude\\s+(\\S+)$', re.M)
def kfile_scan(node, env, path, arg):
    contents = node.get_text_contents()
    return env.File(include_re.findall(contents))
kscan = Scanner(function=kfile_scan, skeys=['.k'], path_function=FindPathDirs('KPATH')
def build_function(target, source, env):
    # Code to build "target" from "source"
    return None
bld = Builder(
    action=build_function,
    suffix='.foo',
    source_scanner=kscan,
    src_suffix='.input',
)
env = Environment(BUILDERS={'Foo': bld})
env.Foo('file')

触发构建器时,发射器函数可以修改传递给操作函数的源或目标列表。

扫描器功能不会影响构建器在构建操作期间看到的源或目标列表。然而,扫描仪功能将影响构建器是否应该重建(例如,如果扫描仪来源的任何文件发生更改)

多平台配置(Autoconf 功能)

SCons 集成了对构建配置的支持,其风格类似于 GNU Autoconf,但设计为透明的多平台。配置系统可以帮助确定外部构建要求(例如系统库或头文件)在构建系统上是否可用。本节介绍如何使用此 SCons 功能。 (另请参阅 SCons 手册页以获取更多信息)

配置上下文

SCons 中多平台构建配置的基本框架是通过调用 Configure 函数在构建环境中创建一个 configure context,对库、函数、头文件等进行所需的检查,然后调用 configure context 的 Finish 方法完成配置

env = Environment()
conf = Configure(env)
# Checks for libraries, header files, etc. go here!
env = conf.Finish()

Finish 调用是必需的;如果在上下文处于活动状态时创建新上下文,即使在不同的构造环境中,scons 也会抱怨并退出。

SCons 提供了许多预定义的基本检查,以及用于添加您自己的自定义检查的机制。

配置检查失败有几种可能的策略。某些检查可能针对的是您无法继续操作的功能。这里的简单方法就是在此时退出 SCons——本章中的许多示例都是以这种方式编写的。但是,如果有多个硬需求,在任何一个硬需求失败的情况下设置一个标志并累积它们的记录可能对用户更友好,这样在配置上下文完成时它们可以被优先列出导致构建失败——因为必须迭代设置、每次迭代修复一个新需求可能会令人沮丧。其他检查可能是针对您可以不用的功能,这里的策略通常是设置一个构造变量,构建的其余部分可以检查它是否存在/存在,或者设置特定的编译器标志、库列表等。视情况而定,因此您可以根据可用功能适当地继续构建。

请注意,SCons 使用它自己的依赖机制来确定何时需要运行检查——也就是说,SCons 不会在每次调用时都运行检查,而是缓存之前检查返回的值并使用缓存的值,除非某些东西已经变了。这在处理跨平台构建问题时节省了大量开发人员时间。

接下来的部分描述了 SCons 支持的基本检查,以及如何添加您自己的自定义检查

检查头文件是否存在

测试头文件是否存在需要知道头文件是什么语言。此信息在 CheckHeader 方法的语言关键字参数中提供。由于 scons 在 C/C++ 代码的世界中成长,配置上下文也有一个 CheckCHeader 方法,专门检查是否存在 C 头文件:

env = Environment()
conf = Configure(env)
if not conf.CheckCHeader('math.h'):
    print('Math.h must be installed!')
    Exit(1)
if conf.CheckCHeader('foo.h'):
    conf.env.Append(CPPDEFINES='HAS_FOO_H')
env = conf.Finish()

如示例所示,您可以根据情况选择如果给定的头文件不存在则终止构建,也可以根据头文件的存在与否修改构建环境(同样适用于任何其他检查)。如果要检查的元素很多,如果您不在第一次失败时终止,而是跟踪发现的问题直到最后并报告所有问题,这对用户来说可能更友好,这样用户就不必迭代多次,每次找到一个需要安装的新依赖。

如果需要检查 C++ 头文件是否存在,请使用 CheckCXXHeader 方法

env = Environment()
conf = Configure(env)
if not conf.CheckCXXHeader('vector.h'):
    print('vector.h must be installed!')
    Exit(1)
env = conf.Finish()
检查功能的可用性

使用 CheckFunc 方法检查特定函数的可用性:

env = Environment()
conf = Configure(env)
if not conf.CheckFunc('strcpy'):
    print('Did not find strcpy(), using local version')
    conf.env.Append(CPPDEFINES=('strcpy','my_local_strcpy'))
env = conf.Finish()
检查库的可用性

使用 CheckLib 方法检查库的可用性。您只需指定库名称的基本部分,不需要添加 lib 前缀或 .a 或 .lib 后缀:

env = Environment()
conf = Configure(env)
if not conf.CheckLib('m'):
    print('Did not find libm.a or m.lib, exiting!')
    Exit(1)
env = conf.Finish()

因为成功使用库的能力通常取决于能否访问描述库接口的头文件,您可以使用 CheckLibWithHeader 方法同时检查库和头文件:

env = Environment()
conf = Configure(env)
if not conf.CheckLibWithHeader('m', 'math.h', language='c'):
    print('Did not find libm.a or m.lib, exiting!')
    Exit(1)
env = conf.Finish()

这本质上是分别调用 CheckHeader 和 CheckLib 函数的简写

检查 typedef 的可用性

使用 CheckType 方法检查 typedef 的可用性

env = Environment()
conf = Configure(env)
if not conf.CheckType('off_t'):
    print('Did not find off_t typedef, assuming int')
    conf.env.Append(CPPDEFINES=('off_t','int'))
env = conf.Finish()

您还可以添加一个字符串,该字符串将放置在用于检查 typedef 的测试文件的开头。

这提供了一种方法来指定必须包含以查找 typedef 的文件:

env = Environment()
conf = Configure(env)
if not conf.CheckType('off_t', '#include <sys/types.h>\\n'):
    print('Did not find off_t typedef, assuming int')
    conf.env.Append(CPPDEFINES=('off_t','int'))
env = conf.Finish()
检查数据类型的大小

使用 CheckTypeSize 方法检查数据类型的大小

env = Environment()
conf = Configure(env)
int_size = conf.CheckTypeSize('unsigned int')
print('sizeof unsigned int is', int_size)
env = conf.Finish()
检查程序的存在

使用 CheckProg 方法检查程序是否存在

env = Environment()
conf = Configure(env)
if not conf.CheckProg('foobar'):
  print('Unable to find the program foobar on the system')
  Exit(1)
env = conf.Finish()
扩展 SCons:添加您自己的自定义检查

自定义检查是一个 Python 函数,它检查运行系统上是否存在特定条件,通常使用 SCons 提供的方法来处理检查编译是否成功、链接是否成功、程序是否可运行等细节。对特定库是否存在的简单自定义检查可能如下所示:

mylib_test_source_file = """
#include <mylib.h>
int main(int argc, char **argv)
{
    MyLibrary mylib(argc, argv);
    return 0;
}
"""
def CheckMyLibrary(context):
context.Message('Checking for MyLibrary...')
    result = context.TryLink(mylib_test_source_file, '.c')
    context.Result(result)
    return result

Message 和 Result 方法通常应该开始和结束自定义检查,让用户知道发生了什么:Message 调用打印指定的消息(没有尾随换行符),如果检查成功则 Result 调用打印 yes,否则打印 no 't。 TryLink 方法实际测试指定的程序文本是否会成功链接。

(请注意,自定义检查可以根据您选择传递给它的任何参数修改其检查,或者通过使用或修改 context.env 属性中的配置上下文环境。)然后通过传递将此自定义检查功能附加到配置上下文将检查名称映射到基础函数的 Configure 调用的字典

env = Environment()
conf = Configure(env, custom_tests={'CheckMyLibrary': CheckMyLibrary})

您通常希望检查和函数名称相同,就像我们在此处所做的那样,以避免潜在的混淆。

然后我们可以将这些部分放在一起并实际调用 CheckMyLibrary 检查,如下所示:

mylib_test_source_file = """
#include <mylib.h>
int main(int argc, char **argv)
{
    MyLibrary mylib(argc, argv);
    return 0;
}
"""
def CheckMyLibrary(context):
    context.Message('Checking for MyLibrary... ')
    result = context.TryLink(mylib_test_source_file, '.c')
    context.Result(result)
    return result
env = Environment()
conf = Configure(env, custom_tests={'CheckMyLibrary': CheckMyLibrary})
if not conf.CheckMyLibrary():
    print('MyLibrary is not installed!')
    Exit(1)
env = conf.Finish()
# We would then add actual calls like Program() to build
# something using the "env" construction environment.
清理目标时不配置

使用前面部分中描述的多平台配置将运行配置命令,即使在调用 scons -c 来清理目标时也是如此

虽然在删除目标时运行平台检查不会造成任何伤害,但通常是不必要的。您可以通过使用 GetOption 方法检查是否已在命令行上调用 -c(clean)选项来避免这种情况

env = Environment()
if not env.GetOption('clean'):
    conf = Configure(env, custom_tests={'CheckMyLibrary': CheckMyLibrary})
    if not conf.CheckMyLibrary():
        print('MyLibrary is not installed!')
        Exit(1)
    env = conf.Finish()

缓存内置文件

在多开发人员软件项目中,有时您可以通过允许每个开发人员共享他们构建的派生文件的缓存来大大加快每个开发人员的构建速度。毕竟,任何正在进行的更改影响多个派生文件的情况相对较少,大多数将保持不变。使用缓存还可以帮助单个开发人员:例如,如果您希望在干净的树中开始处理新功能,则可以从缓存中检索那些可以重用的构建工件以填充树并节省大量初始构建时间。 SCons 使这变得简单可靠

指定派生文件缓存目录

要启用派生文件的缓存,请在任何 SConscript 文件中使用 CacheDir 函数: CacheDir('/usr/local/build_cache')

您指定的缓存目录必须对所有将访问缓存文件的开发人员具有读写权限(如果--cache-readonly 被使用,只需要读取访问权限)。它还应该位于所有构建都可以访问的某个中心位置。在开发人员使用单独系统(如单独的工作站)进行构建的环境中,此目录通常位于共享或 NFS 安装文件系统上。虽然 SCons 会根据需要创建指定的缓存目录,但在这种多用户场景中,通常最好提前创建它,以便正确设置访问权限。

发生的情况如下:当构建指定了 CacheDir 时,每次构建文件时,它都会存储在由其构建签名索引的缓存目录中。在后续构建中,在调用操作来构建文件之前,会计算构建签名并且 SCons 检查派生文件缓存目录以查看是否已经存在具有完全相同构建签名的文件。如果是这样,派生文件将不会在本地构建,而是从派生文件缓存目录复制到本地构建目录,

请注意,CacheDir 功能需要计算构建签名,即使您将 SCons 配置为使用时间戳来确定文件是否是最新的(有关 Decider 函数的信息,请参阅第 6 章,依赖项一章),因为构建签名是用于确定缓存中是否存在目标文件。因此,使用 CacheDir 可能会减少或抵消使用时间戳进行最新决策带来的任何性能改进

保持构建输出一致

使用派生文件缓存的一个潜在缺点是 SCons 打印的输出在调用与调用之间可能不一致,因为任何给定的文件都可能被重建一次并在下一次从派生文件缓存中检索。这会使分析构建输出变得更加困难,尤其是对于每次都希望输出一致的自动化脚本。

但是,如果您使用 --cache-show 选项,SCons 将打印它为构建文件而执行的命令行,即使它正在从派生文件缓存中检索文件。这使构建输出在构建之间保持一致

当然,代价是您不再知道 SCons 是否已从缓存中检索到派生文件或已在本地重建

不对特定文件使用派生文件缓存

您可能希望禁用配置中某些特定文件的缓存。例如,如果只想将可执行文件放在中央缓存中,而不是中间目标文件,则可以使用 NoCache 函数指定不应缓存目标文件

env = Environment()
obj = env.Object('hello.c')
env.Program('hello.c')
CacheDir('cache')
NoCache('hello.o')

然后当你在清除构建目标后运行 scons 时,它会在本地重新编译目标文件(因为它不存在于派生文件缓存目录中),但仍然意识到派生文件缓存目录包含一个最新的 -可以检索而不是重新链接的日期可执行程序

禁用派生文件缓存

从派生文件缓存中检索已构建的文件通常比重建文件节省大量时间,但节省多少(甚至是否节省时间)在很大程度上取决于您的系统或网络配置。例如,通过繁忙的网络从繁忙的服务器检索缓存文件最终可能比在本地重建文件慢。

在这些情况下,您可以指定 --cache-disable 命令行选项来告诉 SCons 不要从派生文件缓存目录中检索已经构建的文件

使用已构建的文件填充派生文件缓存

有时,您可能已经在本地构建树中构建了一个或多个派生文件,您希望将这些文件提供给其他进行构建的人。例如,您可能会发现在禁用缓存(根据上一节)的情况下执行集成构建更有效,并且仅在集成构建成功完成后才使用构建的文件填充派生文件缓存目录。这样,缓存将只被作为完整、成功构建的一部分的派生文件填满,而不会被稍后在调试集成问题时可能被覆盖的文件填满

在这种情况下,您可以使用 --cache-force 选项告诉 SCons 将所有派生文件放入缓存中,即使这些文件已经存在于您的本地树中,因为它们是由先前的调用构建的

请注意上面的示例运行如何演示 --cache-disable 选项如何避免将构建的 hello.o 和 hello 文件放入缓存中,但是在使用 --cache-force 选项后,文件已放入缓存中下次调用检索

最小化缓存争用:—random 选项

如果您允许多个构建同时更新派生文件缓存目录,那么同时发生的两个构建有时会开始相互“竞争”,以相同的顺序构建相同的文件。例如,如果您将多个文件链接到一个可执行程序中:

Program('prog', ['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c '])

SCons 通常会按照正常的排序顺序构建程序所依赖的输入目标文件

但是如果两个这样的构建同时发生,它们可能几乎同时查看缓存,并且都决定必须重建 f1.o 并将其推入派生文件缓存目录,然后都决定 f2.o 必须是重建(并推入派生文件缓存目录),然后双方都决定必须重建 f3.o...这不会导致任何实际的构建问题——两个构建都会成功,生成正确的输出文件,并填充缓存——但它确实代表了浪费精力。

为了缓解这种缓存争用,您可以使用 --random 命令行选项告诉 SCons 以随机顺序构建依赖项:

使用 --random 选项的多个构建通常会以不同的随机顺序构建它们的依赖项,这最大限度地减少了对派生文件缓存目录中同名文件的大量争用的机会。多个同时构建有时可能仍会争先恐后地尝试构建相同的目标文件,但长时间的低效争用序列应该很少见。

当然,请注意,--random 选项会导致 SCons 打印的输出在每次调用之间不一致,这在尝试比较不同构建运行的输出时可能会出现问题。

如果您想确保以随机顺序构建依赖项而不必在命令行上指定 --random,您可以使用 SetOption 函数在任何 SConscript 文件中设置随机选项

SetOption('random', 1)
Program('prog', ['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c'])
使用自定义 CacheDir 类

SCons 的内部 CacheDir 类可以扩展以支持围绕缓存行为细节的自定义,例如使用压缩缓存文件、加密缓存文件、收集统计信息和数据,或许多其他方面。

要创建您自己的自定义缓存类,您的自定义类必须是 SCons.CacheDir.CacheDir 类的子类。然后,您可以将自定义类传递给 CacheDir 方法,或者在该环境中配置缓存之前将构造变量 $CACHEDIR_CLASS 设置为该类。 SCons 将在执行缓存操作时在内部调用和使用您的自定义类。下面的示例显示了一个简单的用例,该用例覆盖了 copy_from_cache 方法以记录从缓存中提取的字节总数

import SCons
import os
class CustomCacheDir(SCons.CacheDir.CacheDir):
    total_retrieved = 0
    @classmethod
    def copy_from_cache(cls, env, src, dst):
        # record total bytes pulled from cache
        cls.total_retrieved += os.stat(src).st_size
        super().copy_from_cache(env, src, dst)
env = Environment()
env.CacheDir('scons-cache', CustomCacheDir)
# ...