scons user 3.1.2

前言

感谢您抽出时间阅读有关 SCons 的内容。SCons 是一款下一代软件构建工具,或者称为 make 工具,即一种用于构建软件(或其他文件)并在底层输入文件发生更改时使已构建的软件保持最新状态的软件实用程序。

SCons 最显著的特点是其配置文件实际上是用 Python 编程语言编写的脚本。这与大多数其他构建工具形成鲜明对比,后者通常会发明一种新语言来配置构建过程。当然,学习 SCons 仍然需要一定的过程,因为您必须知道调用哪些函数才能正确设置构建过程,但对于任何看过 Python 脚本的人来说,所使用的底层语法应该是熟悉的。

矛盾的是,使用 Python 作为配置文件格式使 SCons 比其他构建工具的晦涩语言更容易被非程序员学习,而这些晦涩语言通常是程序员为其他程序员发明的。这在很大程度上归功于 Python 的一致性和可读性,而这正是 Python 的标志。事实证明,将一种真正的脚本语言作为配置文件的基础,恰好使更有经验的程序员能够根据需要轻松地使用构建工具完成更复杂的任务。

1. SCons 原则

在设计和实现 SCons 时,我们努力遵循以下几个首要原则:

正确性

首先也是最重要的,默认情况下,SCons 保证构建的正确性,即使这意味着要稍微牺牲一些性能。我们致力于确保无论被构建软件的结构如何、编写方式如何,或者用于构建它的工具多么特殊,构建过程都是正确的。

性能

在保证构建正确的前提下,我们努力使 SCons 尽可能快速地构建软件。特别是,在为了保证构建正确性而可能需要减慢 SCons 默认行为的任何地方,我们也尝试通过优化选项使其易于加速,这些选项允许您在所有极端情况下牺牲保证的正确性,以换取通常情况下更快的构建速度。

便利性

SCons 试图在合理的范围内尽可能为您提供开箱即用的功能,包括在您的系统上检测正确的工具并正确使用它们来构建软件。

简而言之,我们努力使 SCons 只需 “做正确的事”,就能正确构建软件,同时尽量减少麻烦。

2. 关于本指南完整性的一点说明

在阅读本指南时,有一点需要注意:与许多开源软件一样,SCons 的文档并不总是与可用功能保持同步更新。换句话说,SCons 能做的很多事情在本用户指南中尚未涵盖。(仔细想想,这也描述了许多专有软件,不是吗?)

尽管本用户指南并不像我们希望的那样完整,但我们的开发过程确实强调确保 SCons 的手册页与新功能保持同步更新。因此,如果您试图弄清楚如何实现 SCons 支持的某项功能,但在此处找不到足够的(或任何)信息,那么查看手册页以确定该信息是否包含在内是值得的。如果您这样做了,也许您甚至可以考虑为用户指南贡献一个章节,这样下一个寻找该信息的人就不必经历同样的过程了……?

3. 致谢

如果没有许多人的帮助,SCons 就不会存在,其中许多人可能甚至没有意识到他们提供了帮助或启发。因此,不分先后顺序,冒着遗漏某人的风险:

首先,SCons 要深深感谢 Bob Sidebotham,他是经典的基于 Perl 的 Cons 工具的原作者,Bob 大约在 1996 年首次将其发布给世界。Bob 在经典 Cons 上的工作提供了底层架构和使用真正的脚本语言指定构建配置的模型。我在 Cons 上的实际工作经验为 SCons 的许多设计决策提供了信息,包括改进的并行构建支持、使用户能够轻松定义 Builder 对象,以及将构建引擎与包装接口分离。

Greg Wilson 在 2000 年 2 月发起 Software Carpentry 设计竞赛时,对将 SCons 作为一个真正的项目启动起到了关键作用。没有那次推动,将经典 Cons 架构的优势与 Python 的可读性相结合可能仅仅停留在一个好主意上。

整个 SCons 团队的合作绝对非常愉快,如果没有过去几年人们贡献的精力、热情和时间,SCons 不可能成为如此有用的工具。Chad Austin、Anthony Roach、Bill Deegan、Charles Crain、Steve Leblanc、Greg Noel、Gary Oberbrunner、Greg Spencer 和 Christoph Wiedemann 组成的 “核心团队” 在审查我的(和其他)更改并在问题进入代码库之前发现问题方面做得非常出色。特别值得注意的技术贡献包括:Anthony 在任务引擎方面出色而创新的工作为 SCons 提供了极其优越的并行构建模型;Charles 是关键 Node 基础设施的大师;Christoph 在 Configure 基础设施方面的工作增加了类似 Autoconf 的关键功能;Greg 为 Microsoft Visual Studio 提供了出色的支持。

特别感谢 David Snopek 贡献了他的底层 “Autoscons” 代码,该代码构成了 Christoph 实现 Configure 功能的基础。考虑到 David 最初在 GPL 下发布该代码,而 SCons 在限制性较小的 MIT 风格许可证下发布,他将此代码提供给 SCons 非常慷慨。

感谢 Peter Miller 开发的出色变更管理系统 Aegis,它从一开始就为 SCons 项目提供了稳健的开发方法,并向我展示了如何将增量回归测试集成到实际开发周期中(早在极限编程出现之前很多年)。

最后,感谢 Guido van Rossum 开发的优雅脚本语言,它不仅是 SCons 实现的基础,也是其接口本身的基础。

4. 联系方式

联系参与 SCons 的人员(包括作者)的最佳方式是通过 SCons 邮件列表。

如果您想询问有关如何使用 SCons 的一般问题,请发送电子邮件至 scons-users@scons.org。

如果您想直接联系 SCons 开发社区,请发送电子邮件至 scons-dev@scons.org。

如果您想接收有关 SCons 的公告,请加入低流量的 announce@scons.tigris.org 邮件列表。

第 1 章 构建和安装 SCons

本章将引导您完成在系统上安装 SCons 的基本步骤,以及在没有预构建包可用(或者只是更喜欢自己构建的灵活性)的情况下构建 SCons 的过程。不过,在此之前,本章还将描述在系统上安装 Python 的基本步骤,以防这是必要的。幸运的是,SCons 和 Python 在几乎任何系统上都非常容易安装,而且许多系统已经预装了 Python。

1.1 安装 Python

由于 SCons 是用 Python 编写的,因此在使用 SCons 之前,您需要在系统上安装 Python。在尝试安装 Python 之前,您应该通过在系统的命令行提示符下输入 python -V(大写的 'V')或 python --version 来检查系统上是否已经有 Python 可用。对于 Linux/Unix/MacOS/BSD 类型的系统,如下所示:

bash

$ python -V
Python 3.7.1

在 Windows 系统的 cmd shell 或 PowerShell 中(注意 PowerShell 需要拼写为 "python.exe" 而不是 "python"):

bash

C:\>python -V
Python 3.7.1

如果系统上未安装 Python,您将看到一条错误消息,例如 "command not found"(在 UNIX 或 Linux 上)或 "‘python’ is not recognized as an internal or external command, operable progam or batch file"(在 Windows 上)。在这种情况下,您需要先安装 Python 才能安装 SCons。

下载和安装 Python 的标准信息位置是Download Python | Python.org。请查看该页面及相关链接以开始安装。

对于 Linux 系统,Python 几乎肯定可以作为受支持的包使用,可能默认已安装;这通常比通过其他方式安装更可取,也比从源代码安装更容易。许多此类系统为 Python 2 和 Python 3 提供单独的包。如果您需要使用的发行版未提供的版本,从源代码构建仍然是一个有用的选择。

SCons 可以与 Python 2.7.x 或 Python 3.5 及更高版本一起使用。如果您需要安装 Python 并且可以选择,我们建议使用可用的最新 Python 版本。较新的 Python 有显著的改进,有助于提高 SCons 的性能。

1.2 安装 SCons

安装 SCons 的标准方法是从 Python 包索引(PyPi)安装:

bash

% python -m pip install scons

如果您不想安装到 Python 系统位置,或者没有这样做的权限,您可以添加一个标志来安装到您自己账户的特定位置:

bash

% python -m pip install --user scons

SCons 已预先打包,可在许多 Linux 系统上安装。检查您的包安装系统,看看是否有可用的 SCons 包。如果可用,许多人更喜欢安装发行版原生包,因为它们提供了一个集中的管理和更新点。一些发行版提供两个 SCons 包,一个使用 Python 2,另一个使用 Python 3。如果您需要的 SCons 特定版本与可用包不同,pip 有一个版本选项,或者您可以按照下一节中的说明操作。

1.3 在任何系统上构建和安装 SCons

如果您的系统没有预构建的 SCons 包,并且使用 pip 安装不合适,那么您仍然可以使用原生 Python distutils 包轻松构建和安装 SCons。

第一步是从 SCons 下载页面http://www.scons.org/download.html下载 scons-3.1.2.tar.gz 或 scons-3.1.2.zip。

使用 Linux 或 UNIX 上的 tar 等实用程序,或 Windows 上的 WinZip 等实用程序解压缩下载的归档文件。这将创建一个名为 scons-3.1.2 的目录,通常在您的本地目录中。然后将工作目录更改为该目录,并执行以下命令安装 SCons:

bash

# cd scons-3.1.2
# python setup.py install

这将构建 SCons,将 scons 脚本安装到用于运行 setup.py 的 Python 的 scripts 目录(/usr/local/bin 或 C:\Python27\Scripts),并将 SCons 构建引擎安装到所用 Python 的相应库目录(/usr/local/lib/scons 或 C:\Python27\scons)。因为这些是系统目录,所以您可能需要 root 权限(在 Linux 或 UNIX 上)或管理员权限(在 Windows 上)才能以这种方式安装 SCons。

1.3.1 并排构建和安装多个版本的 SCons

SCons 的 setup.py 脚本有一些扩展,支持在并排位置轻松安装多个版本的 SCons。例如,这使得在将正式构建过程迁移到新版本之前,更容易下载和试验不同版本的 SCons。

要在特定版本的位置安装 SCons,请在调用 setup.py 时添加 --version-lib 选项:

bash

# python setup.py install --version-lib

例如,这将把 SCons 构建引擎安装到 /usr/lib/scons-3.1.2 或 C:\Python27\scons-3.1.2 目录中。

如果您第一次安装 SCons 时使用了 --version-lib 选项,则不必在每次安装新版本时都指定它。SCons 的 setup.py 脚本会检测特定版本的目录名称,并假设您希望将所有版本安装到特定版本的目录中。您可以在将来通过显式指定 --standalone-lib 选项来覆盖该假设。

1.3.2 安装 SCons 到其他位置

您可以通过指定 --prefix = 选项将 SCons 安装到默认位置以外的其他位置:

bash

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

这会将 scons 脚本安装到 /opt/scons/bin,将构建引擎安装到 /opt/scons/lib/scons。

请注意,您可以同时指定 --prefix = 和 --version-lib 选项,在这种情况下,setup.py 会将构建引擎安装到相对于指定前缀的特定版本目录中。在上例中添加 --version-lib 会将构建引擎安装到 /opt/scons/lib/scons-3.1.2。

1.3.3 无管理权限构建和安装 SCons

如果您没有权限将 SCons 安装到系统位置,只需使用 --prefix = 选项将其安装到您选择的位置。例如,要将 SCons 安装到相对于用户目录的适当位置,将脚本安装到HOME/bin,将构建引擎安装到 $HOME/lib/scons,只需键入:

bash

$ python setup.py install --prefix=$HOME

当然,您可以指定任何其他您喜欢的位置,如果您想相对于指定前缀安装特定版本的目录,可以使用 --version-lib 选项。

这也可用于试验比系统位置中安装的版本更新的 SCons 版本。当然,您安装较新版本 scons 脚本的位置(上例中的 $HOME/bin)必须在包含系统安装版本 scons 脚本的目录之前配置在您的 PATH 变量中。

第 2 章 简单构建

在本章中,您将看到几个使用 SCons 进行非常简单的构建配置的示例,这些示例将展示使用 SCons 在不同类型的系统上从几种不同的编程语言构建程序是多么容易。

2.1 构建简单的 C/C++ 程序

这是著名的 C 语言 "Hello, World!" 程序:

c

int
main()
{printf("Hello, world!\n");
}

以下是使用 SCons 构建它的方法。在名为 SConstruct 的文件中输入以下内容:

python

运行

Program('hello.c')

这个最小的配置文件为 SCons 提供了两条信息:您想要构建什么(一个可执行程序),以及您想要从哪个输入文件构建它(hello.c 文件)。Program 是一个 builder_method,一个 Python 调用,告诉 SCons 您想要构建一个可执行程序。

就这样。现在运行 scons 命令来构建程序。在像 Linux 或 UNIX 这样符合 POSIX 标准的系统上,您会看到类似以下内容:

bash

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.

在带有 Microsoft Visual C++ 编译器的 Windows 系统上,您会看到类似以下内容:

bash

C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.

首先,请注意您只需要指定源文件的名称,SCons 会正确推断出要从源文件名的基名构建的目标文件和可执行文件的名称。

其次,请注意相同的输入 SConstruct 文件无需任何更改,即可在两个系统上生成正确的输出文件名:在 POSIX 系统上是 hello.o 和 hello,在 Windows 系统上是 hello.obj 和 hello.exe。这是 SCons 使编写可移植软件构建极其容易的一个简单示例。

(注意,在本指南的所有示例中,我们不会同时提供 POSIX 和 Windows 的输出;只需记住,除非另有说明,任何示例都应在两种类型的系统上同样有效。)

2.2 构建目标文件

Program 构建器方法只是 SCons 提供的用于构建不同类型文件的众多构建器方法之一。另一个是 Object 构建器方法,它告诉 SCons 从指定的源文件构建一个目标文件:

python

运行

Object('hello.c')

现在,当您运行 scons 命令来构建程序时,它将在 POSIX 系统上只构建 hello.o 目标文件:

bash

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
scons: done building targets.

在 Windows 系统上(使用 Microsoft Visual C++ 编译器)只构建 hello.obj 目标文件:

bash

C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
scons: done building targets.

2.3 简单的 Java 构建

SCons 也使 Java 构建变得极其容易。但是,与 Program 和 Object 构建器方法不同,Java 构建器方法要求您指定一个目标目录的名称,您希望将类文件放在该目录中,然后指定.java 文件所在的源目录:

python

运行

Java('classes', 'src')

如果 src 目录包含一个单一的 hello.java 文件,那么运行 scons 命令的输出将如下所示(在 POSIX 系统上):

bash

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
javac -d classes -sourcepath src src/hello.java
scons: done building targets.

我们将在第 26 章 "Java 构建" 中更详细地介绍 Java 构建,包括构建 Java 归档文件(.jar)和其他类型的文件。

2.4 构建后清理

使用 SCons 时,无需添加特殊命令或目标名称来在构建后进行清理。相反,您只需在调用 SCons 时使用 - c 或 --clean 选项,SCons 就会删除相应的已构建文件。因此,如果我们构建上面的示例,然后之后调用 scons -c,POSIX 上的输出如下所示:

bash

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.
% scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed hello.o
Removed hello
scons: done cleaning targets.

Windows 上的输出如下所示:

bash

C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.
C:\>scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed hello.obj
Removed hello.exe
scons: done cleaning targets.

请注意,SCons 会更改其输出,告诉您它正在 "Cleaning targets ..." 和 "done cleaning targets"。

2.5 SConstruct 文件

如果您习惯使用像 Make 这样的构建系统,那么您已经知道 SConstruct 文件是 SCons 相当于 Makefile 的文件。也就是说,SConstruct 文件是 SCons 读取以控制构建的输入文件。

2.5.1 SConstruct 文件是 Python 脚本

然而,SConstruct 文件和 Makefile 之间有一个重要区别:SConstruct 文件实际上是一个 Python 脚本。如果您还不熟悉 Python,也不用担心。本用户指南将逐步向您介绍使用 SCons 有效所需了解的相对少量的 Python 知识。而且 Python 非常容易学习。

使用 Python 作为脚本语言的一个方面是,您可以使用 Python 的注释约定在 SConstruct 文件中添加注释;也就是说,'#' 和行尾之间的所有内容都将被忽略:

python

运行

# Arrange to build the "hello" program.
Program('hello.c')    # "hello.c" is the source file.

在本指南的其余部分中,您将看到能够使用真正脚本语言的强大功能可以大大简化解决实际构建中复杂需求的方案。

2.5.2 SCons 函数与顺序无关

SConstruct 文件与普通 Python 脚本不完全相同,而更像 Makefile 的一个重要方面是,在 SConstruct 文件中调用 SCons 函数的顺序不会影响 SCons 实际构建您希望它构建的程序和目标文件的顺序。换句话说,当您调用 Program 构建器(或任何其他构建器方法)时,您并不是告诉 SCons 在调用构建器方法的那一刻构建程序。相反,您是告诉 SCons 构建您想要的程序,例如,一个从名为 hello.c 的文件构建的程序,而何时构建该程序(以及任何其他文件)则由 SCons 决定。(我们将在下面的第 6 章 "依赖关系" 中了解更多关于 SCons 如何决定何时需要构建或重新构建文件的信息。)

SCons 通过打印状态消息来反映调用像 Program 这样的构建器方法与实际构建程序之间的区别,这些消息指示何时 "只是读取" SConstruct 文件,以及何时实际构建目标文件。这是为了明确 SCons 何时执行构成 SConstruct 文件的 Python 语句,以及何时 SCons 实际执行命令或其他操作来构建必要的文件。

让我们通过一个例子来澄清这一点。Python 有一个 print 语句,可以将一串字符打印到屏幕上。如果我们在调用 Program 构建器方法的周围添加 print 语句:

python

运行

print("Calling Program('hello.c')")
Program('hello.c')
print("Calling Program('goodbye.c')")
Program('goodbye.c')
print("Finished calling Program()")

然后当我们执行 SCons 时,我们会在关于读取 SConscript 文件的消息之间看到 print 语句的输出,这表明此时正在执行 Python 语句:

bash

% scons
scons: Reading SConscript files ...
Calling Program('hello.c')
Calling Program('goodbye.c')
Finished calling Program()
scons: done reading SConscript files.
scons: Building targets ...
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.

还要注意,SCons 首先构建了 goodbye 程序,即使 "reading SConscript" 输出显示我们在 SConstruct 文件中首先调用了 Program ('hello.c')。

2.6 减少 SCons 输出的冗长性

您已经看到 SCons 如何打印一些关于它正在做什么的消息,围绕着用于构建软件的实际命令:

bash

C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.

这些消息强调了 SCons 执行工作的顺序:首先读取并执行所有配置文件(通常称为 SConscript 文件),然后才构建目标文件。这些消息的其他好处包括,有助于区分在读取配置文件时发生的错误和在构建目标时发生的错误。

当然,一个缺点是这些消息会使输出变得混乱。幸运的是,在调用 SCons 时使用 - Q 选项可以轻松禁用它们:

bash

C:\>scons -Q
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)

由于我们希望本用户指南专注于 SCons 实际在做什么,因此我们将使用 - Q 选项从本指南所有剩余示例的输出中删除这些消息。

第 3 章 构建中不太简单的事情

在本章中,您将看到几个使用 SCons 进行非常简单的构建配置的示例,这些示例将展示使用 SCons 在不同类型的系统上从几种不同的编程语言构建程序是多么容易。

3.1 指定目标(输出)文件的名称

您已经看到,当您调用 Program 构建器方法时,它会以与源文件相同的基名构建生成的程序。也就是说,以下调用从 hello.c 源文件构建可执行程序,将在 POSIX 系统上构建名为 hello 的可执行程序,在 Windows 系统上构建名为 hello.exe 的可执行程序:

python

运行

Program('hello.c')

如果您想构建一个名称与源文件名的基名不同的程序,只需将目标文件名放在源文件名的左侧:

python

运行

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

(SCons 要求目标文件名在前,源文件名在后,这样顺序就模仿了大多数编程语言(包括 Python)中的赋值语句:"program = source files"。)

现在,在 POSIX 系统上运行时,SCons 将构建一个名为 new_hello 的可执行程序:

bash

% scons -Q
cc -o hello.o -c hello.c
cc -o new_hello hello.o

在 Windows 系统上,SCons 将构建一个名为 new_hello.exe 的可执行程序:

bash

C:\>scons -Q
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:new_hello.exe hello.obj
embedManifestExeCheck(target, source, env)

3.2 编译多个源文件

您刚刚看到了如何配置 SCons 从单个源文件编译程序。当然,更常见的情况是,您需要从许多输入源文件而不是一个源文件构建程序。为此,您需要将源文件放在 Python 列表中(用方括号括起来),如下所示:

python

运行

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

上述示例的构建过程如下所示:

bash

% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o prog prog.o file1.o file2.o

请注意,SCons 从列表中指定的第一个源文件推断输出程序名称;也就是说,因为第一个源文件是 prog.c,SCons 将生成的程序命名为 prog(或在 Windows 系统上为 prog.exe)。如果您想指定不同的程序名称,那么(如我们在上一节中所见)您将源文件列表向右滑动,为输出程序文件名腾出空间。(SCons 将输出文件名放在源文件名的左侧,以便顺序模仿赋值语句:"program = source files"。)这使我们的示例变为:

