GN 简介

注意 大多数开发者应通过 fx 工作流 (例如 fx buildfx set)与构建系统互动,而不是直接运行 GN、Ninja 或 Bazel(即使是通过 fx gnfx ninja 等封装容器)。

本文将介绍 GN 的术语和思维方式。您只需了解这些背景知识,即可掌握 GN 的基本概念以及 GN 在 Fuchsia 中的使用方式。 GN(以及 Fuchsia 构建)比下文讨论的内容要复杂得多,但普通开发者无需深入了解其中的大部分内容。

GN 文档页面QuickStart语言提供了有关 GN 的更详细 背景信息,而参考文档则包含完整的语言文档。使用 gn help 命令可以交互方式输出各个主题的参考文档。 Ninja 也有自己的文档。

在运行 jiri update 后,Fuchsia 检出中的 fx gnfx ninja 命令可提供对预构建二进制文件的访问权限。

两阶段操作:gnninja

make 不同,gn 只是整个过程的一半。 顾名思义:GN 代表 Generate Ninja。这些工具之间存在责任划分,对应于将构建运行分为两个步骤:

  1. gn gen 会获取所有配置选择并做出所有决定。 它实际上所做的只是在 build 目录中生成 .ninja 文件。 只有在更改配置或彻底清除 build 目录时,才需要手动执行此步骤。 一般来说,只有在 GN 文件发生更改时才需要执行此步骤,而在增量构建中,如果 GN 文件或配置发生更改,则会自动执行此步骤。

  2. 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 从 Chromium 项目继承了 GN,现在 GN 在整个树中用作主要构建系统。

build 目录和 args.gn

Ninja 始终在 build 目录中运行。 Ninja 运行的所有命令都从 build 目录的根目录运行。 常见的命令是 ninja -C build-dir

GN 和 Ninja 都不关心您使用哪个 build 目录。 通常的做法是使用源目录的子目录,并且由于文件路径通常会重新定位为相对于 build 目录,因此如果您将 build 目录放在其他位置,则提供给编译器的文件名中将包含大量 ../;但它应该可以正常运行。 长期以来,在 Chromium 中(在 GN 本身之前)使用源目录中的 out/_something_ 是一种常见的做法,Fuchsia 继承了该默认设置。 不过,没有什么会关心您选择的 build 目录名称,尽管 out 子目录位于 Fuchsia 的顶级 .gitignore 文件中。

基本命令是 gn gen build-dir。 此命令会根据需要创建 build-dir/,并使用当前配置的 Ninja 文件填充该目录。 如果 build-dir/args.gn 存在,则 gn gen 将读取该文件以设置 GN 构建实参(请参阅下文)。 args.gn 是 GN 语法中的一个文件,可为 GN 构建实参分配值,以替换任何硬编码的默认值。 这意味着只需重复 gn gen build-dir 即可保留上次所做的操作。

您还可以将 --args=... 添加到 gn gen,或使用 gn args 命令来配置构建实参。 gn args 命令可让您在 args.gn 文件上运行 $EDITOR,退出编辑器后,该命令将使用新实参为您重新运行 gn gen。 您还可以随时编辑 args.gn,下次运行 Ninja 时将重新生成 build 文件。

还可以使用调用 gn genfx set 命令设置实参。例如,如需通过 fx setfoobar 设置为“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 目录(命令运行的位置)的相对路径。

预定义变量在源路径上下文中用于查找 build 目录的各个部分:

  • $root_build_dir 是 build 目录本身
  • $root_out_dir 是当前工具链的子目录(请参阅下文)
    • 这是所有“顶级”目标所在的位置。在许多 GN 构建中,所有 可执行文件和库都位于此处。
  • $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() 的通用元目标类型,它不对应于构建生成的文件,而是一种很好地构建依赖项图的方式。 像 default 这样的顶级目标通常是群组。 您可以为某个硬件的所有驱动程序创建一个群组,为某个用例中的所有二进制文件创建一个群组,等等。

当某些代码在运行时使用某些内容(数据文件、另一个可执行文件等) 但在构建时不会将其用作直接输入时,该文件属于使用它的目标的 data_deps 列表。 这也足以将该内容放入 BOOTFS 映像中的指定位置。

目标还可以使用 testonly = true 进行标记,以表明该目标包含测试。GN 会阻止非 testonly 的目标依赖于 testonly 的目标,从而允许对测试二进制文件的最终位置进行一定程度的控制。

构建映像文件由一个或多个 zbi() 目标驱动。 这将通过构建和使用 ZBI 主机工具来创建 ZBI。目标可以通过存在于其依赖项图中来放置在此映像中,因此您可以为其提供对内核以及您希望在映像中包含的任何驱动程序或可执行文件的依赖项。

请注意,获取 Ninja 文件中定义的目标的粒度为 BUILD.gn 文件,而来自默认目标或任何其他目标的依赖项图的粒度为单个目标。 因此,如果默认目标图中的 BUILD.gn 文件中包含某个目标,则该文件(和工具链,请参阅下文)中的所有目标都可用作 Ninja 命令行上的目标,即使它们默认情况下不会构建。

更多高级概念

GN 表达式语言和 GN 范围

GN 是一种简单的动态类型命令式语言,其最终目的只是生成声明式 Ninja 规则。 一切都围绕范围展开,范围既是该语言的词法绑定结构,也是一种数据类型。

GN 值可以采用以下几种类型:

  • 布尔值,truefalse
  • 整数,使用普通十进制语法进行签名;使用不多
  • 字符串,始终用双引号括起来(请注意下文有关 $ 扩展的说明)
  • 范围,用大括号括起来: { ... };请参阅下文。
  • 值列表,用方括号括起来:[ 1, true, "foo", { x=1 y=2 } ] 是一个 包含四个元素的列表。

值是动态类型的,并且没有任何隐式类型强制转换,但永远不会进行类型检查。 不同类型的值永远不会比较为相等,但比较它们不会出错。

字符串字面量会扩展双引号内的简单 $var${var} 表达式。 这是立即扩展:x${var}yx + var + y 相同,当 var 是字符串时。这样,任何值都可以呈现为经过美化的字符串。

由字母数字和下划线组成的标识符可以通过赋值运算符填充范围。 使用 = 进行命令式赋值和使用 += 进行修改实际上是 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 中,将有多个工具链:

  • 主机
  • Vanilla 用户空间(使用默认 -fPIE 编译)
  • 用户空间中的共享库(使用 -fPIC 编译)
  • userboot
  • Kernel
  • ARM64 的内核物理地址模式(使用 -mstrict-align 编译)
  • x86 的 Multiboot(使用 -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 代码会保留其描述的来源,但它仍然可以为内核和用户等执行不同的共享来源子集。