注意:大多数开发者应通过
fx工作流(例如fx build和fx set)与构建系统交互,而不是直接运行 GN、Ninja 或 Bazel(即使通过fx gn或fx ninja等封装容器)。
本部分将介绍 GN 的术语和思维方式。这些背景信息应该足以让您了解 GN 及其在 Fuchsia 中的使用方式。 GN(和 Fuchsia build)比下文讨论的要复杂得多,但普通开发者不需要更深入地了解其中的大部分内容。
GN 文档页面QuickStart和语言提供了有关 GN 的更详细背景信息,参考提供了完整的语言文档。使用 gn help 命令以交互方式输出各个主题的参考文档。Ninja 也有自己的文档。
在运行 jiri update 后的 Fuchsia 检出中,命令 fx gn 和 fx ninja 可提供对预构建二进制文件的访问权限。
两阶段操作:gn 和 ninja
与make不同,gn只是故事的一半。顾名思义,GN 代表生成 Ninja。这些工具之间的职责划分对应于将运行 build 分为两个步骤:
gn gen会做出所有配置选择和决策。 它实际上只是在 build 目录中生成.ninja文件。只有在您更改配置或完全删除 build 目录时,才需要手动执行此步骤。一般来说,只有在 GN 文件发生更改时才需要执行此操作,而在增量 build 中,如果 GN 文件或配置发生更改,则会自动执行此操作。ninja运行命令来编译和链接等。它负责处理增量构建和并行处理。每次更改源文件时,您都需要执行此步骤,例如运行make。当相关BUILD.gn文件(或其他一些相关文件)发生更改时,GN 会自动发出规则,通过再次运行gn gen来重新生成 Ninja 文件,因此对于首次构建后的大多数更改,ninja会完成所有操作。
与 GNU make 等工具相比,Ninja 非常简单。它只是比较时间并运行命令,其输入文件由机器而非人类编写。不过,它内置了一些我们在 make 中竭力实现的功能:
- 在命令行发生更改时重新构建每个文件。只有在 GN 再次运行时,命令行才会真正发生变化。但在此之后,Ninja 会智能地进行增量构建,重新执行已更改文件的命令,而不重新运行未更改文件的命令。
- 处理编译器生成的依赖项文件。Ninja 知道编译器在
.d文件中发出的 makefile 子集,并在 GN 指示时直接使用它们。 - 默认使用
-j$(getconf _NPROCESSORS_ONLN)运行。您可以在使用远程构建服务时传递-j1以进行序列化,或传递-j1024,但它通常会开箱即用地执行您想要的并行处理。 - 防止并行作业的
stdout/stderr输出交错。Ninja 会缓冲输出,以便出错提示不会因多个进程的输出而变得混乱。 - 支持简洁/详细的命令输出。默认情况下,Ninja 会针对其运行的每个命令发出简短的
Kbuild样式消息,采用冗长的进度条样式。-v 开关类似于Kbuild中的 V=1,用于显示每个实际命令。
GN 是作为 Chromium 项目的一部分开发的,旨在取代旧版构建系统。Fuchsia 从它们那里继承了 GN,现在 GN 在整个树中用作主要构建系统。
build 目录和 args.gn
Ninja 始终在 build 目录中运行。Ninja 运行的所有命令都是从 build 目录的根目录运行的。共同点是ninja -C build-dir。
GN 和 Ninja 都不关心您使用哪个 build 目录。通常的做法是使用源目录的子目录,并且由于文件路径通常会重新基于 build 目录,因此如果您将 build 目录放在其他位置,提供给编译器的文件名中会有很多 ../;但应该可以正常运行。在 Chromium 中,在源目录中使用 out/_something_ 长期以来一直是一种常见做法(早于 GN 本身),Fuchsia 继承了该默认设置。不过,虽然 out 子目录位于 Fuchsia 的顶级 .gitignore 文件中,但您选择的 build 目录名称并不重要。
基本命令为 gn gen build-dir。此命令会根据需要创建 build-dir/,并使用当前配置的 Ninja 文件填充该目录。如果 build-dir/args.gn 存在,则 gn gen 将读取该文件以设置 GN build 参数(见下文)。args.gn 是一个采用 GN 语法的文件,可为 GN build 参数分配值,以替换任何硬编码的默认值。这意味着,只需重复 gn gen build-dir 即可保留您上次执行的操作。
您还可以将 --args=... 添加到 gn gen,或使用 gn args 命令来配置 build 参数。gn args 命令可让您在 args.gn 文件上运行 $EDITOR,并且在退出编辑器后,该命令将使用新参数为您重新运行 gn gen。您也可以随时修改 args.gn,下次运行 Ninja 时,系统会重新生成 build 文件。
您也可以使用 fx set 命令(该命令会调用 gn gen)来设置参数。例如,如需通过 fx set 将 foobar 设置为 true,请使用以下代码:
$ fx set <your configuration> --args 'foobar=true'
如需了解详情,请参阅 GN 构建参数。
GN 语法和格式
GN 语法不区分空格。x=1 y=2 等同于:
x = 1
y = 2
不过,GN 代码有一种真正的缩进和格式设置样式。gn format 命令会将语法有效的 GN 代码重新设置为规范样式。支持 Emacs 和 Vim 的编辑器语法。Tricium 将强制执行规范格式,并进行大规模重新格式化。如果您不喜欢这种格式,请提交 bug 或在上游 GN 中进行更改,如果更改被接受,我们将对所有人的代码进行批量重新格式化,以符合新的唯一标准。
源路径和 GN 标签
GN 使用 POSIX 样式的路径(始终表示为字符串)来表示文件和引用 GN 定义的实体。路径可以是相对路径,即相对于包含 BUILD.gn 文件的目录(路径字符串出现在该文件中)。它们也可以是“源绝对路径”,即相对于源代码树的根目录。在 GN 中,源绝对路径以 // 开头。
当源路径最终在命令中使用时,它们会被转换为适合操作系统的路径,这些路径可以是绝对路径,也可以是相对于 build 目录(命令运行的位置)的相对路径。
预定义变量在源路径上下文中使用,用于定位构建目录的各个部分:
$root_build_dir是 build 目录本身$root_out_dir是当前工具链的子目录(见下文)- 这是所有“顶级”目标的存放位置。在许多 GN build 中,所有可执行文件和库都存放在此处。
$target_out_dir是$root_out_dir的子目录,用于存放当前BUILD.gn文件中的目标所构建的文件。目标文件会存放在这里。$target_gen_dir是建议放置生成代码的对应位置$root_gen_dir是存放此子目录之外所需的生成代码的位置
GN 标签是我们引用 BUILD.gn 文件中定义的内容的方式。它们基于源路径,并且始终显示在 GN 字符串中。GN 标签的完整语法为 "dir:name",其中 dir 部分是命名特定 BUILD.gn 文件的源路径。name 是指该文件中使用 target_type("name") { ... } 定义的目标。为简便起见,您可以定义一个与其目录同名的目标。不含 : 部分的标签 "//path/to/dir" 是 "//path/to/dir:dir" 的简写形式。这是最常见的情况。
依赖关系图和 BUILD.gn 文件
GN 中的所有内容都根植于依赖关系图。有一个根 BUILD.gn 文件。只有在依赖于该目录中的标签时,系统才会读取其他 BUILD.gn 文件。
没有通配符。每个目标都必须命名为其他某个目标的依赖项,才能进行构建。您可以在 ninja 命令行中指定各个目标,以明确构建这些目标。否则,它们必须位于 //:default 目标(在根 BUILD.gn 文件中名为 default)的图表中。
有一种名为 group() 的通用元目标类型,它不对应于 build 生成的文件,而是用于很好地构建依赖项图。default 等顶级目标通常是组。您可以为某个硬件的所有驱动程序创建一个组,为某个用例中的所有二进制文件创建一个组,等等。
当某些代码在运行时使用某些内容(数据文件、其他可执行文件等)时但在构建时不会将其用作直接输入,则该文件属于使用它的目标的 data_deps 列表。这也足以将该内容放入 BOOTFS 映像中的指定位置。
目标也可以使用 testonly = true 进行标记,以指明目标包含测试。GN 会阻止非 testonly 的目标依赖于 testonly 的目标,从而在一定程度上控制测试二进制文件的最终位置。
构建映像文件由一个或多个 zbi() 目标驱动。这将通过构建和使用 ZBI 主机工具来创建 ZBI。目标可以通过存在于此映像的依赖关系图中来放置在此映像中,因此您可以为其提供对内核以及您希望在映像中包含的任何驱动程序或可执行文件的依赖关系。
请注意,获取 Ninja 文件中定义的目标是以 BUILD.gn 文件为粒度,而默认目标或任何其他目标的依赖关系图是以单个目标为粒度。因此,如果图表中的 BUILD.gn 文件中默认包含某个目标,则该文件(和工具链,见下文)中的所有目标都可作为 Ninja 命令行中的目标,即使它们默认不构建。
更多高级概念
GN 表达式语言和 GN 作用域
GN 是一种简单的动态类型命令式语言,其最终目的只是生成声明式 Ninja 规则。一切都围绕着作用域展开,作用域既是语言的词法绑定构造,也是一种数据类型。
GN 值可以采用以下几种类型:
- 布尔值:
true或false - 整数,使用普通十进制语法签名;不常用
- 字符串,始终用“双引号”括起来(请参阅下文关于
$扩展的说明) - 范围(用大括号括起来):
{ ... };请参阅下文。 - 方括号中的值列表:
[ 1, true, "foo", { x=1 y=2 } ]是一个包含四个元素的列表。
值是动态类型的,没有任何隐式类型强制转换,但绝不会进行类型检查。不同类型的值绝不会比较为相等,但比较它们不会出错。
字符串字面量会展开双引号内的简单 $var 或 ${var} 表达式。这是一种即时扩展:当 var 是字符串时,x${var}y 与 x +
var + y 相同。这样,任何值都可以呈现为美观输出的字符串。
由字母数字和下划线组成的标识符可以通过赋值运算符填充范围。使用 = 进行命令式赋值和通过 += 进行修改实际上是 GN 语言所做的全部工作(还有一些特殊的方法可以产生副作用,例如 print(),用于调试;以及 write_file(),谨慎使用)。
每个文件在内部都表示为一个作用域,并且没有全局作用域。
共享的“全局变量”可以在 .gni 文件中定义,并在使用它们的位置 (import("//path/to/something.gni")) 导入。每个 .gni 文件在每个工具链中处理一次(请参阅下文了解工具链),并将生成的范围复制到导入文件范围中。
目标声明会引入一个子作用域:
foo = true
executable("target") {
foo = 12
}
# Outside the target, foo == true
GN 在诊断变量已定义但从未在范围内使用过的错误时非常严格。目标内的范围就像目标的关键字实参列表,用于检查实参名称是否拼写正确。如果省略了必需的参数,目标定义代码也可以使用 assert() 来诊断错误。
值也可以是范围。然后,当您使用它时,它的行为就像一个结构体:value.member。但作用域始终是一个 GN 代码块,执行后会生成一组名称和值:
foo = {
x = global_tuning + 42
if (some_global && other_thing == "foobar") {
y = 2
}
}
此字段始终定义 foo.x,但有时仅定义 foo.y。
GN 工具链
GN 有一个名为“工具链”的概念。这一切都将在后台进行,开发者无需直接处理,但了解其机制会有所帮助。
它封装了编译器和默认编译开关。这也是以不同方式对同一内容进行两次编译的唯一真正方法。Fuchsia 中将有多个工具链:
- 主机
- 原生用户空间(使用默认
-fPIE编译) - 用户空间中的共享库(使用
-fPIC编译) userboot- 内核
- ARM64 的内核物理地址模式(使用
-mstrict-align编译) - x86 的 32 位内核物理地址模式(使用
-m32编译) - Gigaboot 的 UEFI
- 工具链还用于“变体”方案,该方案允许我们为部分用户空间选择性地启用 ASan 或类似工具。
每个工具链都由一个 GN 标签标识。目标标签的完整语法实际上是 //path/to/dir:name(//path/to/toolchain/label)。通常,工具链会被省略,并扩展为 label($current_toolchain),即标签引用通常位于同一工具链中。
所有 GN 文件都会在每个工具链中单独实例化。每个工具链可以采用不同的方式设置全局变量,因此 GN 代码可以使用 if
(is_kernel) 或 if (current_toolchain == some_toolchain) 等测试在不同的上下文中表现出不同的行为。这样,GN 代码就会与其描述的源代码保持一致,但仍可为内核和用户等执行不同的共享源代码子集。