python

运行

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

在 Linux 上,此示例的构建过程如下所示:

bash

% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o program prog.o file1.o file2.o

或在 Windows 上:

bash

C:\>scons -Q
cl /Fofile1.obj /c file1.c /nologo
cl /Fofile2.obj /c file2.c /nologo
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:program.exe prog.obj file1.obj file2.obj
embedManifestExeCheck(target, source, env)

3.3 使用 Glob 创建文件列表

您还可以使用 Glob 函数查找所有匹配特定模板的文件,使用标准的 shell 模式匹配字符 *、? 和 [abc] 来匹配 a、b 或 c 中的任何一个。也支持 [!abc],用于匹配除 a、b 或 c 之外的任何字符。这使得许多多源文件构建变得相当容易:

python

运行

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

SCons 手册页详细介绍了如何在变体目录(见下面的第 16 章 "变体构建")和存储库(见下面的第 22 章 "从代码存储库构建")中使用 Glob,排除某些文件以及返回字符串而不是节点。

3.4 指定单个文件与文件列表

我们现在向您展示了两种指定程序源的方法,一种是使用文件列表:

python

运行

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

另一种是使用单个文件:

python

运行

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

您实际上也可以将单个文件名放在列表中,出于一致性考虑,您可能更喜欢这样做:

python

运行

Program('hello', ['hello.c'])

SCons 函数将接受任一形式的单个文件名。实际上,在内部,SCons 将所有输入视为文件列表,但允许您省略方括号,以便在只有单个文件名时减少一些输入。

重要提示

虽然 SCons 函数对于单个文件名是使用字符串还是列表比较宽容,但 Python 本身对于列表和字符串的处理更为严格。因此,在 SCons 允许使用字符串或列表的地方:

python

运行

# 以下两个调用都能正确工作:
Program('program1', 'program1.c')
Program('program2', ['program2.c'])

尝试执行混合字符串和列表的 "Python 操作" 会导致错误或产生不正确的结果:

python

运行

common_sources = ['file1.c', 'file2.c']# 以下代码不正确,会产生Python错误
# 因为它试图将字符串添加到列表中:
Program('program1', common_sources + 'program1.c')# 以下代码正确工作,因为它将两个列表相加形成另一个列表。
Program('program2', common_sources + ['program2.c'])

3.5 使文件列表更易读

使用 Python 列表表示源文件的一个缺点是,每个文件名必须用引号(单引号或双引号)括起来。当文件名列表很长时,这可能会变得繁琐且难以阅读。幸运的是,SCons 和 Python 提供了多种方法来确保 SConstruct 文件保持易读性。

为了更轻松地处理长文件名列表,SCons 提供了一个 Split 函数,它接受一个用引号引起来的文件名列表,文件名之间用空格或其他空白字符分隔,并将其转换为单独的文件名列表。使用 Split 函数将前面的示例转换为:

python

运行

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

(如果您已经熟悉 Python,您会意识到这类似于 Python 标准字符串模块中的 split () 方法。然而,与字符串的 split () 成员函数不同,Split 函数不要求输入为字符串,它会将单个非字符串对象包装在列表中,或者如果参数已经是列表,则原封不动地返回。这在确保可以将任意值传递给 SCons 函数而无需手动检查变量类型时非常有用。)

将对 Split 函数的调用放在 Program 调用内部也可能有点笨拙。一个更易读的替代方法是将 Split 调用的输出赋给一个变量名,然后在调用 Program 函数时使用该变量:

python

运行

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

最后,Split 函数不关心引号字符串中的文件名之间有多少空白字符。这允许您创建跨越多行的文件名列表,这通常使编辑更容易:

python

运行

src_files = Split("""main.cfile1.cfile2.c""")
Program('program', src_files)

(注意在这个示例中,我们使用了 Python 的 "三引号" 语法,它允许字符串包含多行。三个引号可以是单引号或双引号。)

3.6 关键字参数

SCons 还允许您使用 Python 关键字参数来标识输出文件和输入源文件。输出文件称为 target,源文件(逻辑上)称为 source。Python 语法如下:

python

运行

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

因为关键字明确标识了每个参数是什么,所以如果您愿意,实际上可以颠倒顺序:

python

运行

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

您是否选择使用关键字参数来标识目标文件和源文件,以及使用关键字时指定它们的顺序,纯粹是个人选择;无论哪种方式,SCons 的功能都是相同的。

3.7 编译多个程序

为了在同一个 SConstruct 文件中编译多个程序,只需多次调用 Program 方法,为每个需要构建的程序调用一次:

python

运行

Program('foo.c')
Program('bar', ['bar1.c', 'bar2.c'])

然后 SCons 将按以下方式构建程序:

bash

% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o bar bar1.o bar2.o
cc -o foo.o -c foo.c
cc -o foo foo.o

请注意,SCons 不一定按照您在 SConstruct 文件中指定的顺序构建程序。但是,SCons 确实认识到必须先构建各个目标文件,然后才能构建生成的程序。我们将在下面的 "依赖关系" 部分更详细地讨论这一点。

3.8 在多个程序之间共享源文件

通过在多个程序之间共享源文件来重用代码是很常见的。一种方法是从公共源文件创建一个库,然后可以将其链接到生成的程序中。(创建库将在下面的第 4 章 "使用库进行构建和链接" 中讨论。)

在多个程序之间共享源文件的一种更直接但可能不太方便的方法是,简单地将公共文件包含在每个程序的源文件列表中:

python

运行

Program(Split('foo.c common1.c common2.c'))
Program('bar', Split('bar1.c bar2.c common1.c common2.c'))

SCons 认识到 common1.c 和 common2.c 源文件的目标文件各自只需要构建一次,即使生成的目标文件各自都被链接到两个生成的可执行程序中:

bash

% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o common1.o -c common1.c
cc -o common2.o -c common2.c
cc -o bar bar1.o bar2.o common1.o common2.o
cc -o foo.o -c foo.c
cc -o foo foo.o common1.o common2.o

如果两个或多个程序共享许多公共源文件,那么当您需要更改公共文件列表时,在每个程序的列表中重复公共文件可能会成为一个维护问题。您可以通过创建一个单独的 Python 列表来保存公共文件名,并使用 Python + 运算符将其与其他列表连接起来,从而简化此操作:

python

运行

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)

这在功能上等同于前面的示例。

3.9 调用构建器时覆盖构建变量

在调用构建器方法时,可以通过传递额外的关键字参数来覆盖或添加构建变量。这些被覆盖或添加的变量仅在构建目标时有效,因此它们不会影响构建的其他部分。例如,如果您只想为一个程序添加额外的库:

python

运行

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

或者生成具有非标准后缀的共享库:

python

运行

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

也可以在覆盖中使用 parse_flags 关键字参数,将命令行风格的参数合并到适当的构建变量中(请参阅 env.MergeFlags)。

此示例将 'include' 添加到,将添加到CPPDEFINES,将 'm' 添加到 $LIBS。

python

运行

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

在调用构建器操作期间,环境不会被克隆,而是创建一个 OverrideEnvironment (),它比整个 Environment () 更轻量级。

第四章:使用库进行构建和链接

将软件的各个部分收集到一个或多个库中,以此来组织大型软件项目,这种做法往往很实用。SCons 能让你轻松创建库,并在程序里使用这些库。

4.1 构建库

要构建自己的库,可使用Library而非Program来进行指定:

python

运行

Library('foo', ['f1.c', 'f2.c', 'f3.c'])

SCons 会依据你所用的系统,添加合适的库前缀和后缀。所以,在 POSIX 或者 Linux 系统上,上述示例的构建过程如下(尽管并非所有系统都会调用 ranlib):

bash

% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a

在 Windows 系统上,构建上述示例的情况如下:

bash

C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
lib /nologo /OUT:foo.lib f1.obj f2.obj f3.obj

库的目标名称规则和程序的类似:要是你没有明确指定目标库的名称,SCons 会从所指定的第一个源文件的名称推导出一个名称,而且如果你没有添加文件前缀和后缀,SCons 会添加上合适的。

4.1.1 从源代码或目标文件构建库

前面的示例展示了如何从源文件列表构建库。不过,你也能在调用Library时传入目标文件,它会正确识别出这些是目标文件。实际上,你可以在源列表中随意混合源代码文件和目标文件:

python

运行

Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])

SCons 会明白,在创建最终库之前,只需把源代码文件编译成目标文件:

bash

% scons -Q
cc -o f1.o -c f1.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o f4.o
ranlib libfoo.a

当然,在这个示例中,要使构建成功,目标文件必须已经存在。关于如何显式构建目标文件并将其包含到库中,可参考下面的第五章 “节点对象”。

4.1.2 显式构建静态库:StaticLibrary 构建器

Library函数会构建传统的静态库。要是你想明确指定所构建库的类型,可以使用StaticLibrary函数来替代Library

python

运行

StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])

StaticLibrary函数和Library函数在功能上并无差异。

4.1.3 构建共享(DLL)库:SharedLibrary 构建器

如果你想构建共享库(在 POSIX 系统上)或者 DLL 文件(在 Windows 系统上),可以使用SharedLibrary函数:

python

运行

SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])

在 POSIX 系统上的输出:

bash

% scons -Q
cc -o f1.os -c f1.c
cc -o f2.os -c f2.c
cc -o f3.os -c f3.c
cc -o libfoo.so -shared f1.os f2.os f3.os

在 Windows 系统上的输出:

bash

C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
link /nologo /dll /out:foo.dll /implib:foo.lib f1.obj f2.obj f3.obj
RegServerFunc(target, source, env)
embedManifestDllCheck(target, source, env)

再次注意,SCons 会正确处理输出文件的构建,在 POSIX 编译时添加-shared选项,在 Windows 上添加/dll选项。

4.2 与库链接

通常,你构建库是为了将其与一个或多个程序进行链接。要将库与程序链接,可在$LIBS构造变量中指定库,在$LIBPATH构造变量中指定查找库的目录:

python

运行

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

当然要注意,你无需指定库的前缀(如lib)或后缀(如.a.lib)。SCons 会为当前系统使用正确的前缀或后缀。

在 POSIX 或 Linux 系统上,构建上述示例的情况如下:

bash

% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
cc -o prog.o -c prog.c
cc -o prog prog.o -L. -lfoo -lbar

在 Windows 系统上,构建上述示例的情况如下:

bash

C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
lib /nologo /OUT:foo.lib f1.obj f2.obj f3.obj
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:prog.exe /LIBPATH:. foo.lib bar.lib prog.obj
embedManifestExeCheck(target, source, env)

和往常一样,要注意 SCons 会处理好每个系统上与指定库链接所需的正确命令行构建。

还要注意,如果只需链接单个库,你可以用单个字符串而非 Python 列表来指定库名,所以:

python

运行

Program('prog.c', LIBS='foo', LIBPATH='.')

这等同于:

python

运行

Program('prog.c', LIBS=['foo'], LIBPATH='.')

这和 SCons 处理用字符串或列表来指定单个源文件的方式类似。

4.3 查找库:$LIBPATH 构造变量

默认情况下,链接器只会在系统定义的特定目录中查找库。SCons 知道如何在你通过$LIBPATH构造变量指定的目录中查找库。$LIBPATH由目录名列表组成,如下所示:

python

运行

Program('prog.c', LIBS = 'm',LIBPATH = ['/usr/lib', '/usr/local/lib'])

建议使用 Python 列表,因为这样在各系统间具有可移植性。或者,你也可以把所有目录名放在一个字符串中,用系统特定的路径分隔符分隔:在 POSIX 系统上用冒号:

python

运行

LIBPATH = '/usr/lib:/usr/local/lib'

在 Windows 系统上用分号:

python

运行

LIBPATH = 'C:\\lib;D:\\lib'

(注意,在 Windows 路径名中,Python 要求反斜杠分隔符在字符串中进行转义。)

执行链接器时,SCons 会创建合适的标志,让链接器在与 SCons 相同的目录中查找库。所以在 POSIX 或 Linux 系统上,构建上述示例的情况如下:

bash

% scons -Q
cc -o prog.o -c prog.c
cc -o prog prog.o -L/usr/lib -L/usr/local/lib -lm

在 Windows 系统上,构建上述示例的情况如下:

bash

C:\>scons -Q
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:prog.exe /LIBPATH:\usr\lib /LIBPATH:\usr\local\lib m.lib prog.obj
embedManifestExeCheck(target, source, env)

再次注意,SCons 会处理好创建正确命令行选项的特定系统细节。

第五章:节点对象

在内部,SCons 把它所知道的所有文件和目录都表示为节点。这些内部对象(并非目标文件)可通过多种方式使用,从而让你的 SConscript 文件具备可移植性且易于阅读。

5.1 构建器方法返回目标节点列表

所有构建器方法都会返回一个节点对象列表,这些节点对象标识了将要构建的目标文件。可以将这些返回的节点作为参数传递给其他构建器方法。

例如,假设我们想用不同的选项来构建组成一个程序的两个目标文件。这意味着要为每个目标文件分别调用一次Object构建器,并指定所需的选项:

python

运行

Object('hello.c', CCFLAGS='-DHELLO')
Object('goodbye.c', CCFLAGS='-DGOODBYE')

将这些目标文件组合成最终程序的一种方法是,在调用Program构建器时将目标文件的名称列为源文件:

python

运行

Object('hello.c', CCFLAGS='-DHELLO')
Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(['hello.o', 'goodbye.o'])

用字符串指定名称存在一个问题,那就是我们的 SConstruct 文件在不同操作系统之间不再具有可移植性。例如,它在 Windows 上无法工作,因为 Windows 上的目标文件名为hello.objgoodbye.obj,而不是hello.ogoodbye.o

更好的解决办法是,把调用Object构建器返回的目标列表赋值给变量,然后在调用Program构建器时将这些变量连接起来:

python

运行

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

这让我们的 SConstruct 文件再次具备了可移植性,在 Linux 上的构建输出如下:

bash

% scons -Q
cc -o goodbye.o -c -DGOODBYE goodbye.c
cc -o hello.o -c -DHELLO hello.c
cc -o hello hello.o goodbye.o

在 Windows 上:

bash

C:\>scons -Q
cl /Fogoodbye.obj /c goodbye.c -DGOODBYE
cl /Fohello.obj /c hello.c -DHELLO
link /nologo /OUT:hello.exe hello.obj goodbye.obj
embedManifestExeCheck(target, source, env)

在本指南的后续内容中,我们会看到使用构建器方法返回的节点列表的示例。

5.2 显式创建文件和目录节点

值得一提的是,SCons 对表示文件的节点和表示目录的节点做了明确区分。SCons 支持FileDir函数,它们分别返回文件或目录节点:

python

运行

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

通常,你无需直接调用FileDir,因为调用构建器方法时会自动将字符串视为文件或目录的名称,并为你将其转换为节点对象。在需要明确告知 SCons 传递给构建器或其他函数的节点类型,或者明确引用目录树中的特定文件时,FileDir函数就会派上用场。

有时,你可能需要引用文件系统中的某个条目,但事先并不清楚它是文件还是目录。针对这种情况,SCons 还支持Entry函数,该函数返回的节点既可以表示文件,也可以表示目录。

python

运行

xyzzy = Entry('xyzzy')

返回的xyzzy节点在首次被构建器方法或其他需要文件节点或目录节点的函数使用时,会被转换为文件节点或目录节点。

5.3 打印节点文件名

使用节点最常见的操作之一是用它来打印该节点所代表的文件名。不过要记住,由于调用构建器返回的对象是一个节点列表,所以你必须使用 Python 下标从列表中获取单个节点。例如,下面的 SConstruct 文件:

python

运行

object_list = Object('hello.c')
program_list = Program(object_list)
print("The object file is: %s"%object_list[0])
print("The program file is: %s"%program_list[0])

在 POSIX 系统上会打印出以下文件名:

bash

% scons -Q
The object file is: hello.o
The program file is: hello
cc -o hello.o -c hello.c
cc -o hello hello.o

在 Windows 系统上会打印出以下文件名:

bash

C:\>scons -Q
The object file is: hello.obj
The program file is: hello.exe
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)

注意,在上述示例中,object_list[0]从列表中提取出一个实际的节点对象,Python 的print语句会将该对象转换为字符串进行打印。

5.4 将节点的文件名用作字符串

上一节中所描述的打印节点名称之所以可行,是因为节点对象的字符串表示形式就是文件名。如果你想对文件名进行打印以外的操作,可以使用 Python 的内置函数str来获取它。例如,如果你想在读取和执行 SConstruct 文件时,使用 Python 的os.path.exists来判断某个文件是否存在,可以按如下方式获取字符串:

python

运行

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)

在 POSIX 系统上,这段代码的执行情况如下:

bash

% scons -Q
hello does not exist!
cc -o hello.o -c hello.c
cc -o hello hello.o
5.5 GetBuildPath:从节点或字符串获取路径

env.GetBuildPath(file_or_list)会返回节点或表示路径的字符串的路径。它也可以接受节点和 / 或字符串的列表,并返回路径列表。如果传入单个节点,结果与调用str(node)相同(见上文)。字符串中可以嵌入构造变量,这些变量会像往常一样使用调用环境的变量集进行展开。路径可以是文件或目录,并且不一定需要存在。

python

运行

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

这将打印以下文件名:

bash

