9 构建系统
在本章中,我们将讨论构建系统,以及如何在 Zig 中构建整个项目。Zig 的一个关键优势是它包含一个嵌入在语言本身中的构建系统。这非常棒,因为这样你就不必依赖与编译器分离的外部系统来构建代码了。
您可以在Zig 官方网站上题为“构建系统”的文章1中找到对 Zig 构建系统的详细描述。我们还有Felix 撰写的一系列精彩文章2。因此,本章将是您参考和依赖的额外资源。
构建代码是 Zig 最擅长的事情之一。在 C/C++ 甚至 Rust 中,有一件事特别困难,那就是将源代码交叉编译到多个目标(例如,多个计算机架构和操作系统),而 Zigzig
编译器被认为是目前最适合这项特定任务的软件之一。
9.1源代码是如何构建的?
我们已经在1.2.1 节中讨论过使用低级语言构建源代码所面临的挑战。正如我们在那一节中所描述的,程序员发明了构建系统 (Build Systems) 来克服使用低级语言构建源代码过程中的这些挑战。
低级语言使用编译器将源代码编译(或构建)成二进制指令。在 C 和 C++ 中,我们通常使用诸如gcc
、g++
或 之类的编译器clang
将 C 和 C++ 源代码编译成这些指令。每种语言都有自己的编译器,Zig 也不例外。
在 Zig 中,我们有zig
编译器将 Zig 源代码编译成计算机可以执行的二进制指令。在 Zig 中,编译(或构建)过程涉及以下组件:
- 包含您的源代码的 Zig 模块;
- 库文件(动态库或静态库);
- 编译器标志可根据您的需要定制构建过程。
这些是在 Zig 中构建源代码所需的连接内容。在 C 和 C++ 中,你会有一个额外的组件,即你正在使用的库的头文件。但是 Zig 中没有头文件,所以只有在将 Zig 源代码与 C 库链接时才需要关心它们。如果不是这种情况,你可以不用管它。
您的构建过程通常组织在构建脚本中。在 Zig 中,我们通常将此构建脚本写入项目根目录中的 Zig 模块中,名为build.zig
。您编写此构建脚本,然后,当您运行它时,您的项目将被构建为二进制文件,您可以使用这些文件并将其分发给用户。
此构建脚本通常围绕_目标对象_进行组织。目标就是要构建的东西,或者换句话说,就是你希望zig
编译器为你构建的东西。“目标”这个概念存在于大多数构建系统中,尤其是在 CMake 3中。
您可以在 Zig 中构建四种类型的目标对象,分别是:
- 可执行文件(例如
.exe
Windows 上的文件)。 - 共享库(例如
.so
Linux 中的文件或.dll
Windows 上的文件)。 - 静态库(例如
.a
Linux 中的文件或.lib
Windows 上的文件)。 - 仅执行单元测试的可执行文件(或“单元测试可执行文件”)。
我们将在第 9.3 节详细讨论这些目标对象。
9.2函数build()
Zig 中的构建脚本始终包含声明的公共(和顶级)build()
函数。它类似于我们在1.2.3 节main()
中讨论过的项目主 Zig 模块中的函数。但是,此函数不是创建代码的入口点,而是构建过程的入口点。build()
此build()
函数应接受指向对象的指针Build
作为输入,并使用这个“构建对象”执行构建项目所需的步骤。此函数的返回类型始终为void
,并且此Build
结构体直接来自 Zig 标准库(std.Build
)。因此,您只需将 Zig 标准库导入build.zig
模块即可访问此结构体。
仅作为一个非常简单的例子,这里您可以看到从 Zig 模块构建可执行文件所需的源代码hello.zig
。
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
});
b.installArtifact(exe);
}
您可以在此构建脚本中定义和使用其他函数和对象。您还可以像在项目中的任何其他模块中一样导入其他 Zig 模块。此构建脚本的唯一实际要求是定义一个公共的顶级build()
函数,该函数接受指向结构体的指针Build
作为输入。
9.3目标对象
正如我们在前几节中所述,构建脚本围绕目标对象构建。每个目标对象通常都是您希望从构建过程中获取的二进制文件(或输出)。您可以在构建脚本中列出多个目标对象,以便构建过程一次性为您生成多个二进制文件。
例如,也许您是一位跨平台应用程序的开发者,由于该应用程序是跨平台的,您可能需要向最终用户发布针对该应用程序支持的每个操作系统的二进制文件。因此,您可以在构建脚本中为要发布软件的每个操作系统(Windows、Linux 等)定义不同的目标对象。这将使编译zig
器能够一次性将您的项目构建到多个目标操作系统。Zig Build System 官方文档中有一个很棒的代码示例4演示了这一策略。
Build
目标对象由我们在第 9.2 节中介绍的结构体的以下方法创建:
addExecutable()
创建一个可执行文件;addSharedLibrary()
创建共享库文件;addStaticLibrary()
创建静态库文件;addTest()
创建执行单元测试的可执行文件。
这些函数是来自Build
结构体的方法,这些方法作为函数的输入build()
。它们都会创建一个Compile
对象作为输出,该对象表示zig
编译器要编译的目标对象。所有这些函数都接受类似的结构体字面量作为输入。该结构体字面量定义了关于您正在构建的目标对象的三个基本规范:name
、target
和root_source_file
。
我们已经在上一个示例中看到了这三个选项的用法,其中我们使用了addExecutable()
方法来创建一个可执行的目标对象。此示例如下所示。请注意结构体path()
中 方法的使用Build
,它在选项中定义了一个路径root_source_file
。
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
});
该name
选项指定要赋予此目标对象定义的二进制文件的名称。因此,在本例中,我们将构建一个名为 的可执行文件hello
。通常将此name
选项设置为项目名称。
此外,该target
选项指定了此二进制文件的目标计算机体系结构(或目标操作系统)。例如,如果您希望此目标对象在使用特定x86_64
体系结构的 Windows 计算机上运行,则可以将此target
选项设置为x86_64-windows-gnu
。这将使zig
编译器将项目编译为在 Windows 计算机上运行。您可以通过在终端中运行该命令来x86_64
查看编译器支持的完整体系结构和操作系统列表。zig``zig targets
现在,如果您要构建项目以在当前用于运行此构建脚本的机器上运行,则可以将此target
选项设置为对象host
的方法Build
,就像我们在上面的示例中所做的那样。此host
方法标识当前运行zig
编译器的机器。
最后,该root_source_file
选项指定项目的根 Zig 模块。该 Zig 模块包含应用程序的入口点(即main()
函数),或者库的主要 API。这也意味着,构成项目的所有 Zig 模块都会自动从此“根源文件”中的 import 语句中发现。zig
编译器可以通过 import 语句检测 Zig 模块何时依赖于其他模块,从而发现项目中使用的完整 Zig 模块映射。
这很方便,而且与其他构建系统不同。例如,在 CMake 中,您必须明确列出要包含在构建过程中的所有源文件的路径。这可能是 C 和 C++ 编译器“缺乏条件编译”的症状。由于它们缺乏此功能,您必须明确选择要将哪些源文件发送到 C/C++ 编译器,因为并非所有 C/C++ 代码都可移植或受所有操作系统支持,因此会导致 C/C++ 编译器出现编译错误。
现在,关于构建过程的一个重要细节是,您必须使用结构的方法明确安装在构建脚本中创建的目标对象。installArtifact()``Build
build
每次通过调用编译器命令来启动项目的构建过程时,都会在项目的根目录中创建zig
一个名为 的新目录zig-out
。此新目录包含构建过程的输出,即从源代码构建的二进制文件。
该installArtifact()
方法的作用是将您定义的构建目标对象安装(或复制)到此zig-out
目录。这意味着,如果您没有安装在构建脚本中定义的目标对象,那么这些目标对象在构建过程结束时基本上会被丢弃。
例如,您可能正在构建一个使用第三方库的项目,而该库是与项目一起构建的。因此,在构建项目时,您首先需要构建第三方库,然后将其链接到项目的源代码。因此,在这种情况下,我们在构建过程中会生成两个二进制文件(项目的可执行文件和第三方库)。但只有一个文件值得关注,即我们项目的可执行文件。我们可以丢弃第三方库的二进制文件,只需不将其安装到此zig-out
目录中即可。
这个installArtifact()
方法很简单。只需记住将它应用于要保存到zig-out
目录的每个目标对象即可,如下例所示:
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
});
b.installArtifact(exe);
9.4设置构建模式
我们已经讨论了创建新目标对象时设置的三个基本选项。但还有第四个选项可用于设置此目标对象的构建模式,即选项optimize
。之所以这样称呼此选项,是因为 Zig 中的构建模式更多地被视为“优化与安全”问题。因此,优化在这里起着重要作用。别担心,我很快会回到这个问题上。
在 Zig 中,我们有四种构建模式(如下所示)。每一种构建模式都具有不同的优势和特点。正如我们在第 5.2.1 节中所述,当您没有明确选择构建模式时,zig
编译器默认使用构建模式。Debug
Debug
,在构建过程的输出(即目标对象定义的二进制文件)中产生并包含调试信息的模式;ReleaseSmall
,尝试生成较小二进制文件的模式;ReleaseFast
,尝试优化代码的模式,以便尽可能快地生成二进制文件;ReleaseSafe
,通过在可能的情况下采取保护措施,尝试使您的代码尽可能安全。
因此,在构建项目时,您可以将目标对象的构建模式设置为ReleaseFast
例如,这将指示zig
编译器在代码中应用重要的优化。这会创建一个二进制文件,该文件在大多数情况下运行速度更快,因为它包含代码的更优化版本。然而,结果往往会导致代码中丢失一些安全特性。因为一些安全检查从最终的二进制文件中移除,这虽然会使代码运行速度更快,但安全性却有所降低。
这个选择取决于你当前的优先级。如果你正在构建加密或银行系统,你可能更倾向于优先考虑代码的安全性,因此,你会选择ReleaseSafe
构建模式,这种模式运行速度稍慢,但更安全,因为它在构建过程中生成的二进制文件中包含了所有可能的运行时安全检查。另一方面,如果你正在编写游戏,你可能更倾向于优先考虑性能而不是安全性,使用ReleaseFast
构建模式,这样你的用户就可以在游戏中体验到更快的帧率。
在下面的示例中,我们创建了与之前示例相同的目标对象。但这次,我们将此目标对象的构建模式指定为ReleaseSafe
mode。
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
.optimize = .ReleaseSafe
});
b.installArtifact(exe);
9.5设置构建版本
每次在构建脚本中构建目标对象时,都可以按照语义版本控制框架为该特定构建分配一个版本号。您可以访问语义版本控制网站5SemanticVersion
了解有关语义版本控制的更多信息。无论如何,在 Zig 中,您可以通过向选项提供一个结构体来指定构建的版本,version
如下例所示:
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
.version = .{
.major = 2, .minor = 9, .patch = 7
}
});
b.installArtifact(exe);
9.6在构建脚本中检测操作系统
在构建系统中,根据构建过程中所针对的操作系统 (OS) 使用不同的选项、包含不同的模块或链接不同的库是很常见的。
os.tag
在 Zig 中,您可以通过查看 Zig 库中的模块内部来检测构建过程的目标操作系统builtin
。在下面的示例中,当构建过程的目标是 Windows 系统时,我们使用 if 语句来运行一些任意代码。
const builtin = @import("builtin");
if (builtin.target.os.tag == .windows) {
// Code that runs only when the target of
// the compilation process is Windows.
}
9.7在构建过程中添加运行步骤
Rust 的一个巧妙之处在于,你可以使用cargo run
Rust 编译器中的一个命令 ( ) 来编译和运行源代码。我们在1.2.5 节run
中看到了如何在 Zig 中执行类似的工作,即通过编译器的命令构建和运行 Zig 源代码zig
。
但是,我们如何才能同时构建并运行构建脚本中目标对象指定的二进制文件呢?答案是,在构建脚本中包含一个“运行工件”。运行工件是通过结构体addRunArtifact()
中的方法创建的Build
。我们只需将描述我们要执行的二进制文件的目标对象作为输入提供给此函数即可。结果,此函数会创建一个能够执行此二进制文件的运行工件。
在下面的示例中,我们定义了一个名为的可执行二进制文件hello
,并且我们使用此addRunArtifact()
方法来创建将执行该可执行文件的运行工件hello
。
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("src/hello.zig"),
.target = b.host
});
b.installArtifact(exe);
const run_arti = b.addRunArtifact(exe);
现在我们已经创建了这个运行构件,我们需要将其包含在构建过程中。为此,我们在构建脚本中声明一个新步骤,通过结构体step()
的方法调用此构件Build
。
我们可以给这个步骤起任何名字,但为了方便理解,我将其命名为“运行”。此外,我还为这个步骤提供了一个简短的描述(“运行项目”)。
const run_step = b.step(
"run", "Run the project"
);
现在我们已经声明了这个“运行步骤”,我们需要告诉 Zig,这个“运行步骤”依赖于运行构件。换句话说,运行构件始终依赖于某个“步骤”才能有效执行。通过创建此依赖关系,我们最终确定了从构建脚本构建和运行可执行文件所需的命令。
我们可以使用运行步骤中的方法在运行步骤和运行工件之间建立依赖关系dependsOn()
。因此,我们首先创建运行步骤,然后使用dependsOn()
运行步骤中的方法将其与运行工件链接起来。
run_step.dependOn(&run_arti.step);
我们在本节中逐一编写的这个特定构建脚本的完整源代码都可以在build_and_run.zig
模块中找到。您可以访问本书的官方仓库 6来查看此模块。
当你在构建脚本中声明一个新步骤时,该步骤将通过编译器build
中的命令可用zig
。你实际上可以通过zig build --help
在终端中运行来查看此步骤,如下例所示,我们可以看到在构建脚本中声明的这个新“运行”步骤出现在输出中。
zig build --help
Steps:
...
run Run the project
...
build
现在,我们需要做的就是调用我们在构建脚本中创建的“运行”步骤。我们使用编译器命令后指定的步骤名称来调用它zig
。这将使编译器构建我们的可执行文件并同时执行它。
zig build run
9.8在项目中构建单元测试
我们已经在第 8 章中详细讨论了如何在 Zig 中编写单元测试,并且还讨论了如何通过编译器test
的命令执行这些单元测试zig
。但是,正如我们run
在上一节中对命令所做的那样,我们可能还希望在构建脚本中包含一些命令,以便在项目中构建和执行单元测试。
因此,我们将再次讨论如何在 Zig 的构建脚本中使用编译器的特定内置命令zig
(在本例中为test
命令)。这就是“测试目标对象”发挥作用的地方。如第 9.3 节addTest()
所述,我们可以使用结构体的方法创建测试目标对象Build
。我们需要做的第一件事是在构建脚本中声明一个测试目标对象。
const test_exe = b.addTest(.{
.name = "unit_tests",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
b.installArtifact(test_exe);
测试目标对象本质上会选择test
项目中所有 Zig 模块中的所有块,并仅构建项目中这些块中存在的源代码。因此,此目标对象会创建一个可执行文件,其中仅包含项目中所有这些块(即单元测试)test
中存在的源代码。test
完美!现在我们已经声明了这个测试目标对象,当我们使用命令触发构建脚本时,编译器unit_tests
会构建一个名为 的可执行文件。构建过程完成后,我们可以在终端中直接执行这个可执行文件。zig``build``unit_tests
但是,如果您还记得上一节,我们已经了解了如何在构建脚本中创建运行步骤,以执行由构建脚本构建的可执行文件。
因此,我们可以简单地在构建脚本中添加一个运行步骤,以便通过zig
编译器中的单个命令运行这些单元测试,从而简化我们的工作。在下面的示例中,我们演示了在构建脚本中注册一个名为“tests”的新构建步骤来运行这些单元测试的命令。
const run_arti = b.addRunArtifact(test_exe);
const run_step = b.step("tests", "Run unit tests");
run_step.dependOn(&run_arti.step);
现在我们已经注册了这个新的构建步骤,我们可以通过在终端中调用以下命令来触发它。你也可以在本书官方仓库的build_tests.zig
模块中查看此特定构建脚本的完整源代码7。
zig build tests
9.9使用用户提供的选项定制构建过程
有时,您需要创建一个可由项目用户自定义的构建脚本。您可以通过在构建脚本中创建用户提供的选项来实现。我们使用结构体option()
中的方法来创建用户提供的选项Build
。
通过这种方法,我们创建了一个“构建选项”,可以build.zig
通过命令行传递给脚本。用户可以在编译器build
的命令中设置此选项zig
。换句话说,我们创建的每个构建选项都将成为一个新的命令行参数,可以通过build
编译器的命令访问。
这些“用户提供的选项”通过在命令行中使用前缀来设置-D
。例如,如果我们声明一个名为 的选项use_zlib
,它接收一个布尔值,指示是否应该将源代码链接到zlib
,我们可以在命令行中使用 来设置此选项的值-Duse_zlib
。下面的代码示例演示了这个想法:
const std = @import("std");
pub fn build(b: *std.Build) void {
const use_zlib = b.option(
bool,
"use_zlib",
"Should link to zlib?"
) orelse false;
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("example.zig"),
.target = b.host,
});
if (use_zlib) {
exe.linkSystemLibrary("zlib");
}
b.installArtifact(exe);
}
zig build -Duse_zlib=false
9.10链接到外部库
每个构建过程的一个重要部分是链接阶段。此阶段负责将代表代码的多个目标文件组合成一个可执行文件。如果您在代码中使用了外部库,它还会将此可执行文件链接到外部库。
在 Zig 中,我们有两个“库”的概念:1)系统库;2)本地库。系统库是指系统中已安装的库。而本地库是指属于当前项目的库;它存在于项目目录中,并且与项目源代码一起构建。
两者之间的基本区别在于,系统库据称已经构建并安装在您的系统中,您只需将源代码链接到该库即可开始使用它。我们通过使用对象linkSystemLibrary()
的方法来实现这一点Compile
。此方法接受字符串形式的库名称作为输入。请记住,在9.3 节中,对象Compile
是您在构建脚本中声明的目标对象。
当你将特定的目标文件与系统库链接时,zig
编译器会pkg-config
查找系统中该库的二进制文件和头文件的位置。找到这些文件后,zig
编译器中的链接器会将你的目标文件与该库的文件链接起来,生成一个单独的二进制文件。
在下面的示例中,我们正在创建一个名为的可执行文件image_filter
,并且我们使用方法将此可执行文件链接到 C 标准库linkLibC()
,但我们也将此可执行文件链接到libpng
当前安装在我的系统中的 C 库。
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "image_filter",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.linkSystemLibrary("png");
b.installArtifact(exe);
}
如果您在项目中链接了 C 库,通常最好也将您的代码链接到 C 标准库。因为这个 C 库很可能在某些时候会用到 C 标准库的某些功能。C++ 库也是如此。因此,如果您要链接 C++ 库,最好使用该linkLibCpp()
方法将您的项目链接到 C++ 标准库。
在订单方面,当您想要链接到本地库时,您应该使用对象linkLibrary()
的方法Compile
。此方法需要接收另一个Compile
对象作为输入。也就是说,构建脚本中定义的另一个目标对象,使用addStaticLibrary()
或addSharedLibrary()
方法定义要构建的库。
正如我们之前所讨论的,本地库是项目本地的库,它会与项目一起构建。因此,您需要在构建脚本中创建一个目标对象来构建此本地库。然后,将项目中所需的目标对象与标识此本地库的目标对象链接起来。
看一下从libxev
库8的构建脚本中提取的这个示例。您可以看到,在这个代码片段中,我们从模块中声明了一个共享库文件c_api.zig
。然后,在构建脚本的后面,我们声明了一个名为 的可执行文件"dynamic-binding-test"
,该文件链接到我们之前在脚本中定义的共享库。
const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});
const dynamic_lib = b.addSharedLibrary(.{
.name = dynamic_lib_name,
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(dynamic_lib);
// ... more lines in the script
const dynamic_binding_test = b.addExecutable(.{
.name = "dynamic-binding-test",
.target = target,
.optimize = optimize,
});
dynamic_binding_test.linkLibrary(dynamic_lib);
9.11构建 C 代码
该zig
编译器内置了一个 C 编译器。换句话说,您可以使用该编译器来构建 C 项目。您可以通过编译器的命令zig
来调用该 C 编译器。cc``zig
举个例子,我们来使用著名的FreeType 库9。FreeType 是世界上使用最广泛的软件之一。它是一个用 C 语言编写的库,旨在生成高质量的字体。但它也在业界被广泛使用,用于在计算机屏幕上原生渲染文本和字体。
在本节中,我们将逐步编写一个构建脚本,该脚本能够从源代码构建 FreeType 项目。您可以在 GitHub 的freetype-zig
仓库10中找到此构建脚本的源代码。
从官方网站11下载 FreeType 源代码后,就可以开始编写build.zig
模块了。我们首先定义目标对象,该对象定义了我们要编译的二进制文件。
addStaticLibrary()
作为示例,我将使用创建目标对象的方法将项目构建为静态库文件。此外,由于 FreeType 是一个 C 库,我还将libc
通过该linkLibC()
方法链接该库,以确保在编译过程中涵盖所有对 C 标准库的使用。
const target = b.standardTargetOptions(.{});
const opti = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "freetype",
.optimize = opti,
.target = target,
});
lib.linkLibC();
9.11.1创建 C 编译器标志
编译器标志也被许多程序员称为“编译器选项”,在 GCC 官方文档中也被称为“命令选项”。也可以将它们称为 C 编译器的“命令行参数”。通常,我们使用编译器标志来打开(或关闭)编译器的某些功能,或者调整编译过程以满足项目需求。
在用 Zig 编写的构建脚本中,我们通常在一个简单的数组中列出编译过程中要使用的 C 编译器标志,如下例所示。
const c_flags = [_][]const u8{
"-Wall",
"-Wextra",
"-Werror",
};
理论上,没有什么可以阻止您使用此数组向编译过程添加“包含路径”(带有-I
标志)或“库路径”(带有标志)。但是 Zig 中有正式的方法可以在编译过程中添加这些类型的路径。这两部分都将在第 9.11.5 节和第 9.11.4 节-L
中讨论。
addCSourceFile()
无论如何,在 Zig 中,我们使用和方法将 C 标志与要编译的 C 文件一起添加到构建过程中addCSourceFiles()
。在上面的示例中,我们刚刚声明了要使用的 C 标志。但我们还没有将它们添加到构建过程中。为此,我们还需要列出要编译的 C 文件。
9.11.2列出你的 C 文件
包含“跨平台”源代码的 C 文件列在c_source_files
下面的对象中。这些 C 文件默认包含在 FreeType 库支持的每个平台上。由于 FreeType 库中的 C 文件数量庞大,为了简洁起见,我在下面的代码示例中省略了其余文件。
const c_source_files = [_][]const u8{
"src/autofit/autofit.c",
"src/base/ftbase.c",
// ... and many other C files.
};
现在,除了“跨平台”源代码之外,FreeType 项目中还有一些平台特定的 C 文件。这意味着,它们包含只能在特定平台上编译的源代码,因此,它们只会包含在这些特定目标平台上的构建过程中。列出这些 C 文件的对象在下面的代码示例中公开。
const windows_c_source_files = [_][]const u8{
"builds/windows/ftdebug.c",
"builds/windows/ftsystem.c"
};
const linux_c_source_files = [_][]const u8{
"src/base/ftsystem.c",
"src/base/ftdebug.c"
};
现在我们已经声明了想要包含的文件和要使用的 C 编译器标志,我们可以使用addCSourceFile()
和addCSourceFiles()
方法将它们添加到描述 FreeType 库的目标对象中。
这两个函数都是Compile
对象(即目标对象)的方法。 该addCSourceFile()
方法能够将单个 C 文件添加到目标对象,而 则addCSourceFiles()
用于在单个命令中添加多个 C 文件。addCSourceFile()
当您需要在项目中的特定 C 文件上使用不同的编译器标志时,您可能更喜欢使用 。但是,如果您可以在所有 C 文件中使用相同的编译器标志,那么您可能会找到addCSourceFiles()
更好的选择。
请注意,我们使用了addCSourceFiles()
以下示例中的方法,来添加 C 文件和 C 编译器标志。另请注意,我们使用了9.6 节os.tag
中介绍的方法来添加平台特定的 C 文件。
const builtin = @import("builtin");
lib.addCSourceFiles(
&c_source_files, &c_flags
);
switch (builtin.target.os.tag) {
.windows => {
lib.addCSourceFiles(
&windows_c_source_files,
&c_flags
);
},
.linux => {
lib.addCSourceFiles(
&linux_c_source_files,
&c_flags
);
},
else => {},
}
9.11.3定义 C 宏
-D
C 宏是 C 编程语言的重要组成部分,通常通过C 编译器的标志来定义。在 Zig 中,您可以使用定义defineCMacro()
正在构建的二进制文件的目标对象中的方法来定义要在构建过程中使用的 C 宏。
在下面的示例中,我们使用lib
前面几节中定义的对象来定义 FreeType 项目在编译过程中使用的一些 C 宏。这些 C 宏指定 FreeType 是否应该包含来自不同外部库的功能。
lib.defineCMacro("FT_DISABLE_ZLIB", "TRUE");
lib.defineCMacro("FT_DISABLE_PNG", "TRUE");
lib.defineCMacro("FT_DISABLE_HARFBUZZ", "TRUE");
lib.defineCMacro("FT_DISABLE_BZIP2", "TRUE");
lib.defineCMacro("FT_DISABLE_BROTLI", "TRUE");
lib.defineCMacro("FT2_BUILD_LIBRARY", "TRUE");
9.11.4添加库路径
库路径是计算机中 C 编译器查找(或搜索)库文件以链接到源代码的路径。换句话说,当您在 C 源代码中使用某个库,并要求 C 编译器将源代码链接到该库时,C 编译器会在“库路径”集合中列出的路径中搜索该库的二进制文件。
这些路径是平台相关的,默认情况下,C 编译器会先查看计算机中一组预定义的位置。但您可以向此列表添加更多路径(或更多位置)。例如,您可能在计算机上非常规位置安装了某个库,您可以通过将此路径添加到预定义路径列表中,让 C 编译器“看到”这个“非常规位置”。
在 Zig 中,您可以使用addLibraryPath()
目标对象中的方法向此集合添加更多路径。首先,定义一个LazyPath
包含要添加的路径的对象,然后将该对象作为addLibraryPath()
方法的输入,如下例所示:
const lib_path: std.Build.LazyPath = .{
.cwd_relative = "/usr/local/lib/"
};
lib.addLibraryPath(lib_path);
9.11.5添加包含路径
预处理器搜索路径是 C 社区的一个流行概念,但许多 C 程序员也将其称为“包含路径”,因为此“搜索路径”中的路径与#include
C 文件中找到的语句相关。
包含路径类似于库路径。它们是计算机中一组预定义的位置,C 编译器会在编译过程中查找文件。但包含路径不是查找库文件,而是编译器查找 C 源代码中包含的头文件的位置。这就是为什么许多 C 程序员更喜欢将这些路径称为“预处理器搜索路径”。因为头文件是在编译过程的预处理器阶段处理的。
因此,每个通过 a 语句包含在 C 源代码中的头文件#include
都需要在某个地方找到,并且 C 编译器会在“包含路径”集合中列出的路径中搜索该头文件。包含路径通过 标志添加到编译过程中-I
。
addIncludePath()
在 Zig 中,您可以使用目标对象中的方法向这组预定义的路径中添加新路径。此方法也接受LazyPath
对象作为输入。
const inc_path: std.Build.LazyPath = .{
.path = "./include"
};
lib.addIncludePath(inc_path);
-
https://ziglang.org/learn/build-system/#user-provided-options ↩︎
-
https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html ↩︎
-
https://github.com/pedropark99/zig-book/blob/main/ZigExamples/build_system/build_and_run.zig ↩︎
-
https://github.com/pedropark99/zig-book/blob/main/ZigExamples/build_system/build_tests.zig ↩︎