% scons -Q
['foo.c', 'sub/dir/value']
scons: `.' is up to date.

还有一个函数版本的GetBuildPath,可以在不使用环境的情况下调用;它使用默认的 SCons 环境对任何字符串参数进行替换。

第六章:依赖关系

到目前为止,我们已经了解了 SCons 如何处理一次性构建。但像 SCons 这样的构建工具的一个主要功能是,当源文件发生变化时,只重新构建必要的部分 —— 换句话说,SCons 不应浪费时间去重建不需要重建的东西。只需在构建完我们简单的 hello 示例后再次调用 SCons,你就能看到这一点在起作用:

bash

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

第二次执行时,SCons 会意识到 hello 程序相对于当前的 hello.c 源文件是最新的,从而避免重新构建它。在命令行上明确指定 hello 程序的名称,你可以更清楚地看到这一点:

bash

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

注意,SCons 只对命令行上明确命名的目标文件报告 “...is up to date”,以避免输出混乱。

6.1 确定输入文件何时发生变化:Decider 函数

避免不必要重建的另一个方面是构建工具的基本行为,即当输入文件发生变化时重建相关内容,以使构建的软件保持最新状态。默认情况下,SCons 通过计算每个文件内容的 MD5 签名或校验和来跟踪这一点,不过你也可以轻松配置 SCons 改为使用修改时间(或时间戳)。你甚至可以指定自己的 Python 函数来判断输入文件是否发生了变化。

6.1.1 使用 MD5 签名确定文件是否发生变化

默认情况下,SCons 根据文件内容的 MD5 校验和而不是文件的修改时间来跟踪文件是否发生了变化。这意味着,如果你习惯了 Make 通过更新文件的修改时间(例如使用 touch 命令)来强制重建的惯例,可能会对 SCons 的默认行为感到惊讶:

bash

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

即使文件的修改时间发生了变化,SCons 也会意识到 hello.c 文件的内容没有变化,因此不需要重建 hello 程序。这避免了不必要的重建,例如,当有人重写了文件内容但没有实际更改时。但是,如果文件的内容确实发生了变化,SCons 会检测到变化并根据需要重建程序:

bash

% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
%     [CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o

注意,如果你愿意,可以使用 Decider 函数显式指定这种默认行为(MD5 签名):

python

运行

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

调用 Decider 函数时,你也可以使用字符串 'content' 作为 'MD5' 的同义词。

6.1.1.1 使用 MD5 签名的影响

使用 MD5 签名来确定输入文件是否发生变化有一个意想不到的好处:如果源文件的更改方式使得重建后的目标文件内容与上次构建时完全相同,那么依赖于这个重建但未更改的目标文件的任何 "下游" 目标文件实际上不需要重建。

因此,如果用户只更改了 hello.c 文件中的注释,那么重建的 hello.o 文件将与之前构建的完全相同(假设编译器不会在目标文件中放入任何特定于构建的信息)。SCons 会意识到不需要重建 hello 程序:

bash

% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
%   [CHANGE A COMMENT IN hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.

本质上,当 SCons 意识到目标文件重建后的内容与上次构建完全相同时,它会 "短路" 任何依赖构建。这确实需要一些额外的处理时间来读取目标 (hello.o) 文件的内容,但在避免的重建过程非常耗时的情况下,通常可以节省时间。

6.1.2 使用时间戳确定文件是否发生变化

如果你愿意,你可以配置 SCons 在决定是否需要重建目标时使用文件的修改时间而不是文件内容。SCons 提供了两种方法来使用时间戳确定输入文件自上次构建目标以来是否发生了变化。

最常见的使用时间戳的方法与 Make 相同:即,如果源文件的修改时间比目标文件新,SCons 就会决定必须重建目标。为此,可以如下调用 Decider 函数:

python

运行

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

这使得 SCons 在更新文件的修改时间(例如使用 touch 命令)时表现得像 Make:

bash

% scons -Q hello.o
cc -o hello.o -c hello.c
% touch hello.c
% scons -Q hello.o
cc -o hello.o -c hello.c

实际上,由于这种行为与 Make 的行为相同,因此在调用 Decider 函数时,你也可以使用字符串 'make' 作为 'timestamp-newer' 的同义词:

python

运行

Object('hello.c')
Decider('make')

完全像 Make 一样使用时间戳的一个缺点是,如果输入文件的修改时间突然变得比目标文件旧,目标文件将不会被重建。例如,如果从备份存档中恢复了源文件的旧副本,就可能发生这种情况。恢复的文件内容可能与上次构建依赖目标时不同,但由于源文件的修改时间不比目标新,目标不会被重建。

由于 SCons 实际上在构建目标时存储了有关源文件时间戳的信息,因此它可以通过检查源文件时间戳的精确匹配来处理这种情况,而不仅仅是检查源文件是否比目标文件新。为此,在调用 Decider 函数时指定参数 'timestamp-match':

python

运行

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

以这种方式配置后,只要源文件的修改时间发生变化,SCons 就会重建目标。因此,如果我们使用 touch -t 选项将 hello.c 的修改时间更改为旧日期(1989 年 1 月 1 日),SCons 仍会重建目标文件:

bash

% scons -Q hello.o
cc -o hello.o -c hello.c
% touch -t 198901010000 hello.c
% scons -Q hello.o
cc -o hello.o -c hello.c

一般来说,选择 timestamp-newer 而不是 timestamp-match 的唯一原因是,如果你有特定的理由需要这种类似 Make 的行为,即在修改后的源文件比目标旧时不重建目标。

6.1.3 使用 MD 签名和时间戳确定文件是否发生变化

作为一种性能增强,SCons 提供了一种使用文件内容的 MD5 校验和的方法,但仅在文件的时间戳发生变化时才读取这些内容。为此,使用 'MD5-timestamp' 参数调用 Decider 函数:

python

运行

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

如此配置后,SCons 的行为仍将与使用 Decider ('MD5') 时相同:

bash

% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
% edit hello.c[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o

然而,在上述输出中,当构建是最新的时,第二次调用 SCons 只需查看 hello.c 文件的修改时间,而不必打开它并对其内容进行 MD5 校验和计算。这可以显著加快许多最新构建的速度。

使用 Decider ('MD5-timestamp') 的唯一缺点是,如果源文件在上次 SCons 构建文件后的一秒内被修改,SCons 将不会重建目标文件。在大多数开发者编程时,这实际上不是问题,因为不太可能有人在构建后一秒内快速思考并对源文件进行实质性更改。然而,某些构建脚本或持续集成工具可能依赖于自动应用文件更改然后尽快重建的能力,在这种情况下,使用 Decider ('MD5-timestamp') 可能不合适。

6.1.4 编写自己的自定义 Decider 函数

我们传递给 Decider 函数的不同字符串值,本质上是 SCons 用来选择几个特定内部函数之一的,这些内部函数实现了各种方法来判断自目标文件构建以来依赖项(通常是源文件)是否发生了变化。事实证明,你也可以提供自己的函数来判断依赖项是否发生了变化。

例如,假设我们有一个输入文件,其中包含大量特定格式的数据,用于重建许多不同的目标文件,但每个目标文件实际上只依赖于输入文件的一个特定部分。我们希望每个目标文件只依赖于输入文件的该部分。然而,由于输入文件可能包含大量数据,我们希望仅在其时间戳发生变化时才打开输入文件。这可以通过一个自定义 Decider 函数来实现,该函数可能如下所示:

python

运行

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 Truereturn False
Decider(decide_if_changed)

注意,在函数定义中,依赖项(输入文件)是第一个参数,然后是目标。这两个参数都作为 SCons Node 对象传递给函数,我们使用 Python 的 str () 将它们转换为字符串。

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

第四个参数 repo_node 是在比较 BuildInfo 时如果不为 None 则使用的 Node。这通常仅在目标节点仅存在于存储库中时设置。

  • .csig:上次构建目标时依赖文件内容的内容签名或 MD5 校验和。
  • .size:上次构建目标时依赖文件的大小(以字节为单位)。
  • .timestamp:上次构建目标时依赖文件的修改时间。

注意,在自定义 Decider 函数中忽略某些参数是完全正常的,如果它们不影响你判断依赖文件是否发生变化的方式。

另一件需要注意的事情是,上述三个属性在首次运行时可能不存在。如果没有先前的构建,则没有创建任何目标,并且还不存在.sconsign DB 文件。因此,你应该始终检查所讨论的 prev_ni 属性是否可用。

我们最后给出一个基于 csig 的 decider 函数的小例子。注意每次函数调用时如何通过 get_csig 初始化依赖文件的签名信息(这是必需的!)。

python

运行

env = Environment()def config_file_decider(dependency, target, prev_ni, repo_node=None):import os.path# 我们总是必须初始化.csig值...dep_csig = dependency.get_csig()# .csig可能不存在,因为还没有构建目标...if 'csig' not in dir(prev_ni):return True# 目标文件可能还不存在if not os.path.exists(str(target.abspath)):return Trueif dep_csig != prev_ni.csig:# 源文件有一些更改 => 更新已安装的文件return Truereturn Falsedef update_file():f = open("test.txt","a")f.write("some line\n")f.close()update_file()# 激活我们自己的decider函数
env.Decider(config_file_decider)env.Install("install","test.txt")
6.1.5 混合使用不同的方法确定文件是否发生变化

前面的例子都展示了调用全局 Decider 函数来配置 SCons 做出的所有依赖决策。然而,有时你希望能够为不同的目标配置不同的决策方法。当需要这样做时,你可以使用 env.Decider 方法只影响使用特定构建环境构建的目标的配置决策。

例如,如果我们想从同一个源使用 MD5 校验和构建一个程序,使用文件修改时间构建另一个程序,我们可以这样配置:

python

运行

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

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

bash

% scons -Q
cc -o program1.o -c -I. program1.c
cc -o prog-MD5 program1.o
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
% touch inc.h
% scons -Q
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
6.2 确定输入文件何时发生变化的旧函数

SCons 仍然支持两个曾经是配置判断输入文件是否发生变化的主要方法的函数。这些函数在 SCons 2.0 版本中已被正式弃用,不建议使用,主要是因为它们依赖于源文件和目标文件处理方式之间有些令人困惑的区别。这里记录这些函数主要是以防你在较旧的 SConscript 文件中遇到它们。

6.3 隐式依赖:$CPPPATH 构造变量

现在假设我们的 "Hello, World!" 程序实际上有一个 #include 行,在编译时包含 hello.h 文件:

c

#include <hello.h>
int
main()
{printf("Hello, %s!\n", string);
}

为了完整起见,hello.h 文件如下所示:

c

#define string    "world"

在这种情况下,我们希望 SCons 认识到,如果 hello.h 文件的内容发生变化,hello 程序必须重新编译。为此,我们需要像这样修改 SConstruct 文件:

python

运行

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

$CPPPATH 值告诉 SCons 在当前目录('.')中查找 C 源文件(.c 或.h 文件)包含的任何文件。在 SConstruct 文件中进行此赋值后:

bash

% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
%     [CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o

首先,注意 SCons 从 $CPPPATH 变量添加了 - I. 参数,以便编译能够在本地目录中找到 hello.h 文件。

其次,要意识到 SCons 知道必须重建 hello 程序,因为它扫描 hello.c 文件的内容,查找指示在编译中包含另一个文件的 #include 行。SCons 将这些记录为目标文件的隐式依赖项。因此,当 hello.h 文件发生变化时,SCons 意识到 hello.c 文件包含它,并重建依赖于 hello.c 和 hello.h 文件的最终 hello 程序。

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

python

运行

Program('hello.c', CPPPATH = ['include', '/home/project/inc'])

在 POSIX 或 Linux 上看起来像这样:

bash

% scons -Q hello
cc -o hello.o -c -Iinclude -I/home/project/inc hello.c
cc -o hello hello.o

在 Windows 上看起来像这样:

bash

C:\>scons -Q hello.exe
cl /Fohello.obj /c hello.c /nologo /Iinclude /I\home\project\inc
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
6.4 缓存隐式依赖

扫描每个文件的 #include 行确实需要一些额外的处理时间。当你对大型系统进行完整构建时,扫描时间通常只占构建总时间的很小一部分。然而,当你重建大型系统的全部或部分时,你最有可能注意到扫描时间:SCons 可能会花费一些额外的时间来 "思考" 必须构建什么,然后才发出第一个构建命令(或决定一切都是最新的,无需重建)。

实际上,让 SCons 扫描文件相对于追踪因不正确依赖而引入的细微问题所可能损失的时间来说是节省时间的。尽管如此,SCons 扫描文件时的 "等待时间" 可能会让等待构建完成的单个开发者感到烦恼。因此,SCons 允许你缓存其扫描器找到的隐式依赖项,以供后续构建使用。你可以通过在命令行上指定 --implicit-cache 选项来做到这一点:

bash

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

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

python

运行

SetOption('implicit_cache', 1)

SCons 默认不会像这样缓存隐式依赖项,因为 --implicit-cache 会导致 SCons 简单地使用上次运行时存储的隐式依赖项,而不检查这些依赖项是否仍然正确。具体来说,这意味着 --implicit-cache 会指示 SCons 在以下情况下不能 "正确" 重建:

  • 当使用 --implicit-cache 时,SCons 将忽略对搜索路径(如或LIBPATH)可能做的任何更改。如果 $CPPPATH 的更改通常会导致使用来自不同目录的同名不同文件,这可能会导致 SCons 不重建文件。
  • 当使用 --implicit-cache 时,如果在搜索路径中比上次找到文件的目录更早的目录中添加了同名文件,SCons 将不会检测到。
6.4.1 --implicit-deps-changed 选项

在使用缓存的隐式依赖项时,有时你想 "重新开始",让 SCons 重新扫描它之前缓存了依赖项的文件。例如,如果你最近安装了用于编译的新版本外部代码,外部头文件将发生变化,之前缓存的隐式依赖项将过时。你可以通过使用 --implicit-deps-changed 选项运行 SCons 来更新它们:

bash

% scons -Q --implicit-deps-changed hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.

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

6.4.2 --implicit-deps-unchanged 选项

默认情况下,在缓存依赖项时,SCons 会注意到文件何时被修改,并重新扫描文件以获取任何更新的隐式依赖项信息。然而,有时你可能想强制 SCons 使用缓存的隐式依赖项,即使源文件发生了变化。例如,当你更改了源文件但知道你没有更改任何 #include 行时,这可以加快构建速度。在这种情况下,你可以使用 --implicit-deps-unchanged 选项:

bash

% scons -Q --implicit-deps-unchanged hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.

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

6.5 显式依赖:Depends 函数

有时,一个文件依赖于另一个文件,而这个依赖关系未被 SCons 扫描器检测到。对于这种情况,SCons 允许你显式指定一个文件依赖于另一个文件,并且当被依赖的文件发生变化时,依赖它的文件必须重新构建。这是通过 Depends 方法来指定的:

python

运行

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

plaintext

% scons -Q hello
cc -c hello.c -o hello.o
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit other_file[CHANGE THE CONTENTS OF other_file]
% scons -Q hello
cc -c hello.c -o hello.o
cc -o hello hello.o

请注意,依赖项(Depends 的第二个参数)也可以是一个节点对象列表(例如,由调用构建器返回的列表):

python

运行

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

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

plaintext

% scons -Q hello
cc -c goodbye.c -o goodbye.o
cc -o goodbye goodbye.o
cc -c hello.c -o hello.o
cc -o hello hello.o

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

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

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

c

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

plaintext

% scons -Q
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
%    [CHANGE CONTENTS OF foo.h]
% scons -Q
scons: `.' is up to date.

显然,扫描器不知道头文件的依赖关系。由于不是一个功能齐全的 C 预处理器,扫描器不会展开宏。

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

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

python

运行

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

plaintext

% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
%    [CHANGE CONTENTS OF foo.h]
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c

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

plaintext

% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding `hello.o' because `foo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.

在第一次通过时,在编译目标文件时生成依赖文件。此时,SCons 不知道对 foo.h 的依赖。在第二次通过时,由于检测到 foo.h 作为新的依赖项,目标文件会重新生成。

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

6.7 忽略依赖:Ignore 函数

有时,即使依赖文件发生变化,也有理由不重新构建程序。在这种情况下,你可以明确告诉 SCons 忽略某个依赖,如下所示:

python

运行

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

plaintext

% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
scons: `hello' is up to date.

现在,上面的示例有点人为设计的感觉,因为很难想象在实际情况中,如果 hello.h 文件发生变化,你却不想重新构建 hello 程序。一个更现实的例子可能是,如果 hello 程序在一个由多个系统共享的目录中构建,而这些系统的 stdio.h 包含文件的副本不同。在这种情况下,SCons 会注意到不同系统的 stdio.h 副本之间的差异,并且每次你切换系统时都会重新构建 hello 程序。你可以通过以下方式避免这些重新构建:

python

运行

hello = Program('hello.c', CPPPATH=['/usr/include'])
Ignore(hello, '/usr/include/stdio.h')

Ignore 还可用于防止默认情况下构建生成的文件。这是因为目录依赖于其内容。因此,要从默认构建中忽略生成的文件,你可以指定目录应忽略该生成的文件。请注意,如果用户在 scons 命令行中明确请求目标,或者该文件是另一个被请求和 / 或默认构建的文件的依赖项,则该文件仍将被构建。

python

运行

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

plaintext

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

6.8 仅顺序依赖:Requires 函数

偶尔,指定某个文件或目录在必要时必须在其他目标构建之前构建或创建,但该文件或目录的更改不需要重新构建目标,这可能会很有用。这种关系被称为仅顺序依赖,因为它只影响构建的顺序 —— 目标之前的依赖项 —— 但它不是严格的依赖关系,因为目标不应该因依赖文件的更改而改变。

例如,假设你想每次运行构建时创建一个文件,该文件标识构建的时间、版本号等,并且该文件包含在你构建的每个程序中。版本文件的内容每次构建都会改变。如果你指定一个正常的依赖关系,那么依赖该文件的每个程序每次运行 SCons 时都会重新构建。例如,我们可以在 SConstruct 文件中使用一些 Python 代码,每次运行 SCons 时创建一个新的 version.c 文件,其中包含一个包含当前日期的字符串,然后通过在源文件中列出 version.c 来将程序与生成的目标文件链接:

python

运行

import timeversion_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 文件会更改):

plaintext

% scons -Q hello
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o

(请注意,为了使上述示例正常工作,我们在每次运行之间暂停一秒钟,以便 SConstruct 文件创建的 version.c 文件中的时间字符串比上一次运行晚一秒。)

一种解决方案是使用 Requires 函数指定 version.o 必须在链接步骤使用它之前重新构建,但 version.o 的更改实际上不应导致 hello 可执行文件重新链接:

python

运行

import timeversion_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 程序的源文件之一,所以我们必须找到其他方法将其添加到链接命令行中。在这个示例中,我们有点取巧,将目标文件名(从 Object 调用返回的 version_obj 列表中提取)放入变量中,因为LINKFLAGS 已经包含在 $LINKCOM 命令行中。

通过这些更改,我们得到了所需的行为,即只有当 hello.c 发生变化时才重新链接 hello 可执行文件,即使 version.o 被重新构建(因为 SConstruct 文件每次运行时仍会直接更改 version.c 的内容):

plaintext

% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
% sleep 1
%     [CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.

6.9 AlwaysBuild 函数

SCons 处理依赖的方式也可能受到 AlwaysBuild 方法的影响。当一个文件传递给 AlwaysBuild 方法时,如下所示:

python

运行

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

那么指定的目标文件(在我们的示例中是 hello)在遍历依赖图时,每次评估该目标文件时,都会被认为是过时的并重新构建:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o

AlwaysBuild 函数的名称有点误导性,因为它实际上并不意味着每次调用 SCons 时目标文件都会重新构建。相反,它意味着在评估命令行上指定的目标(及其依赖项)时,只要遇到目标文件,该目标就会被重新构建。因此,如果在命令行上指定了其他目标,并且该目标本身不依赖于 AlwaysBuild 目标,那么只有当它相对于其依赖项过时的时候才会被重新构建:

plaintext

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

第七章 环境

环境是一组值的集合,这些值会影响程序的执行方式。SCons 区分三种不同类型的环境,这些环境会影响 SCons 自身的行为(取决于 SConscript 文件中的配置),以及它执行的编译器和其他工具:

  1. 外部环境:外部环境是用户运行 SCons 时其环境中的一组变量。这些变量可以通过 Python 的 os.environ 字典在 SConscript 文件中使用。请参阅下面的 7.1 节 “使用外部环境中的值”。
  2. 构建环境:构建环境是在 SConscript 文件中创建的一个独特对象,它包含影响 SCons 决定使用何种操作来构建目标的一些值,甚至可以定义从哪些源文件构建哪些目标。SCons 最强大的功能之一是能够创建多个构建环境,包括从现有构建环境克隆一个新的、自定义的构建环境的能力。请参阅下面的 7.2 节 “构建环境”。
  3. 执行环境:执行环境是 SCons 在执行外部命令(如编译器或链接器)以构建一个或多个目标时设置的值。请注意,这与外部环境不同(见上文)。请参阅下面的 7.3 节 “控制发出命令的执行环境”。

与 Make 不同,SCons 不会自动在不同环境之间复制或导入值(显式克隆构建环境的情况除外,克隆的构建环境会从其父环境继承值)。这是一个经过深思熟虑的设计选择,以确保在默认情况下,无论用户外部环境中的值如何,构建都是可重复的。这避免了一类构建问题,即开发人员的本地构建能够成功,是因为自定义变量设置导致使用了不同的编译器或构建选项,但签入的更改会破坏官方构建,因为它使用了不同的环境变量设置。

请注意,SConscript 编写者可以轻松地安排在不同环境之间复制或导入变量,这通常非常有用(甚至是完全必要的),以便开发人员能够以适当的方式自定义构建。重点不是在不同环境之间复制变量是不好的并且必须始终避免。相反,构建系统的实现者应该有意识地选择如何以及何时将变量从一个环境导入另一个环境,并在使构建可重复和使用方便之间做出明智的决策,以达到正确的平衡。

7.1 使用外部环境中的值

用户在执行 SCons 时生效的外部环境变量设置可通过普通的 Python os.environ 字典获取。这意味着,如果您想在 SConscript 文件中使用用户外部环境中的值,则必须添加 import os 语句。

python

运行

import os

更有用的是,您可以在 SConscript 文件中使用 os.environ 字典,用用户外部环境中的值来初始化构建环境。有关如何执行此操作的信息,请参阅下一节 7.2 “构建环境”。

7.2 构建环境

在大型复杂系统中,很少有所有软件都需要以相同的方式进行构建。例如,不同的源文件可能需要在命令行上启用不同的选项,或者不同的可执行程序需要与不同的库进行链接。SCons 通过允许您创建和配置多个构建环境来控制软件的构建方式,从而满足这些不同的构建要求。构建环境是一个对象,它具有许多相关的构建变量,每个变量都有一个名称和一个值。(构建环境还附带一组 Builder 方法,我们将在后面详细了解这些方法。)

7.2.1 创建构建环境:Environment 函数

构建环境是通过 Environment 方法创建的:

python

运行

env = Environment()

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

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

python

运行

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

在这个示例中,构建环境仍然使用相同的默认构建变量值进行初始化,只是用户明确指定使用 GNU C 编译器 gcc,并进一步指定在编译目标文件时应使用 - O2(优化级别二)标志。换句话说,和CCFLAGS 的显式初始化会覆盖新创建的构建环境中的默认值。因此,这个示例的运行结果如下:

plaintext

% scons -Q
gcc -o foo.o -c -O2 foo.c
gcc -o foo foo.o
7.2.2 从构建环境中获取值

您可以使用访问 Python 字典中各个命名项的常规语法来获取单个构建变量:

python

运行

env = Environment()
print("CC is: %s"%env['CC'])

这个示例 SConstruct 文件不会构建任何内容,但由于它实际上是一个 Python 脚本,它会为我们打印 $CC 的值:

plaintext

% scons -Q
CC is: cc
scons: `.' is up to date.

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

python

运行

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

这个 SConstruct 文件将为我们在 POSIX 系统上打印指定的字典项,如下所示:

plaintext

% scons -Q
key = OBJSUFFIX, value = .o
key = LIBSUFFIX, value = .a
key = PROGSUFFIX, value = 
scons: `.' is up to date.

在 Windows 上:

plaintext

C:\>scons -Q
key = OBJSUFFIX, value = .obj
key = LIBSUFFIX, value = .lib
key = PROGSUFFIX, value = .exe
scons: `.' is up to date.

如果您想循环并打印构建环境中所有构建变量的值,按排序顺序执行此操作的 Python 代码可能如下所示:

python

运行

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

需要注意的是,对于前面的示例,实际上有一个构建环境方法可以更简单地完成相同的事情,并且还会尝试很好地格式化输出:

python

运行

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

7.2.3. 从构建环境展开变量值:subst 方法

从构建环境获取信息的另一种方法是对包含构建变量名(以 $ 开头)的字符串使用 subst 方法。举个简单的例子,上一节中使用 env['CC'] 获取 $CC 值的代码也可以这样写:

python

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

使用 subst 展开字符串的一个优势在于,结果中的构建变量会被递归展开,直到字符串中不再有可展开的变量。例如,直接获取 $CCCOM 的值:

python

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

这会输出 $CCCOM 未展开的值,显示其中仍需展开的构建变量:

plaintext

% scons -Q
CCCOM is: $CC $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS -c -o $TARGET $SOURCES
scons: `.' is up to date.

然而,对 $CCCOM 调用 subst 方法:

python

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

会递归展开所有以 $(美元符号)开头的构建变量,显示最终的输出结果:

plaintext

% scons -Q
CCCOM is: gcc -DFOO -c -o
scons: `.' is up to date.

注意,由于这里并非在实际构建过程中展开变量,因此 $TARGET 和 $SOURCES 没有对应的目标文件或源文件可供展开。

7.2.4. 处理变量展开问题

当展开构建变量时发生问题,默认情况下该变量会被展开为空字符串 '',并且不会导致 SCons 构建失败。

python

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

输出结果:

plaintext

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

可以使用 AllowSubstExceptions 函数更改这种默认行为。当变量展开出现问题时会抛出异常,而 AllowSubstExceptions 函数可以控制哪些异常是致命的,哪些可以被安全忽略。默认情况下,NameError和 IndexError 这两种异常会被允许发生:SCons 会捕获这些异常,将变量展开为空字符串,然后继续执行。如果要求所有构建变量名必须存在,并且不允许越界索引,可以不带任何参数调用 AllowSubstExceptions

python

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

此时输出结果:

plaintext

% scons -Q
scons: *** NameError `MISSING' trying to evaluate `$MISSING'
File "/home/my/project/SConstruct", line 3, in <module>

该函数也可用于允许其他可能出现的异常,特别是在使用 ${...} 构建变量语法时。例如,除了默认允许的异常外,还可以允许变量展开中出现除零异常:

python

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

输出结果:

plaintext

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

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

7.2.5. 控制默认构建环境:DefaultEnvironment 函数

到目前为止介绍的所有构建器函数(如 Program 和 Library)都使用一个构建环境,该环境包含 SCons 默认配置的各种编译器和其他工具的设置,或者是 SCons 在系统上发现的工具设置。如果不将这些函数作为特定构建环境的方法调用,它们将使用默认构建环境。默认构建环境的目标是让许多配置 "开箱即用",使用现成的工具以最少的配置更改来构建软件。

如果需要,可以使用 DefaultEnvironment 函数控制默认构建环境,通过传递关键字参数来初始化各种设置:

python

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

按上述方式配置后,所有对 Program 或 Object 构建器的调用都将使用 /usr/local/bin/gcc 编译器来构建目标文件。

DefaultEnvironment 函数返回初始化后的默认构建环境对象,该对象可以像其他构建环境一样进行操作(注意,默认环境类似于单例模式 —— 它只能有一个实例 —— 因此关键字参数仅在首次调用时处理。后续的任何调用都会返回现有的对象)。因此,以下代码与前面的示例等效,将 $CC 变量设置为 /usr/local/bin/gcc,但在默认构建环境初始化后作为单独的步骤进行:

python

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

DefaultEnvironment 函数的一个非常常见的用途是加速 SCons 初始化。为了让大多数默认配置 "开箱即用",SCons 实际上会在本地系统中搜索已安装的编译器和其他实用工具。这种搜索可能需要时间,特别是在文件系统较慢或基于网络的系统上。如果您知道要配置哪些编译器和 / 或其他实用工具,可以通过指定一些特定的工具模块来控制 SCons 执行的搜索,以初始化默认构建环境:

python

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

上面的示例告诉 SCons 明确配置默认环境,使用其正常的 GNU 编译器和 GNU 链接器设置(无需搜索它们或任何其他实用工具),并特别使用 /usr/local/bin/gcc 处的编译器。

7.2.6. 多个构建环境

构建环境的真正优势在于您可以根据需要创建任意数量的不同环境,每个环境都针对构建软件或其他文件的不同方式进行定制。例如,如果我们需要使用 -O2 标志构建一个程序,同时使用 -g(调试)标志构建另一个程序,我们可以这样做:

python

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

执行结果:

plaintext

% scons -Q
cc -o bar.o -c -g bar.c
cc -o bar bar.o
cc -o foo.o -c -O2 foo.c
cc -o foo foo.o

我们甚至可以使用多个构建环境来构建单个程序的多个版本。但是,如果只是简单地尝试在两个环境中使用 Program 构建器,如下所示:

python

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

SCons 会生成以下错误:

plaintext

% scons -Q
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 的目标文件,一个使用 -O2 的 $CCFLAGS 值,另一个使用 -g 的 $CCFLAGS 值。SCons 不能简单地决定其中一个应该优先于另一个,因此会生成错误。为了避免这个问题,我们必须使用 Object 构建器显式指定每个环境将 foo.c 编译为单独命名的目标文件,如下所示:

python

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)

注意,每次调用 Object 构建器都会返回一个值,这是一个表示将被构建的目标文件的 SCons 内部对象。然后,我们将该对象用作 Program 构建器的输入。这避免了在多个位置显式指定目标文件名,并使 SConstruct 文件紧凑易读。我们的 SCons 输出如下所示:

plaintext

% scons -Q
cc -o foo-dbg.o -c -g foo.c
cc -o foo-dbg foo-dbg.o
cc -o foo-opt.o -c -O2 foo.c
cc -o foo-opt foo-opt.o

7.2.7. 复制构建环境:Clone 方法

有时,您希望多个构建环境共享一个或多个变量的相同值。与其在创建每个构建环境时重复所有公共变量,不如使用 Clone 方法创建构建环境的副本。

与创建构建环境的 Environment 调用一样,Clone 方法接受构建变量赋值,这些赋值将覆盖被复制构建环境中的值。例如,假设我们想使用 gcc 创建一个程序的三个版本:一个优化版本、一个调试版本和一个无特殊标志的版本。我们可以通过创建一个将 $CC 设置为 gcc 的 "基础" 构建环境,然后创建两个副本,一个设置用于优化的 $CCFLAGS,另一个设置用于调试的 $CCFLAGS

python

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)

然后我们的输出将如下所示:

plaintext

% scons -Q
gcc -o foo.o -c foo.c
gcc -o foo foo.o
gcc -o foo-dbg.o -c -g foo.c
gcc -o foo-dbg foo-dbg.o
gcc -o foo-opt.o -c -O2 foo.c
gcc -o foo-opt foo-opt.o

7.2.8. 替换值:Replace 方法

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

python

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

替换值(在上面的例子中是 -DDEFINE2)完全替换了构建环境中的值:

plaintext

% scons -Q
cc -o foo.o -c -DDEFINE2 foo.c
cc -o foo foo.o

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

python

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

在这种情况下,构建变量只是被添加到构建环境中:

plaintext

% scons -Q
NEW_VARIABLE = xyzzy
scons: `.' is up to date.

由于变量在构建环境实际用于构建目标之前不会被展开,并且由于 SCons 函数和方法调用是顺序无关的,最后一次替换 "胜出" 并用于构建所有目标,无论 Replace() 调用与构建器方法调用的穿插顺序如何:

python

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,替换实际发生的时间与目标被构建的时间之间的关系就变得明显了:

plaintext

% scons
scons: Reading SConscript files ...
CCFLAGS = -DDEFINE1
CCFLAGS = -DDEFINE2
scons: done reading SConscript files.
scons: Building targets ...
cc -o bar.o -c -DDEFINE2 bar.c
cc -o bar bar.o
cc -o foo.o -c -DDEFINE2 foo.c
cc -o foo foo.o
scons: done building targets.

由于替换发生在读取 SConscript 文件时,当构建 foo.o 目标时,$CCFLAGS 变量已经被设置为 -DDEFINE2,即使对 Replace 方法的调用直到 SConscript 文件的后面才发生。

7.2.9. 仅在未定义时设置值:SetDefault 方法

有时,能够指定只有在构建环境尚未定义某个构建变量时才将其设置为某个值是很有用的。您可以使用 SetDefault 方法来做到这一点,该方法的行为类似于 Python 字典对象的 set_default 方法:

python

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

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

7.2.10. 追加到值的末尾:Append 方法

您可以使用 Append 方法将一个值追加到现有的构建变量:

python

env = Environment(CCFLAGS=['-DMY_VALUE'])
env.Append(CCFLAGS=['-DLAST'])
env.Program('foo.c')

SCons 在编译目标文件时会同时提供 -DMY_VALUE 和 -DLAST 标志:

plaintext

% scons -Q
cc -o foo.o -c -DMY_VALUE -DLAST foo.c
cc -o foo foo.o

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

python

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

输出结果:

plaintext

% scons -Q
NEW_VARIABLE = added
scons: `.' is up to date.

注意,Append 函数会 "智能" 地处理新值如何追加到旧值。如果两者都是字符串,则简单地将前一个字符串和新字符串连接起来。同样,如果两者都是列表,则将列表连接起来。但是,如果一个是字符串而另一个是列表,则将字符串作为新元素添加到列表中。

7.2.11. 追加唯一值:AppendUnique 方法

有时,仅在现有构建变量不包含某个值时添加该值是有用的。这可以通过 AppendUnique 方法实现:

python

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

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

7.2.12. 追加到值的开头:Prepend 方法

您可以使用 Prepend 方法将一个值追加到现有构建变量的开头:

python

env = Environment(CCFLAGS=['-DMY_VALUE'])
env.Prepend(CCFLAGS=['-DFIRST'])
env.Program('foo.c')

SCons 在编译目标文件时会同时提供 -DFIRST 和 -DMY_VALUE 标志:

plaintext

% scons -Q
cc -o foo.o -c -DFIRST -DMY_VALUE foo.c
cc -o foo foo.o

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

python

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

输出结果:

plaintext

% scons -Q
NEW_VARIABLE = added
scons: `.' is up to date.

与 Append 函数一样,Prepend 函数也会 "智能" 地处理新值如何追加到旧值。如果两者都是字符串,则简单地将前一个字符串和新字符串连接起来。同样,如果两者都是列表,则将列表连接起来。但是,如果一个是字符串而另一个是列表,则将字符串作为新元素添加到列表中。

7.2.13. 前置唯一值:PrependUnique 方法

有时,仅在现有值不包含要添加的值时,将新值添加到构建变量的开头是有用的。这可以通过 PrependUnique 方法实现:

python

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

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

7.3. 控制执行命令的环境

当 SCons 构建目标文件时,它不会使用您执行 SCons 时的相同外部环境来执行命令。相反,它使用存储在 $ENV 构建变量中的字典作为执行命令的外部环境。

这种行为的最重要影响是,控制操作系统在哪里查找命令和实用工具的 PATH 环境变量与您调用 SCons 的外部环境中的 PATH 不同。这意味着 SCons 默认情况下不一定能找到您可以从命令行执行的所有工具。

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

最简单的方法是在创建构建环境时显式初始化该值;以下是一种实现方式:

python

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

以这种方式将字典分配给 $ENV 构建变量会完全重置外部环境,因此执行外部命令时设置的唯一变量将是 PATH值。如果您想使用 $ENV 中的其余值而只设置 PATH 值,最直接的方法可能是:

python

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

请注意,SCons 确实允许您以字符串形式定义 PATH 中的目录,用系统的路径分隔符字符分隔(POSIX 系统上为 :,Windows 上为 ;):

python

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

但这样做会降低您的 SConscript 文件的可移植性(尽管在这种情况下这可能不是一个大问题,因为您列出的目录可能是特定于系统的)。

7.3.1. 从外部环境传播 PATH

您可能希望将外部 PATH 传播到命令的执行环境。您可以通过使用 os.environ 字典中的 PATH 值初始化 PATH 变量来实现这一点,os.environ 是 Python 让您访问外部环境的方式:

python

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

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

python

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

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

7.3.2. 在执行环境中添加到 PATH 值

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

python

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

请注意,添加的值是字符串,如果您想向 $PATH 这样的变量添加多个目录,则必须在字符串中包含路径分隔符字符(Linux 或 POSIX 上为 :,Windows 上为 ;)。

7.4. 使用 toolpath 查找外部工具

7.4.1. 默认工具搜索路径

通常在从构建环境使用工具时,默认会检查几个不同的搜索位置。这包括 SCons 内置的 SCons/Tools/ 目录以及相对于根 SConstruct 文件的 site_scons/site_tools 目录。

python

# 内置工具或位于 site_tools 中的工具
env = Environment(tools=['SomeTool'])
env.SomeTool(targets, sources)# 默认情况下搜索位置包括
SCons/Tool/SomeTool.py
SCons/Tool/SomeTool/__init__.py
./site_scons/site_tools/SomeTool.py
./site_scons/site_tools/SomeTool/__init__.py
7.4.2. 为 toolpath 提供外部目录

在某些情况下,您可能希望指定不同的位置来搜索工具。Environment 构造函数包含一个名为 toolpath 的选项,可用于添加额外的搜索目录。

python

# 位于 toolpath 目录选项中的工具
env = Environment(tools=['SomeTool'], toolpath=['/opt/SomeToolPath', '/opt/SomeToolPath2'])
env.SomeTool(targets, sources)# 此示例中的搜索位置将包括:
/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
7.4.3. toolpath 中的嵌套工具

SCons 3.0 现在支持构建器位于 toolpath 的子目录 / 子包中的功能。这类似于 Python 中的命名空间。使用嵌套或命名空间工具,我们可以使用点符号来指定工具所在的子目录。

python

# 命名空间目标
env = Environment(tools=['SubDir1.SubDir2.SomeTool'], toolpath=['/opt/SomeToolPath'])
env.SomeTool(targets, sources)# 在此示例中,搜索位置将包括
/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

对于 Python 2,在子目录中创建工具时需要注意,每个目录中都需要有一个 __init__.py 文件。这个文件可以是空的。这与 Python 从子目录(包)加载模块时使用的约束相同。对于 Python 3,这似乎不再是必需的。

7.4.4. 在 toolpath 中使用 sys.path

如果我们想在 SCons 外部的 sys.path 上访问工具(例如通过 pip 包管理器安装的工具),一种方法是在 toolpath 中使用 sys.path。使用这种方法需要注意的一点是,sys.path 有时可能包含指向 .egg 文件的路径而不是目录。因此,我们需要用这种方法过滤掉那些路径。

python

# 在 toolpath 中使用 sys.path 的命名空间目标
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)

通过在 toolpath 参数中使用 sys.path 并使用嵌套语法,我们可以让 SCons 在通过 pip 安装的包中搜索工具。

python

# 对于 Windows,根据 Python 版本和安装目录,可能是这样的路径
C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool.py
C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool\__init__.py# 对于 Linux,可能是这样的路径
/usr/lib/python3/dist-packages/someinstalledpackage/SomeTool.py
/usr/lib/python3/dist-packages/someinstalledpackage/SomeTool/__init__.py
7.4.5. 使用 PyPackageDir 函数添加到 toolpath

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

python

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(modulename) 函数来定位 Python 包的目录。PyPackageDir 返回一个 Dir 对象,表示作为参数指定的 Python 包 / 模块的目录路径。

python

# 使用 sys.path 的命名空间目标
env = Environment(tools=['SomeTool'], toolpath=[PyPackageDir('tools_example.subdir1.subdir2')])
env.SomeTool(targets, sources)

第 8 章。自动将命令行选项放入构建变量

本章介绍构建环境的 MergeFlagsParseFlags 和 ParseConfig 方法。

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

SCons 构建环境有一个 MergeFlags 方法,它将一个值的字典合并到构建环境中。MergeFlags 将字典中的每个值视为一个选项列表,就像传递给命令(如编译器或链接器)的选项一样。如果选项已经存在于构建环境变量中,MergeFlags 不会重复添加。

MergeFlags 在合并选项时会智能处理。当合并选项到任何名称以 PATH 结尾的变量时,MergeFlags 会保留选项的最左出现项,因为在典型的目录路径列表中,第一个出现的项 “胜出”。当合并选项到其他变量名时,MergeFlags 会保留选项的最右出现项,因为在典型的命令行选项列表中,最后出现的项 “胜出”。

python

运行

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

输出:

plaintext

% scons -Q
['-option', '-O1', '-whatever', '-O3']
scons: `.' is up to date.

请注意,$CCFLAGS 的默认值是一个 SCons 内部对象,它会自动将我们指定为字符串的选项转换为列表。

python

运行

env = Environment()
env.Append(CPPPATH = ['/include', '/usr/local/include', '/usr/include'])
flags = { 'CPPPATH' : ['/usr/opt/include', '/usr/local/include'] }
env.MergeFlags(flags)
print(env['CPPPATH'])

输出:

plaintext

% scons -Q
['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']
scons: `.' is up to date.

请注意,$CPPPATH 的默认值是一个普通的 Python 列表,所以我们必须在传递给 MergeFlags 函数的字典中把它的值指定为列表。

如果传递给 MergeFlags 的不是字典,它会调用 ParseFlags 方法将其转换为字典。

python

运行

env = Environment()
env.Append(CCFLAGS='-option -O3 -O1')
env.Append(CPPPATH=['/include', '/usr/local/include', '/usr/include'])
env.MergeFlags('-whatever -I/usr/opt/include -O3 -I/usr/local/include')
print(env['CCFLAGS'])
print(env['CPPPATH'])

输出:

plaintext

% scons -Q
['-option', '-O1', '-whatever', '-O3']
['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']
scons: `.' is up to date.

在上面的组合示例中,ParseFlags 会将选项分类到相应的变量中,并返回一个字典,供 MergeFlags 应用到指定构建环境的构建变量中。

8.2. 将编译参数分离到各自的变量中:ParseFlags 函数

在构建程序时,SCons 有一系列令人眼花缭乱的构建变量用于不同类型的选项。有时你可能不确定对于某个特定选项应该使用哪个变量。

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

ParseFlags 返回一个字典,其中包含分配到各自构建变量中的选项。通常,这个字典会传递给 MergeFlags,将选项合并到构建环境中,但如果需要提供额外功能,也可以编辑这个字典。(请注意,如果不打算编辑这些标志,直接使用选项调用 MergeFlags 将避免额外的步骤。)

python

运行

from __future__ import print_function
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')

输出:

plaintext

% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo

请注意,如果选项仅限于像上面那样的通用类型,它们将被正确转换为其他平台类型:

plaintext

C:\>scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cl /Fof1.obj /c f1.c /nologo /I\opt\include
link /nologo /OUT:f1.exe /LIBPATH:\opt\lib foo.lib f1.obj
embedManifestExeCheck(target, source, env)

由于假设这些标志用于 GCC 工具链,无法识别的标志将被放置在 $CCFLAGS 中,所以它们将用于 C 和 C++ 编译:

python

运行

from __future__ import print_function
env = Environment()
d = env.ParseFlags("-whatever")
for k,v in sorted(d.items()):if v:print(k, v)
env.MergeFlags(d)
env.Program('f1.c')

输出:

plaintext

% scons -Q
CCFLAGS -whatever
cc -o f1.o -c -whatever f1.c
cc -o f1 f1.o

ParseFlags 也接受一个(递归的)字符串列表作为输入;在处理字符串之前,列表会被展平:

python

运行

from __future__ import print_function
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')

输出:

plaintext

% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo

如果一个字符串以 “!”(感叹号,通常称为 “bang”)开头,该字符串将被传递给 shell 执行。然后解析命令的输出:

python

运行

from __future__ import print_function
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')

输出:

plaintext

% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo

ParseFlags 会定期更新以识别新的选项;有关当前识别的选项的详细信息,请查阅手册页。

8.3. 查找已安装库的信息:ParseConfig 函数

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

较新的惯例是,这些选项可以从通用的 pkg-config 程序中获得,该程序具有通用的框架、错误处理等,因此所有包创建者需要做的就是为他的特定包提供一组字符串。

SCons 构建环境有一个 ParseConfig 方法,它执行一个 config 实用工具(可以是 pkg-config 或更特定的实用工具),并根据指定命令返回的命令行选项在环境中配置适当的构建变量。

python

运行

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

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

plaintext

% scons -Q
['/lib/compat', '/usr/X11/include']
scons: `.' is up to date.

在上面的示例中,SCons 将包含目录添加到了 CPPPATH 中。(根据 pkg-config 命令发出的其他标志,其他变量也可能会被扩展。)

请注意,选项会使用 MergeFlags 方法与现有选项合并,所以每个选项在构建变量中只会出现一次:

python

运行

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

输出:

plaintext

% scons -Q
['/usr/X11/include']
scons: `.' is up to date.

第 9 章。控制构建输出

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

9.1. 提供构建帮助:Help 函数

能够向用户提供一些描述特定目标、构建选项等的帮助信息通常非常有用,这些信息可用于您的构建。SCons 提供了 Help 函数,允许您指定此帮助文本:

python

运行

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

也可以指定 append 标志:

python

运行

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

(请注意上面使用的 Python 三引号语法,对于指定像帮助文本这样的多行字符串非常方便。)

当 SConstruct 或 SConscript 文件中包含对 Help 函数的调用时,指定的帮助文本将在响应 SCons -h 选项时显示:

plaintext

% scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program,'scons debug' to build the debug version.Use scons -H for help about command-line options.

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

当与 AddOption 一起使用时,Help("text", append=False) 将覆盖与 AddOption() 相关的任何帮助输出。要保留 AddOption() 的帮助输出,请设置 append=True

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

python

运行

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

在 Windows 上,将显示完整的帮助文本:

plaintext

C:\>scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program.Type: 'scons windebug' to build the Windows debug version.Use scons -H for help about command-line options.

但在 Linux 或 UNIX 系统上,将只显示相关选项:

plaintext

% scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program.Use scons -H for help about command-line options.

如果 SConstruct 或 SConscript 文件中没有 Help 文本,SCons 将恢复显示其标准列表,该列表描述了 SCons 命令行选项。无论何时使用 -H 选项,此列表也会始终显示。

9.2. 控制 SCons 打印构建命令的方式:$*COMSTR 变量

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

例如,假设您希望 SCons 在编译目标文件时显示 “Compiling” 消息,在链接可执行文件时显示 “Linking” 消息。您可以编写一个 SConstruct 文件,如下所示:

python

运行

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

这将产生以下输出:

plaintext

% scons -Q
Compiling foo.o
Linking foo

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

当然,有时能够查看 SCons 为构建目标执行的确切命令仍然很重要。例如,您可能只是需要验证 SCons 是否配置为向编译器提供正确的选项,或者开发人员可能希望剪切并粘贴编译命令以添加一些自定义测试的选项。

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

python

运行

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

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

plaintext

% scons -Q
Compiling foo.o
Linking foo
% scons -Q -c
Removed foo.o
Removed foo
% scons -Q VERBOSE=1
cc -o foo.o -c foo.c
cc -o foo foo.o
9.3. 提供构建进度输出:Progress 函数

提供良好构建输出的另一个方面是向用户反馈 SCons 在做什么,即使当前没有进行任何构建。对于大型构建尤其如此,当大多数目标已经是最新的时候。因为 SCons 可能需要很长时间来绝对确保每个目标相对于许多依赖文件都是最新的,所以用户很容易错误地认为 SCons 挂起了或者构建存在其他问题。

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

python

运行

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

请注意,Progress 函数不会像 Python 的 print 语句那样自动在字符串末尾打印换行符,我们必须指定我们希望在配置字符串末尾打印的 \n。这样配置后,当 SCons 遍历依赖图时,它将为遇到的每个文件打印 “Evaluating”:

plaintext

% scons -Q
Evaluating SConstruct
Evaluating f1.c
Evaluating f1.o
cc -o f1.o -c f1.c
Evaluating f1
cc -o f1 f1.o
Evaluating f2.c
Evaluating f2.o
cc -o f2.o -c f2.c
Evaluating f2
cc -o f2 f2.o
Evaluating.

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

python

运行

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

【这段先看英文吧。。。】

Note that we also specified the overwrite=True argument to the Progress function, which causes SCons to "wipe out" the previous string with space characters before printing the next Progress string. Without the overwrite=True argument, a shorter file name would not overwrite all of the charactes in a longer file name that precedes it, making it difficult to tell what the actual file name is on the output. Also note that we opened up the /dev/tty file for direct access (on POSIX) to the user's screen. On Windows, the equivalent would be to open the con: file name.

Also, it's important to know that although you can use $TARGET to substitute the name of the node in the string, the Progress function does not perform general variable substitution (because there's not necessarily a construction environment involved in evaluating a node like a source file, for example).

You can also specify a list of strings to the Progress function, in which case SCons will display each string in turn. This can be used to implement a "spinner" by having SCons cycle through a sequence of strings:

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

请注意,在这里我们还使用了 interval= 关键字参数,使得 SCons 仅在每评估五个节点时打印一个新的 “旋转” 字符串。使用 interval= 计数,即使是像上面示例中使用 $TARGET 的字符串,也是减少 SCons 打印进度字符串的工作量的好方法,同时仍然向用户反馈 SCons 仍在评估构建。

最后,你可以通过将 Python 函数(或其他可调用的 Python 对象)传递给 Progress 函数,直接控制如何打印每个评估的节点。你的函数将针对每个评估的节点被调用,使你能够实现更复杂的逻辑,例如添加计数器:

python

运行

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

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

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

9.4. 打印详细的构建状态:GetBuildFailures 函数

SCons 和大多数构建工具一样,构建成功时向 shell 返回状态码 0,失败时返回非零状态码。有时在运行结束时提供有关构建状态的更多信息是很有用的,例如打印一条信息性消息、发送一封电子邮件,或者通知那个破坏构建的可怜家伙。

SCons 提供了 GetBuildFailures 方法,你可以在 Python 的 atexit 函数中使用它来获取一个对象列表,这些对象描述了在尝试构建目标时失败的操作。如果你使用 -j,可能会有多个失败的操作。下面是一个简单的示例:

python

运行

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

atexit.register 调用将 print_build_failures 注册为一个 atexit 回调函数,在 SCons 退出之前被调用。当该函数被调用时,它调用 GetBuildFailures 来获取失败对象的列表。有关返回对象的详细内容,请参阅手册页;一些更有用的属性是 .node.errstr.filename 和 .commandfilename 不一定与 node 是同一个文件;node 是发生错误时正在构建的目标,而 filename 是实际导致错误的文件或目录。注意:只能在构建结束时调用 GetBuildFailures;在其他任何时候调用它都是未定义的。

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

python

运行

# 如果在命令行上传递了 fail=1,则使构建失败
if ARGUMENTS.get('fail', 0):Command('target', 'source', ['/bin/false'])def bf_to_str(bf):"""以有用的方式将 GetBuildFailures() 的一个元素转换为字符串。"""import SCons.Errorsif bf is None:  # 未知目标在列表中产生 Nonereturn '(unknown tgt)'elif isinstance(bf, SCons.Errors.StopError):return str(bf)elif bf.node:return str(bf.node) + ': ' + bf.errstrelif bf.filename:return bf.filename + ': ' + bf.errstrreturn 'unknown failure: ' + bf.errstr
import atexitdef build_status():"""将构建状态转换为一个二元组,(status, msg)。"""from SCons.Script import GetBuildFailuresbf = GetBuildFailures()if bf:# bf 通常是一个构建失败的列表;如果一个元素是 None,# 是因为存在 scons 一无所知的目标。status = 'failed'failures_message = "\n".join(["Failed building %s" % bf_to_str(x)for x in bf if x is not None])else:# 如果 bf 是 None,构建成功完成。status = 'ok'failures_message = ''return (status, failures_message)def display_build_status():"""显示构建状态。由 atexit 调用。在这里你可以做各种复杂的事情。"""status, failures_message = build_status()if status == 'failed':print("FAILED!!!!")  # 可以显示警报、发出铃声等。elif status == 'ok':print("Build succeeded.")print(failures_message)atexit.register(display_build_status)

当运行此代码时,你将看到相应的输出:

plaintext

% scons -Q
scons: `.' is up to date.
Build succeeded.
% scons -Q fail=1
scons: *** [target] Source `source' not found, needed by target `target'.
FAILED!!!!
Failed building target: Source `source' not found, needed by target `target'.

第 10 章。从命令行控制构建

SCons 为 SConscript 文件的编写者提供了多种方法,让运行 SCons 的用户能够对构建执行进行大量控制。用户可以在命令行上指定的参数分为以下三种类型:

选项

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

变量

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

目标

任何既不是选项也不是变量设置(不以连字符开头且不包含等号)的命令行参数都被视为用户(大概)希望 SCons 构建的目标。这是一个表示要构建的目标的 Node 对象列表。SCons 提供了对指定目标列表的访问,以及从 SConscript 文件中设置默认目标列表的方法。请参阅下面的 10.3 节 “命令行目标”。

10.1. 命令行选项

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

10.1.1. 无需每次都指定命令行选项:SCONSFLAGS 环境变量

用户可能会发现每次运行 SCons 时都要提供相同的命令行选项。例如,你可能会发现指定 -j 2 的值以使 SCons 并行运行最多两个构建命令可以节省时间。为了避免每次都手动输入 -j 2,你可以将外部环境变量 SCONSFLAGS 设置为一个包含你希望 SCons 使用的命令行选项的字符串。

例如,如果你使用的是与 Bourne shell 兼容的 POSIX shell,并且你总是希望 SCons 使用 -Q 选项,你可以如下设置 SCONSFLAGS 环境变量:

plaintext

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...... [build output] ...
scons: done building targets.
% export SCONSFLAGS="-Q"
% scons... [build output] ...

在 POSIX 系统上使用 csh 风格 shell 的用户可以如下设置 SCONSFLAGS 环境变量:

plaintext

$ setenv SCONSFLAGS "-Q"

Windows 用户通常可以在系统属性窗口的相应选项卡中设置 SCONSFLAGS

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

SCons 提供了 GetOption 函数来获取由各种命令行选项设置的值。此函数的一个常见用途是检查是否指定了 -h 或 --help 选项。通常,SCons 在读取所有 SConscript 文件之后才会打印其帮助文本,因为在源树层次结构中的某个子 SConscript 文件中可能添加了帮助文本。当然,读取所有 SConscript 文件会花费额外的时间。

如果你知道你的配置在子 SConscript 文件中没有定义任何额外的帮助文本,你可以通过使用 GetOption 函数来加快为用户提供的命令行帮助,仅在用户未指定 -h 或 --help 选项时加载子 SConscript 文件,如下所示:

python

运行

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

一般来说,你传递给 GetOption 函数以获取命令行选项设置值的字符串与 “最常见的” 长选项名(以两个连字符开头)相同,尽管也有一些例外。SCons 命令行选项列表以及用于获取这些选项的 GetOption 字符串,请参阅下面的 10.1.4 节 “用于获取或设置 SCons 命令行选项值的字符串”。

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

你还可以在 SConscript 文件中使用 SetOption 函数来设置 SCons 命令行选项的值。用于设置 SCons 命令行选项值的字符串,请参阅下面的 10.1.4 节 “用于获取或设置 SCons 命令行选项值的字符串”。

SetOption 函数的一个用途是为 -j 或 --jobs 选项指定一个值,这样用户无需手动指定该选项即可获得并行构建的性能提升。一个复杂的因素是 -j 选项的最佳值在一定程度上取决于系统。一个大致的指导原则是,系统的处理器越多,你希望设置的 -j 值越高,以便利用 CPU 的数量。

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

python

运行

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 环境变量中指定的值。(这是字符串拼写与命令行选项不同的例外情况之一。由于历史原因,用于获取或设置 --jobs 值的字符串是 num_jobs。)此示例中的代码打印 num_jobs 值用于说明目的。它使用默认值 2 来提供一些最小的并行性,即使在单处理器系统上也是如此:

plaintext

% scons -Q
running with -j 2
scons: `.' is up to date.

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

plaintext

% export NUM_CPU="4"
% scons -Q
running with -j 4
scons: `.' is up to date.

但是用户在命令行上指定的任何显式 -j 或 --jobs 值将优先使用,无论是否设置了 $NUM_CPU 环境变量:

plaintext

% scons -Q -j 7
running with -j 7
scons: `.' is up to date.
% export NUM_CPU="4"
% scons -Q -j 3
running with -j 3
scons: `.' is up to date.
10.1.4. 用于获取或设置 SCons 命令行选项值的字符串

你可以传递给 GetOption 和 SetOption 函数的字符串通常对应于第一个长格式选项名(以两个连字符开头:--),将任何剩余的连字符字符替换为下划线。

字符串的完整列表以及它们对应的变量如下:

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

SCons 还允许你使用 AddOption 函数定义自己的命令行选项。AddOption 函数接受与标准 Python 库模块 optparse 中的 add_option 方法相同的参数。

一旦你使用 AddOption 函数添加了自定义命令行选项,该选项的值(如果有)可以立即使用标准的 GetOption 函数获取。目前不支持对使用 AddOption 添加的选项使用 SetOption

使用此功能的一个有用示例是为用户提供 --prefix 选项:

python

运行

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/ 目录中:

plaintext

% scons -Q -n
Install file: "foo.in" as "/usr/bin/foo.in"

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

plaintext

% scons -Q -n --prefix=/tmp/install
Install file: "foo.in" as "/tmp/install/usr/bin/foo.in"

注意:
与长选项用空格分隔的选项参数(而不是用 = 分隔),scons 无法正确解析。虽然 --input=ARG 显然是 opt后跟 arg,但对于 --input ARG,在没有说明的情况下无法判断 ARG 是属于 input 选项的参数还是位置参数。scons 将位置参数视为可在 SConscript 中使用的命令行构建选项或命令行目标(有关详细信息,请参阅紧随其后的部分)。因此,必须在处理 SConscript 之前收集它们。由于提供解析任何歧义的处理指令的 AddOption 调用发生在 SConscript 中,scons 无法及时知道以这种方式添加的选项,并且会发生意外情况,例如将选项参数分配为目标和 / 或由于缺少选项参数而引发异常。

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

10.2. 命令行 variable=value 构建变量

10.2. 命令行 variable=value 构建变量

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

% scons -Q debug=1

SCons 提供了一个 ARGUMENTS 字典,用于存储来自命令行的所有 variable=value 赋值。这使你能够根据命令行上的规范修改构建的各个方面。(请注意,除非你希望要求用户始终指定一个变量,否则你可能需要使用 Python 的 ARGUMENTS.get () 函数,该函数允许你指定一个默认值,以便在命令行上未指定时使用。)

以下代码根据 ARGUMENTS 字典中设置的 debug 标志来设置 $CCFLAGS 构建变量:

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

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

% scons -Q debug=0
cc -o prog.o -c prog.c
cc -o prog prog.o
% scons -Q debug=0
scons: . 是最新的。
% scons -Q debug=1
cc -o prog.o -c -g prog.c
cc -o prog prog.o
% scons -Q debug=1
scons: . 是最新的。

请注意,SCons 会跟踪用于构建目标文件的最后一个值,因此只有当 debug 参数的值发生变化时,才会正确地重新构建目标文件和可执行文件。

ARGUMENTS 字典有两个小缺点。首先,因为它是一个字典,所以每个指定的关键字只能存储一个值,因此只会 “记住” 命令行上每个关键字的最后一个设置。如果用户应该能够为给定的关键字在命令行上指定多个值,那么 ARGUMENTS 字典就不适用。其次,它不保留指定变量设置的顺序,如果希望配置根据命令行上指定构建变量设置的顺序表现不同,这就会成为一个问题。

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

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

会产生以下输出:

% scons -Q define=FOO
cc -o prog.o -c -DFOO prog.c
% scons -Q define=FOO define=BAR
cc -o prog.o -c -DFOO -DBAR prog.c

请注意,ARGLIST 和 ARGUMENTS 变量不会相互干扰,只是提供了不同的视角来查看用户在命令行上指定的 variable=value 设置。你可以在同一个 SCons 配置中同时使用这两个变量。一般来说,ARGUMENTS 字典使用起来更方便(因为你可以通过字典访问来获取变量设置),而 ARGLIST 列表更灵活(因为你可以检查用户命令行变量设置的特定顺序)。
10.2.1. 控制命令行构建变量

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

例如,假设你希望用户在为发布版本构建程序时在命令行上设置 RELEASE 构建变量,并且该变量的值应该通过适当的 -D 选项(或其他命令行选项)添加到命令行,以便将值传递给 C 编译器。以下是通过在 $CPPDEFINES 构建变量的字典中设置适当的值来实现的方法:

vars = Variables (None, ARGUMENTS)
vars.Add ('RELEASE', ' 设置为 1 以构建发布版本 ', 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(Add 方法的第三个参数)。第二个参数是一行帮助文本;我们将在下一节中学习如何使用它。

然后,我们将创建的 Variables 对象作为 variables 关键字参数传递给用于创建构建环境的 Environment 调用。这允许用户在命令行上设置 RELEASE 构建变量,并使该变量显示在用于从 C 源文件构建每个对象的命令行中:

% scons -Q RELEASE=1
cc -o bar.o -c -DRELEASE_BUILD=1 bar.c
cc -o foo.o -c -DRELEASE_BUILD=1 foo.c
cc -o foo foo.o bar.o

注意:在 SCons 版本 0.98.1 之前,这些构建变量被称为 “命令行构建选项”。该类实际上被命名为 Options 类,并且在下面的部分中,各种函数被命名为 BoolOption、EnumOption、ListOption、PathOption、PackageOption 和 AddOptions。这些旧名称仍然有效,并且你可能会在较旧的 SConscript 文件中遇到它们,但自 SCons 2.0 版本起它们已被正式弃用。
10.2.2. 为命令行构建变量提供帮助

为了使命令行构建变量最有用,理想情况下,你希望提供一些帮助文本,以便在用户运行 scons -h 时描述可用的变量。你可以手动编写此文本,但 SCons 提供了一种更简单的方法。Variables 对象支持 GenerateHelpText 方法,顾名思义,该方法会生成描述已添加到其中的各种变量的文本。然后,你将此方法的输出传递给 Help 函数:

vars = Variables (None, ARGUMENTS)
vars.Add ('RELEASE', ' 设置为 1 以构建发布版本 ', 0)
env = Environment (variables = vars)
Help (vars.GenerateHelpText (env))

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

% scons -Q -h

RELEASE:设置为 1 以构建发布版本
默认值:0
当前值:0

使用 scons -H 获取有关命令行选项的帮助。
10.2.3. 从文件读取构建变量

为用户提供一种在命令行上指定构建变量值的方法很有用,但如果用户每次运行 SCons 时都必须指定该变量,可能会很麻烦。我们可以通过在创建 Variables 对象时提供文件名,让用户在本地文件中提供自定义的构建变量设置:

vars = Variables ('custom.py')
vars.Add ('RELEASE', ' 设置为 1 以构建发布版本 ', 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 时:

% scons -Q
cc -o bar.o -c -DRELEASE_BUILD=1 bar.c
cc -o foo.o -c -DRELEASE_BUILD=1 foo.c
cc -o foo foo.o bar.o

如果我们将 custom.py 的内容更改为:

RELEASE = 0

目标文件将使用新变量进行适当的重新构建:

% scons -Q
cc -o bar.o -c -DRELEASE_BUILD=0 bar.c
cc -o foo.o -c -DRELEASE_BUILD=0 foo.c
cc -o foo foo.o bar.o
10.2.4. 预定义的构建变量函数

SCons 提供了许多函数,为各种类型的命令行构建变量提供现成的行为。
10.2.4.1. 布尔值:BoolVariable 构建变量函数

能够指定一个控制简单布尔变量(值为真或假)的变量通常很方便。如果能够适应不同用户对表示真或假值的不同偏好,那就更方便了。BoolVariable 函数可以轻松适应这些常见的真或假表示。

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

vars = Variables ('custom.py')
vars.Add (BoolVariable ('RELEASE', ' 设置为构建发布版本 ', 0))
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
env.Program ('foo.c')

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

% scons -Q RELEASE=yes foo.o
cc -o foo.o -c -DRELEASE_BUILD=True foo.c

% scons -Q RELEASE=t foo.o
cc -o foo.o -c -DRELEASE_BUILD=True foo.c

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

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

% scons -Q RELEASE=no foo.o
cc -o foo.o -c -DRELEASE_BUILD=False foo.c

% scons -Q RELEASE=f foo.o
cc -o foo.o -c -DRELEASE_BUILD=False foo.c

其他等同于假的值包括 n、0、off 和 none。

最后,如果用户尝试指定任何其他值,SCons 将提供适当的错误消息:

% scons -Q RELEASE=bad_value foo.o

scons: *** 转换选项时出错:RELEASE
布尔选项的无效值:bad_value
文件 “/home/my/project/SConstruct”,第 4 行,在 <module> 中
10.2.4.2. 从列表中选择单个值:EnumVariable 构建变量函数

假设我们希望用户能够设置一个 COLOR 变量,用于选择应用程序显示的背景颜色,但我们希望将选择限制在一组特定的允许颜色中。可以使用 EnumVariable 轻松设置,它除了变量名称、默认值和帮助文本参数外,还接受一个 allowed_values 列表:

vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 设置背景颜色 ', 'red',
allowed_values=('red', 'green', 'blue')))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')

现在用户可以明确地将 COLOR 构建变量设置为任何指定的允许值:

% scons -Q COLOR=red foo.o
cc -o foo.o -c -DCOLOR="red" foo.c
% scons -Q COLOR=blue foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=green foo.o
cc -o foo.o -c -DCOLOR="green" foo.c

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

% scons -Q COLOR=magenta foo.o

scons: *** 选项 COLOR 的值无效:magenta。有效值为:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中

EnumVariable 函数还支持一种将备用名称映射到允许值的方法。例如,假设我们希望允许用户使用单词 navy 作为 blue 的同义词。我们可以通过添加一个映射字典来实现,该字典将其键值映射到所需的合法值:

vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 设置背景颜色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'}))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')

如预期的那样,用户可以在命令行上使用 navy,并且 SCons 在使用 COLOR 变量构建目标时会将其转换为 blue:

% scons -Q COLOR=navy foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c

默认情况下,使用 EnumVariable 函数时,与合法值仅在大小写方面不同的参数被视为非法值:

% scons -Q COLOR=Red foo.o

scons: *** 选项 COLOR 的值无效:Red。有效值为:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
% scons -Q COLOR=BLUE foo.o

scons: *** 选项 COLOR 的值无效:BLUE。有效值为:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
% scons -Q COLOR=nAvY foo.o

scons: *** 选项 COLOR 的值无效:nAvY。有效值为:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中

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

vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 设置背景颜色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'},
ignorecase=1))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')

会产生以下输出:

% scons -Q COLOR=Red foo.o
cc -o foo.o -c -DCOLOR="Red" foo.c
% scons -Q COLOR=BLUE foo.o
cc -o foo.o -c -DCOLOR="BLUE" foo.c
% scons -Q COLOR=nAvY foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=green foo.o
cc -o foo.o -c -DCOLOR="green" foo.c

请注意,ignorecase 值为 1 时会保留用户提供的大小写拼写。如果你希望 SCons 无论用户使用的大小写如何都将名称转换为小写,请指定 ignorecase 值为 2:

vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 设置背景颜色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'},
ignorecase=2))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')

现在 SCons 将使用 red、green 或 blue 的值,无论用户在命令行上如何拼写这些值:

% scons -Q COLOR=Red foo.o
cc -o foo.o -c -DCOLOR="red" foo.c
% scons -Q COLOR=nAvY foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=GREEN foo.o
cc -o foo.o -c -DCOLOR="green" foo.c
10.2.4.3. 从列表中选择多个值:ListVariable 构建变量函数

你可能希望允许用户控制构建变量的另一种方式是指定一个或多个合法值的列表。SCons 通过 ListVariable 函数支持这种方式。例如,如果我们希望用户能够将 COLORS 变量设置为一个或多个合法值列表中的值:

vars = Variables ('custom.py')
vars.Add (ListVariable ('COLORS', ' 颜色列表 ', 0,
['red', 'green', 'blue']))
env = Environment (variables = vars,
CPPDEFINES={'COLORS' : '"${COLORS}"'})
env.Program ('foo.c')

现在用户可以指定一个用逗号分隔的合法值列表,这些值将被转换为用空格分隔的列表,以便传递给任何构建命令:

% scons -Q COLORS=red,blue foo.o
cc -o foo.o -c -DCOLORS="red blue" foo.c
% scons -Q COLORS=blue,green,red foo.o
cc -o foo.o -c -DCOLORS="blue green red" foo.c

此外,ListVariable 函数允许用户分别指定 all 或 none 关键字来选择所有合法值或不选择任何值:

% scons -Q COLORS=all foo.o
cc -o foo.o -c -DCOLORS="red green blue" foo.c
% scons -Q COLORS=none foo.o
cc -o foo.o -c -DCOLORS="" foo.c

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

% scons -Q COLORS=magenta foo.o

scons: *** 转换选项时出错:COLORS
选项的无效值:magenta
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中


10.2.4.4. 路径名:PathVariable 构建变量函数

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

SCons 支持 PathVariable 函数,以便轻松创建一个构建变量来控制预期的路径名称。例如,如果您需要在预处理器中定义一个变量来控制配置文件的位置:

vars = Variables ('custom.py')
vars.Add (PathVariable ('CONFIG',
' 配置文件的路径 ',
'/etc/my_config'))
env = Environment (variables = vars,
CPPDEFINES={'CONFIG_FILE' : '"$CONFIG"'})
env.Program ('foo.c')

这样一来,用户可以根据需要在命令行上覆盖 CONFIG 构建变量:

% scons -Q foo.o
cc -o foo.o -c -DCONFIG_FILE="/etc/my_config" foo.c
% scons -Q CONFIG=/usr/local/etc/other_config foo.o
scons: `foo.o' 是最新的。

默认情况下,PathVariable 会检查指定的路径是否存在,如果不存在则生成一个错误:

% scons -Q CONFIG=/does/not/exist foo.o

scons: *** 选项 CONFIG 的路径不存在: /does/not/exist
文件 "/home/my/project/SConstruct", 第 6 行,在 <module> 中

PathVariable 提供了一些方法,您可以使用这些方法来改变这种行为。如果您希望确保任何指定的路径实际上是文件而不是目录,可以使用 PathVariable.PathIsFile 方法:

vars = Variables ('custom.py')
vars.Add (PathVariable ('CONFIG',
' 配置文件的路径 ',
'/etc/my_config',
PathVariable.PathIsFile))
env = Environment (variables = vars,
CPPDEFINES={'CONFIG_FILE' : '"$CONFIG"'})
env.Program ('foo.c')

相反,为了确保任何指定的路径是目录而不是文件,可以使用 PathVariable.PathIsDir 方法:

vars = Variables ('custom.py')
vars.Add (PathVariable ('DBDIR',
' 数据库目录的路径 ',
'/var/my_dbdir',
PathVariable.PathIsDir))
env = Environment (variables = vars,
CPPDEFINES={'DBDIR' : '"$DBDIR"'})
env.Program ('foo.c')

如果您希望确保任何指定的路径是目录,并且如果目录不存在则创建该目录,可以使用 PathVariable.PathIsDirCreate 方法:

vars = Variables ('custom.py')
vars.Add (PathVariable ('DBDIR',
' 数据库目录的路径 ',
'/var/my_dbdir',
PathVariable.PathIsDirCreate))
env = Environment (variables = vars,
CPPDEFINES={'DBDIR' : '"$DBDIR"'})
env.Program ('foo.c')

最后,如果您不在乎路径是否存在,是文件还是目录,可以使用 PathVariable.PathAccept 方法来接受用户提供的任何路径:

vars = Variables ('custom.py')
vars.Add (PathVariable ('OUTPUT',
' 输出文件或目录的路径 ',
None,
PathVariable.PathAccept))
env = Environment (variables = vars,
CPPDEFINES={'OUTPUT' : '"$OUTPUT"'})
env.Program ('foo.c')

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

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

vars = Variables ('custom.py')
vars.Add (PackageVariable ('PACKAGE',
' 包的位置 ',
'/opt/location'))
env = Environment (variables = vars,
CPPDEFINES={'PACKAGE' : '"$PACKAGE"'})
env.Program ('foo.c')

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

% scons -Q foo.o
cc -o foo.o -c -DPACKAGE="/opt/location" foo.c
% scons -Q PACKAGE=/usr/local/location foo.o
cc -o foo.o -c -DPACKAGE="/usr/local/location" foo.c
% scons -Q PACKAGE=yes foo.o
cc -o foo.o -c -DPACKAGE="True" foo.c
% scons -Q PACKAGE=no foo.o
cc -o foo.o -c -DPACKAGE="False" foo.c

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

最后,SCons 提供了一种方法,可以一次性向 Variables 对象添加多个构建变量。您无需多次调用 Add 方法,而是可以使用 AddVariables 方法,并传入要添加到对象的构建变量列表。每个构建变量可以指定为一个参数元组(就像您传递给 Add 方法本身的参数一样),或者指定为对预打包的命令行构建变量的预定义函数的调用。顺序任意:

vars = Variables ()
vars.AddVariables (
('RELEASE', ' 设置为 1 以构建发布版本 ', 0),
('CONFIG', ' 配置文件 ', '/etc/my_config'),
BoolVariable ('warnings', ' 使用 - Wall 等进行编译 ', 1),
EnumVariable ('debug', ' 调试输出和符号 ', 'no',
allowed_values=('yes', 'no', 'full'),
map={}, ignorecase=0), # 区分大小写
ListVariable ('shared',
' 要构建为共享库的库 ',
'all',
names = list_of_libs),
PackageVariable ('x11',
' 使用在此处安装的 X11(yes 表示在某些位置搜索)',
'yes'),
PathVariable ('qtdir', 'Qt 的安装根目录位置 ', qtdir),
)

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

当然,用户偶尔可能会在命令行设置中拼错变量名。对于用户在命令行上指定的任何未知变量,SCons 不会生成错误或警告。(这在很大程度上是因为您可能会直接使用 ARGUMENTS 字典来处理参数,因此在一般情况下,SCons 无法知道给定的 “拼错” 的变量是否真的是未知的,是否存在潜在问题,或者您的 SConscript 文件是否会用一些 Python 代码直接处理它。)

然而,如果您使用 Variables 对象来定义一组特定的命令行构建变量,并期望用户能够设置这些变量,那么如果用户提供的变量设置不在 Variables 对象已知的已定义变量名列表中,您可能希望提供自己的错误消息或警告。您可以通过调用 Variables 对象的 UnknownVariables 方法来实现这一点:

vars = Variables (None)
vars.Add ('RELEASE', ' 设置为 1 以构建发布版本 ', 0)
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
unknown = vars.UnknownVariables ()
if unknown:
print ("Unknown variables: % s"% unknown.keys ())
Exit (1)
env.Program ('foo.c')

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

% scons -Q NOT_KNOWN=foo
Unknown variables: ['NOT_KNOWN']

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

请注意,您必须在使用 Environment 调用的 variables = 关键字参数将 Variables 对象应用于构建环境之后,再调用 UnknownVariables。
10.3. 命令行目标
10.3.1. 获取命令行目标: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 目标时会生成警告消息:

% scons -Q
cc -o foo.o -c foo.c
cc -o foo foo.o
% scons -Q bar
Don't forget to copy `bar' to the archive!
cc -o bar.o -c bar.c
cc -o bar bar.o

COMMAND_LINE_TARGETS 变量的另一个实际用途可能是,如果请求了特定目标,则仅读取某些子 SConscript 文件来加速构建过程。
10.3.2. 控制默认目标:Default 函数

您可以控制 SCons 默认构建的目标,也就是说,当命令行上没有指定目标时。如前所述,除非您在命令行上显式指定一个或多个目标,否则 SCons 通常会构建当前目录中或其下的每个目标。然而,有时您可能希望指定默认情况下只构建某些程序或某些目录中的程序。您可以使用 Default 函数来实现这一点:

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

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

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: `hello' 是最新的。
% scons -Q goodbye
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o

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

% scons -Q .
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
cc -o hello.o -c hello.c
cc -o hello hello.o

您也可以多次调用 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 程序:

% scons -Q
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog3.o -c prog3.c
cc -o prog3 prog3.o
% scons -Q .
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o

您可以将一个目录作为参数传递给 Default:

env = Environment()
env.Program(['prog1/main.c', 'prog1/foo.c'])
env.Program(['prog2/main.c', 'prog2/bar.c'])
Default('prog1')

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

% scons -Q
cc -o prog1/foo.o -c prog1/foo.c
cc -o prog1/main.o -c prog1/main.c
cc -o prog1/main prog1/main.o prog1/foo.o
% scons -Q
scons: `prog1' 是最新的。
% scons -Q .
cc -o prog2/bar.o -c prog2/bar.c
cc -o prog2/main.o -c prog2/main.c
cc -o prog2/main prog2/main.o prog2/bar.o

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

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

这将产生如下的构建输出:

% scons -Q
scons: *** 没有指定目标,也没有找到 Default () 目标。停止。
未找到要构建的内容
% scons -Q .
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o

10.3.2.1. 获取默认目标列表: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 标志,这一点会很明显:)

% scons
scons: 正在读取 SConscript 文件...
DEFAULT_TARGETS is ['prog1']
scons: 完成读取 SConscript 文件。
scons: 正在构建目标...
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
scons: 完成构建目标。

其次,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])

这将产生如下输出:

% scons
scons: 正在读取 SConscript 文件...
DEFAULT_TARGETS is now ['prog1']
DEFAULT_TARGETS is now ['prog1', 'prog2']
scons: 完成读取 SConscript 文件。
scons: 正在构建目标...
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
scons: 完成构建目标。

实际上,这意味着您需要注意调用 Default 函数和引用 DEFAULT_TARGETS 列表的顺序,以确保在添加您期望在其中找到的默认目标之前,不会检查该列表。
10.3.3. 获取构建目标列表,无论目标来源如何:BUILD_TARGETS 变量

我们已经介绍了 COMMAND_LINE_TARGETS 变量,它包含在命令行上指定的目标列表,以及 DEFAULT_TARGETS 变量,它包含通过调用 Default 方法或函数指定的目标列表。然而,有时您想要一个 SCons 将尝试构建的目标列表,无论这些目标是来自命令行还是 Default 调用。您可以手动编写代码实现,如下所示:

if COMMAND_LINE_TARGETS:
targets = COMMAND_LINE_TARGETS
else:
targets = DEFAULT_TARGETS

不过,SCons 提供了一个方便的 BUILD_TARGETS 变量,无需进行这种手动操作。本质上,BUILD_TARGETS 变量包含命令行目标的列表(如果有指定的话),如果没有指定命令行目标,它包含通过 Default 方法或函数指定的目标列表。

因为 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 的值会根据命令行上是否指定了目标而变化 —— 只有在没有 COMMAND_LINE_TARGETS 时,BUILD_TARGETS 才会从 DEFAULT_TARGETS 中获取目标:

% scons -Q
BUILD_TARGETS is ['prog1']
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
% scons -Q prog2
BUILD_TARGETS is ['prog2']
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
% scons -Q -c .
BUILD_TARGETS is ['.']
删除 prog1.o
删除 prog1
删除 prog2.o
删除 prog2

[2] 实际上,AddOption 函数是使用 optparse.OptionParser 的一个子类来实现的。

第11章 向其他目录安装文件:安装构建器

程序构建完成后,通常需要将其安装到其他目录供公共使用。通过Install方法可将程序或其他文件复制到目标目录:

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

需注意,安装文件仍被视为一种"构建"行为。由于SCons默认在当前目录及其子目录下构建文件,若如示例所示需将文件安装到顶层SConstruct目录树之外的路径(如/usr/bin),必须显式指定目标目录(或更高级目录如/):

```bash
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q /usr/bin
Install file: "hello" as "/usr/bin/hello"
```

为避免记忆(和输入)具体安装路径的繁琐,可使用Alias函数创建伪目标。例如定义名为install的别名指向目标目录:

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

此时可通过更自然的方式安装程序:

```bash
% scons -Q install
Install file: "hello" as "/usr/bin/hello"
```

11.1 向目录安装多个文件

多次调用Install函数即可安装多个文件:

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

或更简洁地使用文件列表(与其他构建器用法一致):

```python
env.Install('/usr/bin', [hello, goodbye])
```

两种方式执行效果:

```bash
% scons -Q install
Install file: "goodbye" as "/usr/bin/goodbye"
Install file: "hello" as "/usr/bin/hello"
```

11.2 重命名安装文件

Install方法默认保留原文件名。如需重命名,应使用InstallAs函数:

```python
env.InstallAs('/usr/bin/hello-new', hello)
```

安装效果:

```bash
% scons -Q install
Install file: "hello" as "/usr/bin/hello-new"
```

11.3 批量重命名安装文件

安装多个重命名文件时,可多次调用InstallAs或使用等长列表:

```python
env.InstallAs(['/usr/bin/hello-new', '/usr/bin/goodbye-new'],
              [hello, goodbye])
```

执行时将按顺序对应复制:

```bash
% scons -Q install
Install file: "goodbye" as "/usr/bin/goodbye-new"
Install file: "hello" as "/usr/bin/hello-new"
```

11.4 安装版本化共享库

当通过SHLIBVERSION变量创建共享库时,SCons会自动生成版本符号链接。此时应使用InstallVersionedLib安装库及其链接:

```python
foo = env.SharedLibrary("foo", "foo.c", SHLIBVERSION="1.2.3")
env.InstallVersionedLib("lib", foo)
```

在Linux系统将生成libfoo.so.1.2.3及指向它的libfoo.so和libfoo.so.1符号链接。

第12章 跨平台文件系统操作

SCons提供多种跨平台操作工厂函数,用于复制/移动/删除文件目录等操作。这些工厂函数返回可延迟执行的动作对象。

12.1 复制文件/目录:Copy工厂

使用Copy工厂与Command构建器结合可实现文件复制:

```python
Command("file.out", "file.in", Copy("$TARGET", "$SOURCE"))
```

执行时自动展开变量:

```bash
% scons -Q
Copy("file.out", "file.in")
```

也可显式指定路径:

```python
Command("file.out", [], Copy("$TARGET", "file.in"))
```

Copy工厂特别适用于多步骤操作,例如处理需修改原文件的工具:

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

输出序列:

```bash
Copy("tempfile", "file.in")
modify tempfile
Copy("file.out", "tempfile")
```

第三个参数控制符号链接复制方式:
- `True`:浅复制为新链接
- `False`:复制链接目标实体

12.2 删除文件/目录:Delete工厂

Delete工厂用法类似Copy,可用于清理临时文件:

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

执行流程:

```bash
Delete("tempfile")
Copy("tempfile", "file.in")
modify tempfile
Copy("file.out", "tempfile")
```

注意:SCons默认会在执行操作前删除目标文件,通常无需显式调用Delete。特别要避免误用`Delete("$SOURCE")`。

12.3 移动/重命名文件:Move工厂

Move工厂可实现文件移动:

```python
Command("file.out", "file.in", [
    Copy("tempfile", "$SOURCE"),
    "modify tempfile",
    Move("$TARGET", "tempfile")
])
```

执行效果:

```bash
Copy("tempfile", "file.in")
modify tempfile
Move("file.out", "tempfile")
```

12.4 更新文件时间戳:Touch工厂

Touch工厂用于更新文件修改时间:

```python
Command("file.out", "file.in", [
    Copy("$TARGET", "$SOURCE"),
    Touch("$TARGET")
])
```

输出:

```bash
Copy("file.out", "file.in")
Touch("file.out")
```

12.5 创建目录:Mkdir工厂

Mkdir工厂创建目录,适合临时工作区场景:

```python
Command("file.out", "file.in", [
    Delete("tempdir"),
    Mkdir("tempdir"),
    Copy("tempdir/${SOURCE.file}", "$SOURCE"),
    "process tempdir",
    Move("$TARGET", "tempdir/output_file"),
    Delete("tempdir")
])
```

12.6 修改文件权限:Chmod工厂

Chmod工厂修改权限,参数应为八进制数:

```python
Command("file.out", "file.in", [
    Copy("$TARGET", "$SOURCE"),
    Chmod("$TARGET", 0o755)
])
```

执行:

```bash
Copy("file.out", "file.in")
Chmod("file.out", 0755)
```

12.7 立即执行操作:Execute函数

Execute函数可在读取SConscript时立即执行操作,例如确保目录存在:

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

输出显示立即执行:

```bash
scons: Reading SConscript files...
Mkdir("/tmp/my_temp_directory")
```

相比直接调用os.mkdir(),此方式会正确处理`-n`/`-q`参数。若需检查执行状态可通过返回值判断:

```python
if Execute(Mkdir('/tmp/my_temp_directory')):
    Exit(1)
```

[注3] 因历史原因Copy被用于环境复制函数,否则本应是实现文件复构建器的理想名称。

第13章 控制目标文件删除

SCons在两种情况下会删除目标文件:1) 重建前删除旧版本 2) 使用`-c`清理时。Precious和NoClean函数可分别抑制这两种行为。

13.1 防止构建时删除:Precious函数

默认情况下SCons在重建前删除目标。对于需增量更新的库文件,可通过Precious保护:

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

此时SCons不会删除原有库文件:

```bash
ar rc libfoo.a f1.o f2.o f3.o
```

但`-c`清理时仍会删除Precious标记的文件。

13.2 防止清理时删除:NoClean函数

NoClean可防止`-c`清理时删除指定目标,例如保留最终库文件:

```python
env.NoClean(lib)
```

清理时将保留库文件:

```bash
Removed f1.o
Removed f2.o
Removed f3.o
```

13.3 清理附加文件:Clean函数

Clean函数可注册需随目标清理的附加文件(如日志文件):

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

清理时将同步删除:

```bash
Removed foo.out
Removed foo.log
```

第14章 分层构建

大型软件项目的源代码通常不会集中存放在单一目录中,而是采用分层目录结构组织。使用SCons管理此类项目时,需要通过SConscript函数创建分层构建脚本体系。

14.1 SConscript脚本文件

项目顶层的构建脚本名为SConstruct。顶层脚本可通过SConscript函数引入下级构建脚本,这些下级脚本同样可以继续引入更底层的脚本。按照惯例,这些下级构建脚本通常命名为SConscript。例如:

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

此例中SConstruct显式列出了所有下级脚本。也可采用中间层SConscript文件组织,例如drivers目录下的中间脚本:

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

具体采用何种组织方式,可根据项目需求灵活选择。

14.2 路径相对于SConscript所在目录

下级SConscript文件中所有路径均相对于该脚本所在目录进行解析。这种机制使得构建规则可以与源代码保持在相同目录层级,便于维护。例如:

顶层SConstruct:
```python
SConscript(['prog1/SConscript', 'prog2/SConscript'])
```

prog1/SConscript内容:
```python
env = Environment()
env.Program('prog1', ['main.c', 'foo1.c', 'foo2.c'])
```

构建时SCons始终在顶层目录执行:
```bash
cc -o prog1/foo1.o -c prog1/foo1.c
cc -o prog1/prog1 prog1/main.o prog1/foo1.o prog1/foo2.o
```

14.3 使用顶层相对路径

在子目录脚本中引用其他目录文件时,可通过`#`前缀指定相对于顶层目录的路径:

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

14.4 共享构建环境

为避免在每个子脚本重复初始化相同的构建环境,可通过Export/Import机制共享变量:

父脚本:
```python
env = Environment(CCFLAGS='-O2')
Export('env')
```

子脚本:
```python
Import('env')
env.Program('prog', ['prog.c'])
```

14.5 从子脚本返回值

通过Return函数可实现子脚本向父脚本传递数据,例如收集多个子目录的编译结果:

父脚本:
```python
objs = []
for subdir in ['foo', 'bar']:
    objs += SConscript(f'{subdir}/SConscript')
env.Library('libprog', objs)
```

子脚本:
```python
Import('env')
obj = env.Object('foo.c')
Return('obj')
```

第15章 分离源码与构建目录

通常需要将构建产物与源代码分离存放。SCons通过variant_dir机制支持此需求。

15.1 使用SConscript的variant_dir参数

最直接的方式是在调用SConscript时指定variant_dir:

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

此时构建产物将生成在build目录,同时SCons会自动将源文件复制到构建目录:

```bash
cc -o build/hello.o -c build/hello.c
```

15.2 禁用源文件复制

大多数情况下可通过duplicate=0禁用源文件复制:

```python
SConscript('src/SConscript', variant_dir='build', duplicate=0)
```

此时构建行为变为:

```bash
cc -c src/hello.c -o build/hello.o
```

15.3 VariantDir函数

直接使用VariantDir函数也可实现相同效果:

```python
VariantDir('build', 'src', duplicate=0)
env.Program('build/hello.c')
```

15.4 结合Glob函数

文件名通配在variant_dir环境下同样适用:

```python
# src/SConscript
env.Program('hello', Glob('*.c'))
```

第16章 多平台变体构建

利用variant_dir可轻松实现跨平台构建,例如同时支持Windows和Linux:

```python
platform = ARGUMENTS.get('OS', Platform())
env = Environment(PLATFORM=platform)
SConscript('src/SConscript', variant_dir=f'build/{platform}')
```

执行示例:
```bash
scons -Q OS=linux   # 构建Linux版本
scons -Q OS=windows # 构建Windows版本
```

第17章 使用gettext实现国际化与本地化

gettext工具集为基于SCons的项目提供了国际化支持,其构建器可自动化翻译文件的生成与更新,管理方式与autotools类似。

17.1 环境准备
建议操作系统配置支持多语言环境(如en_US、de_DE和pl_PL),并安装GNU gettext工具包。推荐使用poedit编辑器处理翻译文件。

17.2 基础项目示例
以"Hello world"程序为例,原始代码:
```c
#include <stdio.h>
int main() {
    printf("Hello world\n");
}
```

国际化改造后的代码需添加gettext支持:
```c
#include <libintl.h>
int main() {
    setlocale(LC_ALL, "");
    printf(gettext("Hello world\n"));
}
```

17.3 SCons构建配置
完整的SConstruct配置示例:
```python
env = Environment(tools=['default', 'gettext'])
env['XGETTEXTFLAGS'] = [
    '--package-name=hello',
    '--package-version=1.0'
]
po = env.Translate(["en","de","pl"], ["hello.c"], POAUTOINIT=1)
mo = env.MOFiles(po)
InstallAs("locale/en/LC_MESSAGES/hello.mo", "en.mo")
```

工作流程:
1. 执行`scons po-update`生成翻译模板(.pot)和语言文件(.po)
2. 使用poedit编辑各语言po文件(如将德语翻译为"Hallo Welt!\n")
3. 执行`scons`编译mo文件并安装到locale目录

运行时通过环境变量切换语言:
```bash
LANG=de_DE.UTF-8 ./hello  # 输出德语版本
```

17.4 翻译文件维护
当源代码变更时:
- 新增国际化字符串会自动合并到po文件
- 未修改的翻译文件不会触发重建
- 仅实际变化的翻译会重新编译

第18章 自定义构建器

18.1 基础命令构建器
创建执行外部命令的构建器:
```python
bld = Builder(action='foobuild < $SOURCE > $TARGET')
env = Environment(BUILDERS={'Foo': bld})
env.Foo('output', 'input')
```

18.2 构建器注册方式
可通过多种方式注册构建器:
```python
# 方式1:追加到现有构建器
env.Append(BUILDERS={'Foo': bld})

# 方式2:直接字典操作
env['BUILDERS']['Foo'] = bld
```

18.3 自动化文件后缀
通过suffix参数自动处理文件扩展名:
```python
bld = Builder(
    action='foobuild < $SOURCE > $TARGET',
    suffix='.foo',
    src_suffix='.input'
)
```

18.4 Python函数构建器
使用Python函数代替外部命令:
```python
def build_fn(target, source, env):
    with open(str(target[0]), 'w') as t:
        t.write(open(str(source[0])).read())
    return 0

bld = Builder(action=build_fn)
```

18.5 动态命令生成器
通过generator动态生成构建命令:
```python
def cmd_generator(source, target, env, for_signature):
    return f'foobuild {source[0]} > {target[0]}'
bld = Builder(generator=cmd_generator)
```

18.6 目标/源文件修改器
使用emitter动态调整文件列表:
```python
def modify_files(target, source, env):
    return target + ['new_target'], source + ['new_source']
bld = Builder(emitter=modify_files)
```

18.7. 自定义构建器和工具的放置位置

site_scons 目录为您提供了一个放置 Python 模块和包的地方,您可以将这些模块和包导入到您的 SConscript 文件(site_scons)中;还可以放置能集成到 SCons 中的附加工具(site_scons/site_tools);此外,还有一个 site_scons/site_init.py 文件,该文件会在任何 SConstruct 或 SConscript 文件之前被读取,这使您能够改变 SCons 的默认行为。

每个系统类型(Windows、Mac、Linux 等)都会在一组标准目录中搜索 site_scons;具体细节请查看手册页。顶级 SConstruct 的 site_scons 目录总是最后被搜索,并且它的目录会被放置在工具路径的首位,因此它会覆盖其他所有目录中的工具。

如果您从某个地方(例如 SCons 维基或第三方)获取了一个工具,并且想在您的项目中使用它,那么 site_scons 目录是放置该工具的最简单的地方。工具有两种类型:一种是对环境进行操作的 Python 函数;另一种是包含 exists () 和 generate () 两个函数的 Python 模块或包。

单函数工具可以直接包含在您的 site_scons/site_init.py 文件中,在该文件中它会被解析并可供使用。例如,您可以有一个像这样的 site_scons/site_init.py 文件:

def TOOL_ADD_HEADER (env):
""" 一个从向源文件添加头信息的工具HEADER" > $TARGET',
'cat $SOURCE >> $TARGET'])
env.Append (BUILDERS = {'AddHeader' : add_header})
env ['HEADER'] = '' # 设置默认值

以及一个像这样的 SConstruct 文件:

使用来自 site_scons/site_init.py 的 TOOL_ADD_HEADER

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 文件中。(在上述所有情况中,toolname 会被工具的实际名称所替换。)由于 site_scons/site_tools 会自动添加到工具搜索路径的开头,所以在那里找到的任何工具对所有环境都可用。此外,在那里找到的工具会覆盖同名的内置工具,所以如果您需要改变内置工具的行为,site_scons 为您提供了所需的钩子。

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

from SCons.Script import * # 用于 Execute 和 Mkdir
def build_id ():
"""返回一个构建 ID(存根版本)"""
return "100"
def MakeWorkDir (workdir):
"""立即创建指定的目录"""
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_init.py 中,但放在那里并不比放在 site_scons/my_utils.py 中更好,因为您仍然需要将该模块导入到您的 SConscript 中。还要注意,为了在除 SConstruct 或 SConscript 之外的任何文件中引用 SCons 命名空间中的对象,如 Environment、Mkdir 或 Execute,您始终需要执行以下操作:

from SCons.Script import *

在 site_scons 中的模块(如 site_scons/site_init.py)中也是如此。

您可以使用任何用户级或机器级的站点目录,如~/.scons/site_scons 来代替./site_scons,或者使用 --site-dir 选项来指向您自己的目录。site_init.py 和 site_tools 将位于该目录下。如果您根本不想使用 site_scons 目录,即使它存在,也可以使用 --no-site-dir 选项。
第 19 章。不编写构建器:命令构建器

创建一个构建器并将其附加到构建环境中,当您想要重用操作来构建相同类型的多个文件时,这提供了很大的灵活性。然而,如果您只需要执行一个特定的命令来构建单个文件(或一组文件),那么这种方式可能会很麻烦。对于这些情况,SCons 支持一个命令构建器,它安排执行一个特定的操作来构建特定的文件。这看起来很像其他构建器(如 Program、Object 等),但它接受一个额外的参数,即用于构建文件的要执行的命令:

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

执行时,SCons 会运行指定的命令,并按预期替换和TARGET:

% scons -Q
sed 's/x/y/' < foo.in > foo.out

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

请注意,您为命令构建器指定的操作可以是任何合法的 SCons 操作,例如一个 Python 函数:

env = Environment()
def build(target, source, env):

进行构建所需的任何操作

return None
env.Command('foo.out', 'foo.in', build)

执行情况如下:

% scons -Q
build(["foo.out"], ["foo.in"])

请注意,从 SCons 1.1 开始,和TARGET 在源文件和目标文件中都会被展开,所以您可以这样写:

env.Command('${SOURCE.basename}.out', 'foo.in', build)

这与前面的示例效果相同,但可以避免重复编写代码。
第 20 章。伪构建器:AddMethod 函数

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

def install_in_bin_dirs (env, source):
"""将源文件安装到两个 bin 目录中"""
i1 = env.Install ("BIN",source)i2=env.Install("LOCALBIN", source)
return [i1 [0], i2 [0]] # 返回一个列表,就像普通构建器一样
env = Environment (BIN='/usr/bin', LOCALBIN='#install/bin')
env.AddMethod (install_in_bin_dirs, "InstallInBinDirs")
env.InstallInBinDirs (Program ('hello.c')) # 将 hello 安装到两个 bin 目录中

这会产生以下结果:

% scons -Q /
cc -o hello.o -c hello.c
cc -o hello hello.o
将文件 “hello” 安装为 “/usr/bin/hello”
将文件 “hello” 安装为 “install/bin/hello”

如前所述,伪构建器在解析参数方面比普通构建器提供了更大的灵活性。下一个示例展示了一个伪构建器,它有一个命名参数用于修改文件名,还有一个单独的参数用于资源文件(而不是让构建器通过文件扩展名来确定)。这个示例还演示了使用全局 AddMethod 函数向全局 Environment 类添加一个方法,这样它将在所有随后创建的环境中使用。

def BuildTestProg (env, testfile, resourcefile, testdir="tests"):
""" 构建测试程序;
在源文件和目标文件前加上 “test_”,
并将目标文件放入 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')

在 Linux 上,这会产生以下结果:

% scons -Q
cc -o test_stuff.o -c test_stuff.c
cc -o tests/test_stuff test_stuff.o

在 Windows 上,结果如下:

C:>scons -Q
rc /nologo /fores.res res.rc
cl /Fotest_stuff.obj /c test_stuff.c /nologo
link /nologo /OUT:tests\test_stuff.exe test_stuff.obj res.res
embedManifestExeCheck(target, source, env)

使用 AddMethod 比直接向构建环境添加实例方法更好,因为它会作为一个合适的方法被调用,并且因为 AddMethod 提供了将该方法复制到构建环境实例的任何克隆中的功能。
第 21 章。编写扫描器

SCons 有内置的扫描器,这些扫描器知道如何在 C、Fortran 和 IDL 源文件中查找与从这些文件构建的目标所依赖的其他文件相关的信息 —— 例如,对于使用 C 预处理器的文件,扫描器会查找源文件中使用 #include 指令指定的.h 文件。您可以使用 SCons 创建内置扫描器时所使用的相同机制,为 SCons 无法 “开箱即用” 进行扫描的文件类型编写您自己的扫描器。
21.1. 一个简单的扫描器示例

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

include filename.foo

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

import re

include_re = re.compile(r'^include\s+(\S+)$', re.M)

def kfile_scan(node, env, path, arg):
contents = node.get_text_contents()
return env.File(include_re.findall(contents))

需要注意的是,您必须从扫描器函数中返回一个 File 节点的列表,仅返回文件名的简单字符串是不行的。就像我们在这里展示的示例一样,您可以使用当前环境的 File 函数,以便根据文件名序列(带有相对路径)动态创建节点。

扫描器函数必须接受指定的四个参数,并返回一个隐式依赖项的列表。据推测,这些依赖项是通过检查文件内容找到的,不过该函数可以执行任何操作来生成依赖项列表。
node
一个表示正在扫描的文件的 SCons 节点对象。可以使用 str () 函数将节点转换为字符串来获取文件的路径名,或者可以使用内部的 SCons get_text_contents () 对象方法来获取文件内容。
env
对此次扫描有效的构建环境。扫描器函数可以选择使用此环境中的构建变量来影响其行为。
path
一个目录列表,构成了此扫描器用于查找包含文件的搜索路径。这就是 SCons 处理和LIBPATH 变量的方式。
arg
一个可选参数,您可以选择让各种扫描器实例将其传递给此扫描器函数。

可以使用 Scanner 函数创建一个 Scanner 对象,该函数通常接受一个 skeys 参数,用于将文件后缀类型与此扫描器相关联。然后,必须使用 Append 方法将 Scanner 对象与构建环境的 $SCANNERS 构建变量相关联:

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

当我们把所有内容放在一起时,看起来像这样:

import re

include_re = re.compile(r'^include\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,
scons = ['.k'])

env = Environment(ENV = {'PATH' : '/usr/local/bin'})
env.Append(SCANNERS = kscan)

env.Command('foo', 'foo.k', 'kprocess < $SOURCES > $TARGET')

21.2. 向扫描器添加搜索路径:FindPathDirs

许多扫描器需要使用路径变量来搜索包含文件或依赖项;这就是和LIBPATH 的工作方式。搜索路径会作为 path 参数传递给您的扫描器。路径变量可以是节点列表、用分号分隔的字符串,甚至可以包含需要展开的 SCons 变量。幸运的是,SCons 提供了 FindPathDirs 函数,该函数本身返回一个函数,用于在调用扫描器时将给定的路径(作为 SCons 构建变量名给出)展开为路径列表。例如,推迟到那个时候进行计算可以使路径包含引用,而对于每个扫描的文件,TARGET 引用可能会有所不同。

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

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

FindPathDirs 返回一个可调用对象,调用该对象时,它将本质上展开 env ['KPATH'] 中的元素,并告诉扫描器在这些目录中进行搜索。它还会正确地将相关的存储库和变体目录添加到搜索列表中。顺便说一下,返回的方法会以一种高效的方式存储路径,因此即使可能需要进行变量替换,查找速度也会很快。这一点很重要,因为在典型的构建过程中会扫描许多文件。
21.3. 将扫描器与构建器一起使用

使用扫描器的一种方法是与构建器结合使用。构建器有两个可选参数,我们可以使用 source_scanner 和 target_scanner。

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

从 “source” 构建 “target” 的代码

return None

bld = Builder(action = build_function,
suffix = '.foo',
source_scanner = kscan
src_suffix = '.input')
env = Environment(BUILDERS = {'Foo' : bld})
env.Foo('file')

一个发射器函数可以在构建器被触发时修改传递给动作函数的源文件或目标文件列表。

扫描器函数不会影响构建动作期间构建器看到的源文件或目标文件列表。然而,扫描器函数会影响是否应该重新构建构建器(例如,如果扫描器所处理的文件中的任何一个发生了变化)。

第 22 章 从代码仓库构建

通常,一个软件项目会有一个或多个中央仓库,这些仓库是包含源代码或派生文件,或者两者都有的目录树。通过让 SCons 使用来自一个或多个代码仓库的文件在本地构建树中构建文件,您可以避免对文件进行额外的不必要的重新构建。

22.1 Repository 方法

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

python

运行

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

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

22.2 在仓库中查找源文件

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

因此,对于上述 SConstruct 文件,如果 hello.c 文件存在于本地构建目录中,SCons 将正常重新构建 hello 程序:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o

然而,如果本地没有 hello.c 文件,但在 /usr/repository1 中有一个,SCons 将使用在仓库中找到的源文件重新编译 hello 程序:

plaintext

% scons -Q
cc -o hello.o -c /usr/repository1/hello.c
cc -o hello hello.o

类似地,如果本地没有 hello.c 文件,/usr/repository1 中也没有,但在 /usr/repository2 中有一个:

plaintext

% scons -Q
cc -o hello.o -c /usr/repository2/hello.c
cc -o hello hello.o
22.3 在仓库中查找 #include 文件

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

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

plaintext

% scons -Q
cc -o hello.o -c hello.c
hello.c:1: hello.h: No such file or directory

为了通知 C 编译器关于仓库的信息,SCons 会为列表中的每个目录向编译命令添加适当的标志。所以如果我们像这样将当前目录添加到构建环境的CPPPATH 中:

python

运行

env = Environment(CPPPATH = ['.'])
env.Program('hello.c')
Repository('/usr/repository1')

然后重新执行 SCons 会得到:

plaintext

% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 hello.c
cc -o hello hello.o

对于 C 预处理器来说,-I 选项的顺序复制了 SCons 用于其自身依赖分析的仓库目录搜索路径。如果有多个仓库和多个目录,会将仓库目录添加到每个CPPPATH 目录的开头,迅速增加 - I 标志的数量。例如,如果 $CPPPATH 包含三个目录(并且仓库路径名较短!):

python

运行

env = Environment(CPPPATH = ['dir1', 'dir2', 'dir3'])
env.Program('hello.c')
Repository('/r1', '/r2')

然后在命令行上最终会有九个 - I 选项,即三个(对应每个 $CPPPATH 目录)乘以三个(本地目录加上两个仓库):

plaintext

% scons -Q
cc -o hello.o -c -Idir1 -I/r1/dir1 -I/r2/dir1 -Idir2 -I/r1/dir2 -I/r2/dir2 -Idir3 -I/r1/dir3 -I/r2/dir3 hello.c
cc -o hello hello.o
22.3.1 仓库中 #include 文件的限制

SCons 依赖于 C 编译器的 - I 选项来控制预处理器搜索仓库目录以查找 #include 文件的顺序。然而,这会导致 C 预处理器处理包含双引号文件名的 #include 行的方式出现问题。

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

c

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

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

plaintext

% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 /usr/repository1/hello.c
cc -o hello hello.o

C 预处理器的这种行为 —— 总是首先在与源文件相同的目录中搜索双引号的 #include 文件,然后才搜索 - I 指定的目录 —— 一般来说是无法改变的。换句话说,如果您想以这种方式使用代码仓库,就必须接受这个限制。有三种方法可以解决 C 预处理器的这种行为:

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

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

plaintext

% scons -Q -Y /usr/repository1 -Y /usr/repository2

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

22.5 在仓库中查找派生文件

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

通常,这可以由构建集成人员完成,他们会在仓库中运行 SCons 以创建所有派生文件和.sconsign 文件,或者他们会在单独的构建目录中运行 SCons 并将生成的树复制到所需的仓库中:

plaintext

% cd /usr/repository1
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o hello.o -c hello.c
cc -o hello hello.o file1.o file2.o

(请注意,即使 SConstruct 文件将 /usr/repository1 列为仓库,这样做也是安全的,因为 SCons 会在该调用中从其仓库列表中删除当前构建目录。)

现在,在仓库已填充的情况下,我们只需要创建我们当前感兴趣的一个本地源文件,并使用 - Y 选项告诉 SCons 从仓库中获取它需要的任何其他文件:

plaintext

% cd $HOME/build
% edit hello.c
% scons -Q -Y /usr/repository1
cc -c -o hello.o hello.c
cc -o hello hello.o /usr/repository1/file1.o /usr/repository1/file2.o

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

22.6 保证文件的本地副本

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

plaintext

% mkdir $HOME/build2
% cd $HOME/build2
% scons -Q -Y /usr/all/repository hello
scons: `hello' is up-to-date.

为什么 SCons 会说 hello 程序是最新的,而本地构建目录中并没有 hello 程序呢?因为仓库(而不是本地目录)中包含最新的 hello 程序,并且 SCons 正确地确定不需要对该文件的最新副本进行任何重建操作。

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

python

运行

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

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

plaintext

% scons -Y /usr/all/repository hello
Local copy of hello from /usr/all/repository/hello
scons: `hello' is up-to-date.

(请注意,因为制作本地副本的操作不被视为对 hello 文件的 “构建”,所以 SCons 仍然会报告它是最新的。)

第 23 章 多平台配置(类似 Autoconf 的功能)

SCons 对多平台构建配置的集成支持类似于 GNU Autoconf 提供的支持,例如确定本地系统上哪些库或头文件可用。本节描述如何使用 SCons 的这个功能。
注意:本章仍在开发中,所以并不是所有内容都能得到很好的解释。有关更多信息,请查看 SCons 的手册页。

23.1 配置上下文

SCons 中多平台构建配置的基本框架是通过调用 Configure 函数将配置上下文附加到构建环境,对库、函数、头文件等执行一些检查,然后调用配置上下文的 Finish 方法来完成配置:

python

运行

env = Environment()
conf = Configure(env)
# 对库、头文件等的检查放在这里!
env = conf.Finish()

SCons 提供了一些基本检查,以及一种添加您自己的自定义检查的机制。

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

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

23.2 检查头文件是否存在

测试头文件是否存在需要知道头文件的语言。配置上下文有一个 CheckCHeader 方法,用于检查 C 头文件是否存在:

python

运行

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('-DHAS_FOO_H')
env = conf.Finish()

请注意,您可以选择在给定的头文件不存在时终止构建,或者根据头文件的存在情况修改构建环境。

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

python

运行

env = Environment()
conf = Configure(env)
if not conf.CheckCXXHeader('vector.h'):print 'vector.h must be installed!'Exit(1)
env = conf.Finish()
23.3 检查函数是否可用

使用 CheckFunc 方法检查特定函数是否可用:

python

运行

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

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

python

运行

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 方法同时检查库和头文件:

python

运行

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

这本质上是分别调用 CheckHeader 和 CheckLib 函数的简写形式。

23.5 检查 typedef 是否可用

使用 CheckType 方法检查 typedef 是否可用:

python

运行

env = Environment()
conf = Configure(env)
if not conf.CheckType('off_t'):print 'Did not find off_t typedef, assuming int'conf.env.Append(CCFLAGS = '-Doff_t=int')
env = conf.Finish()

您还可以添加一个字符串,该字符串将放置在用于检查 typedef 的测试文件的开头。这提供了一种指定为找到 typedef 必须包含的文件的方法:

python

运行

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(CCFLAGS = '-Doff_t=int')
env = conf.Finish()
23.6 检查数据类型的大小

使用 CheckTypeSize 方法检查数据类型的大小:

python

运行

env = Environment()
conf = Configure(env)
int_size = conf.CheckTypeSize('unsigned int')
print 'sizeof unsigned int is', int_size
env = conf.Finish()

plaintext

% scons -Q
sizeof unsigned int is 4
scons: `.' is up to date.
23.7 检查程序是否存在

使用 CheckProg 方法检查程序是否存在:

python

运行

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()
23.8 添加您自己的自定义检查

自定义检查是一个 Python 函数,用于检查运行系统上是否存在特定条件,通常使用 SCons 提供的方法来处理检查编译是否成功、链接是否成功、程序是否可运行等细节。一个简单的检查特定库是否存在的自定义检查可能如下所示:

python

运行

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。TryLink 方法实际测试指定的程序文本是否能成功链接。

(请注意,自定义检查可以根据您选择传递的任何参数修改其检查,或者通过使用或修改上下文.env 属性中的配置上下文环境来进行修改。)

然后,通过将一个字典传递给 Configure 调用,将这个自定义检查函数附加到配置上下文,该字典将检查的名称映射到基础函数:

python

运行

env = Environment()
conf = Configure(env, custom_tests = {'CheckMyLibrary' : CheckMyLibrary})

通常,您会希望检查的名称和函数名称相同,就像我们在这里所做的那样,以避免潜在的混淆。

然后,我们可以将这些部分组合起来并实际调用 CheckMyLibrary 检查,如下所示:

python

运行

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 resultenv = 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.
    

If MyLibrary is not installed on the system, the output will look like:

% scons
scons: Reading SConscript file ...
Checking for MyLibrary... no
MyLibrary is not installed!

If MyLibrary is installed, the output will look like:

% scons
scons: Reading SConscript file ...
Checking for MyLibrary... yes
scons: done reading SConscript
scons: Building targets ......

23.9. 清理目标时不进行配置

使用前面章节描述的多平台配置时,即使调用scons -c 来清理目标,也会运行配置命令:

plaintext

% scons -Q -c
Checking for MyLibrary... yes
Removed foo.o
Removed foo

虽然在删除目标时运行平台检查不会造成什么危害,但通常这是不必要的。您可以使用GetOption 方法来检查命令行上是否调用了-c(清理)选项,从而避免这种情况:

python

运行

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

plaintext

% scons -Q -c
Removed foo.o
Removed foo

第 24 章。缓存已构建的文件

在多开发者的软件项目中,有时允许开发者共享他们构建的派生文件可以大大加快每个开发者的构建速度。SCons 使得这变得简单且可靠。

24.1 指定共享缓存目录

要启用派生文件的共享,可以在任何 SConscript 文件中使用CacheDir 函数:

python

运行

CacheDir('/usr/local/build_cache')

请注意,您指定的目录必须已经存在,并且所有共享派生文件的开发者都能够对其进行读写操作。它还应该位于所有构建都能够访问的某个中央位置。在开发者使用单独系统(如个人工作站)进行构建的环境中,这个目录通常会位于共享或通过 NFS 挂载的文件系统上。

以下是具体的情况:当构建指定了CacheDir 时,每次构建一个文件,该文件及其 MD5 构建签名都会存储在共享缓存目录中。[4] 在后续的构建中,在调用操作来构建一个文件之前,SCons 会检查共享缓存目录,查看是否存在具有完全相同构建签名的文件。如果存在,派生文件将不会在本地构建,而是会从共享缓存目录复制到本地构建目录,如下所示:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
Retrieved `hello.o' from cache
Retrieved `hello' from cache

请注意,即使您将 SCons 配置为使用时间戳来判断文件是否为最新版本,CacheDir 功能仍然会为共享缓存文件名计算 MD5 构建签名。(有关Decider 函数的信息,请参阅第 6 章 “依赖关系”。)因此,使用CacheDir可能会减少或消除使用时间戳进行最新判断所带来的潜在性能提升。

24.2 保持构建输出的一致性

使用共享缓存的一个潜在缺点是,SCons 打印的输出在每次调用时可能不一致,因为任何给定的文件可能在一次构建时被重建,而在下一次从共享缓存中检索。这会使分析构建输出变得更加困难,特别是对于期望每次输出一致的自动化脚本。

然而,如果您使用--cache-show 选项,即使从共享缓存中检索文件,SCons 也会打印用于构建该文件的命令行。这使得每次运行构建时的构建输出都是一致的:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-show
cc -o hello.o -c hello.c
cc -o hello hello.o

当然,这样做的权衡是您不再知道 SCons 是从缓存中检索派生文件还是在本地重建它。

24.3 对特定文件不使用共享缓存

您可能希望在配置中对某些特定文件禁用缓存。例如,如果您只想将可执行文件放入中央缓存,而不包括中间目标文件,您可以使用NoCache 函数来指定目标文件不应被缓存:

python

运行

env = Environment()
obj = env.Object('hello.c')
env.Program('hello.c')
CacheDir('cache')
NoCache('hello.o')

然后,当您在清理已构建的目标后运行scons 时,它将在本地重新编译目标文件(因为它不存在于共享缓存目录中),但仍然会意识到共享缓存目录中包含一个最新的可执行程序,可以检索该程序而无需重新链接:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
cc -o hello.o -c hello.c
Retrieved `hello' from cache
24.4 禁用共享缓存

从共享缓存中检索已构建的文件通常比重新构建文件节省大量时间,但节省的时间(甚至是否能节省时间)在很大程度上取决于您的系统或网络配置。例如,通过繁忙的网络从繁忙的服务器检索缓存文件可能比在本地重新构建文件更慢。

在这些情况下,您可以指定--cache-disable 命令行选项,告诉 SCons 不从共享缓存目录中检索已构建的文件:

plaintext

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
Retrieved `hello.o' from cache
Retrieved `hello' from cache
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
24.5 用已构建的文件填充共享缓存

有时,您可能在本地构建树中已经构建了一个或多个派生文件,并希望将其提供给其他进行构建的人。例如,您可能会发现,在禁用缓存的情况下执行集成构建(如前一节所述),并在集成构建成功完成后将已构建的文件填充到共享缓存目录中会更有效。这样,缓存中只会填充属于完整、成功构建的派生文件,而不会填充在调试集成问题时可能会被覆盖的文件。

在这种情况下,您可以使用--cache-force 选项告诉 SCons 将所有派生文件放入缓存,即使这些文件已经在您的本地树中由之前的调用构建过:

plaintext

% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q --cache-force
scons: `.' is up to date.
% scons -Q
scons: `.' is up to date.

请注意,上述示例运行表明--cache-disable 选项会避免将已构建的hello.o 和hello 文件放入缓存,但在使用--cache-force 选项后,这些文件会被放入缓存,以便下一次调用时检索。

24.6 最小化缓存竞争:--random 选项

如果您允许多个构建同时更新共享缓存目录,同时进行的两个构建有时会以相同的顺序尝试构建相同的文件,从而产生 “竞争”。例如,如果您要将多个文件链接到一个可执行程序中:

python

运行

Program('prog',['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c'])

SCons 通常会按照正常的排序顺序构建程序所依赖的输入目标文件:

plaintext

% scons -Q
cc -o f3.o -c f3.c
cc -o f4.o -c f4.c
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f5.o -c f5.c
cc -o prog f1.o f2.o f3.o f4.o f5.o

但是,如果同时进行两个这样的构建,它们可能会几乎同时检查缓存,并都决定必须重建f1.o 并将其推入共享缓存目录,然后又都决定必须重建f2.o(并将其推入共享缓存目录),接着又都决定必须重建f3.o…… 这不会导致任何实际的构建问题 —— 两个构建都会成功,生成正确的输出文件,并填充缓存 —— 但这确实是一种浪费精力的情况。

为了缓解这种对缓存的竞争,您可以使用--random 命令行选项告诉 SCons 以随机顺序构建依赖项:

plaintext

  % scons -Q --randomcc -o f3.o -c f3.ccc -o f1.o -c f1.ccc -o f5.o -c f5.ccc -o f2.o -c f2.ccc -o f4.o -c f4.ccc -o prog f1.o f2.o f3.o f4.o f5.o

使用--random 选项的多个构建通常会以不同的随机顺序构建它们的依赖项,这会最小化对共享缓存目录中同名文件的大量竞争的可能性。多个同时进行的构建偶尔可能仍然会竞争构建相同的目标文件,但长时间的低效竞争应该很少发生。

当然,请注意--random 选项会导致 SCons 打印的输出在每次调用时不一致,这在尝试比较不同构建运行的输出时可能会成为一个问题。

如果您想确保依赖项以随机顺序构建,而不必在每个命令行上指定--random,您可以在任何 SConscript 文件中使用SetOption 函数来设置随机选项:

python

运行

SetOption('random', 1)
Program('prog',['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c'])

[4] 实际上,MD5 签名被用作存储内容的共享缓存目录中文件的名称。

第 25 章。别名目标

我们已经了解了如何使用Alias 函数创建一个名为install 的目标:

python

运行

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

然后,您可以在命令行上使用这个别名,更自然地告诉 SCons 您想要安装文件:

plaintext

% scons -Q install
cc -o hello.o -c hello.c
cc -o hello hello.o
Install file: "hello" as "/usr/bin/hello"

不过,与其他构建器方法一样,Alias 方法会返回一个表示正在构建的别名的对象。然后,您可以将这个对象用作另一个构建器的输入。如果您将这样的对象用作对Alias 构建器的另一次调用的输入,这会特别有用,这样您就可以创建一个嵌套别名的层次结构:

python

运行

env = Environment()
p = env.Program('foo.c')
l = env.Library('bar.c')
env.Install('/usr/bin', p)
env.Install('/usr/lib', l)
ib = env.Alias('install-bin', '/usr/bin')
il = env.Alias('install-lib', '/usr/lib')
env.Alias('install', [ib, il])

这个示例定义了单独的installinstall-bin 和install-lib 别名,使您能够更精细地控制要安装的内容:

plaintext

% scons -Q install-bin
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
% scons -Q install-lib
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"
% scons -Q -c /
Removed foo.o
Removed foo
Removed /usr/bin/foo
Removed bar.o
Removed libbar.a
Removed /usr/lib/libbar.a
% scons -Q install
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/80936.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java的多线程笔记

创建一个线程的方法有多种&#xff0c;比如可以继承Thread类或者实现Runnable接口&#xff0c;结论是实现Runnable接口比前者更加优越。 二者代码对比 Java 不支持多继承&#xff0c;如果你继承了 Thread 类&#xff0c;就不能再继承其他类&#xff0c;实现 Runnable 接口后&am…

PDF Base64格式字符串转换为PDF文件临时文件

需求描述&#xff1a; 在对接电子病历系统与河北CA&#xff0c;进行免密文件签章的时候&#xff0c;两者系统入参不同&#xff0c;前者是pdf文件&#xff0c;base64格式&#xff1b;后者要求File类型的PDF文件。 在业务中间层开发时&#xff0c;则需要接收EMR侧提供的base64格式…

代码随想录训练营第二十三天| 572.另一颗树的子树 104.二叉树的最大深度 559.N叉树的最大深度 111.二叉树的最小深度

572.另一颗树的子树&#xff1a; 状态&#xff1a;已做出 思路&#xff1a; 这道题目当时第一时间不是想到利用100.相同的树思路来解决&#xff0c;而是先想到了使用kmp&#xff0c;不过这个题目官方题解确实是有kmp解法的&#xff0c;我使用的暴力解法&#xff0c;kmp的大致思…

【RabbitMq C++】消息队列组件

RabbitMq 消息队列组件 1. RabbitMq介绍2. 安装RabbitMQ3. 安装 RabbitMQ 的 C客户端库4. AMQP-CPP 库的简单使用4.1 使用4.1.1 TCP 模式4.1.2 扩展模式 4.2 常用类与接口介绍4.2.1 Channel4.3.2 ev 5. RabbitMQ样例编写5.1 发布消息5.2 订阅消息 1. RabbitMq介绍 RabbitMq - …

鸿蒙NEXT开发动画案例8

1.创建空白项目 2.Page文件夹下面新建Spin.ets文件&#xff0c;代码如下&#xff1a; /*** SpinKit动画组件 (重构版)* author: CSDN-鸿蒙布道师* since: 2025/05/14*/interface AnimationGroup {indexes: number[];delay: number; }ComponentV2 export struct SpinEight {Re…

MySQL全局优化

目录 1 硬件层面优化 1.1 CPU优化 1.2 内存优化 1.3 存储优化 1.4 网络优化 2 系统配置优化 2.1 操作系统配置 2.2 MySQL服务配置 3 库表结构优化 4 SQL及索引优化 mysql可以从四个层面考虑优化&#xff0c;分别是 硬件系统配置库表结构SQL及索引 从成本和优化效果来看&#xf…

vue和springboot交互数据,使用axios【跨域问题】

vue和springboot交互数据&#xff0c;使用axios【跨域问题】 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是node.js和vue的使用。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性。【帮帮志系列文章】&…

FFMPEG 与 mp4

1. FFmpeg 中的 start_time 与 time_base start_time 流的起始时间戳&#xff08;单位&#xff1a;time_base&#xff09;&#xff0c;表示第一帧的呈现时间&#xff08;Presentation Time&#xff09;。通常用于同步多个流&#xff08;如音频和视频&#xff09;。 time_base …

AI世界的崩塌:当人类思考枯竭引发数据生态链断裂

AI世界的崩塌&#xff1a;当人类思考枯竭引发数据生态链断裂 ——论过度依赖AI创作对技术进化的反噬 一、数据生态的恶性循环&#xff1a;AI的“自噬危机” 当前AI模型的训练依赖于人类创造的原始数据——书籍、论文、艺术作品、社交媒体动态等。据统计&#xff0c;2025年全球…

C++【STL】(2)string

C【STL】string用法扩展 1. assign&#xff1a;为字符串赋新值 用于替换字符串内容&#xff0c;支持多种参数形式。 常用形式&#xff1a; // 用另一个字符串赋值 str.assign("Hello World");// 用另一个字符串的子串&#xff08;从第6个字符开始&#xff0c;取5…

树莓派4基于Debian GNU/Linux 12 (Bookworm)开启VNC,使用MobaXterm连接VNC出现黑屏/灰屏问题

1. 开启树莓派的VNC服务 启用VNC服务&#xff1a;通过raspi-config开启 # 1. 通过 raspi-config 工具开启 sudo raspi-config选择 Interface Options → VNC → Yes退出时会自动启动服务 检查服务状态&#xff1a; sudo systemctl status vncserver-x11-serviced正常输出应显示…

MongoDB使用x.509证书认证

文章目录 自定义证书生成CA证书生成服务器之间的证书生成集群证书生成用户证书 MongoDB配置java使用x.509证书连接MongoDBMongoShell使用证书连接 8.0版本的mongodb开启复制集&#xff0c;配置证书认证 自定义证书 生成CA证书 生成ca私钥&#xff1a; openssl genrsa -out ca…

Python爬虫实战:研究js混淆加密

一、引言 在当今数字化时代,数据已成为推动各行业发展的核心驱动力。网络爬虫作为一种高效的数据采集工具,能够从互联网上自动获取大量有价值的信息。然而,随着互联网技术的不断发展,许多网站为了保护自身数据安全和知识产权,采用了 JavaScript 混淆加密技术来防止数据被…

Java项目层级介绍 java 层级 层次

java 层级 层次 实体层 控制器层 数据连接层 Service : 业务处理类 Repository &#xff1a;数据库访问类 Java项目层级介绍 https://blog.csdn.net/m0_67574906/article/details/145811846 在Java项目中&#xff0c;层级结构&#xff08;Layered Architecture&#xf…

网络安全顶会——SP 2025 论文清单与摘要

1、"Check-Before-you-Solve": Verifiable Time-lock Puzzles 时间锁谜题是一种密码学原语&#xff0c;它向生成者保证该谜题无法在少于T个顺序计算步骤内被破解。近年来&#xff0c;该技术已在公平合约签署和密封投标拍卖等场景中得到广泛应用。然而&#xff0c;求解…

《100天精通Python——基础篇 2025 第18天:正则表达式入门实战,解锁字符串处理的魔法力量》

目录 一、认识正则表达式二、正则表达式基本语法2.1 行界定符2.2 单词定界符2.3 字符类2.4 选择符2.5 范围符2.6 排除符2.7 限定符2.8 任意字符2.9 转义字符2.10 反斜杠2.11 小括号2.11.1 定义独立单元2.11.2 分组 2.12 反向引用2.13 特殊构造2.14 匹配模式 三、re模块3.1 comp…

思迈特软件携手天阳科技,打造ChatBI金融智能分析新标杆

5月10日&#xff0c;广州思迈特软件有限公司&#xff08;以下简称“思迈特软件”&#xff09;与天阳宏业科技股份有限公司&#xff08;以下简称“天阳科技”&#xff09;在北京正式签署战略合作协议。思迈特软件董事长吴华夫、CEO姚诗成&#xff0c;天阳科技董事长兼总裁欧阳建…

OPENSSL-1.1.1的使用及注意事项

下载链接&#xff1a; OpenSSL1.1.1一个广泛使用的开源加密库资源-CSDN文库 OpenSSL 1.1.1 是一个广泛使用的开源加密库&#xff0c;以下是其使用方法及注意事项&#xff1a; 使用方法 安装&#xff1a; Linux系统&#xff1a; 从源码编译安装&#xff1a;访问 OpenSSL 官网…

数据库优化

一、慢 SQL 排查全流程 1. 开启慢查询日志&#xff1a;精准定位问题 SQL 慢查询日志是定位性能问题的首要工具&#xff0c;通过记录执行超时或未使用索引的 SQL&#xff0c;为优化提供依据。 配置步骤&#xff1a; ① 临时启用&#xff08;生效至服务重启&#xff09; sql …

GO语言-导入自定义包

文章目录 1. 项目目录结构2. 创建自定义包3. 初始化模块4. 导入自定义包5. 相对路径导入 在Go语言中导入自定义包需要遵循一定的目录结构和导入规则。以下是详细指南&#xff08;包含两种方式&#xff09;&#xff1a; 1. 项目目录结构 方法1&#xff1a;适用于Go 1.11 &#…