GN 简介

此课程旨在介绍 GN 的术语和思维方式。这些背景信息应足以获取您在 GN 中的方位及其在 Fuchsia 中的使用方式。 GN(和 Fuchsia build)比下面要讨论的更为复杂,但普通开发者不需要更深入地了解其中的大部分内容。

GN 文档页面快速入门语言页面提供了有关 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 会自动发出重新生成 Ninja 文件的规则,因此当相关 BUILD.gn 文件(或一些其他相关文件)发生更改时,GN 会重新运行 gn gen。因此,对于首次构建之后的大多数更改,ninja 都会执行全部操作。

与 GNU make 之类的组件相比,Ninja 非常简单。它只是比较时间并运行命令,其输入文件由机器(而不是人类)编写。不过,它包含了一些有用的功能,我们在此基础上演示了在 make 中完成的操作:

  • 当命令行发生更改时,重新构建每个文件。只有当 GN 再次运行时,命令行才会真正发生改变。但之后,Ninja 就会很聪明地采用增量构建的方式,对已更改的文件重复执行命令,而不会重新运行未更改的命令。
  • 处理编译器生成的依赖项文件。Ninja 知道编译器在 .d 文件中发出的 makefile 子集,并在 GN 指向时直接使用它们。
  • 默认情况下使用 -j$(getconf _NPROCESSORS_ONLN) 运行。使用 Goma 时,您可以传递 -j1 以进行序列化,也可传递 -j1024,但开箱即用,它可以提供您通常需要的并行性。
  • 防止并行作业中的交错 stdout/stderr 输出。Ninja 会缓冲输出,以免错误消息因多个进程发出而出现乱码。
  • 支持简洁/详细命令输出。默认情况下,Ninja 会针对它运行的每个命令,以文字进度表样式发出简短的 Kbuild 样式的消息。-v 开关类似于 Kbuild 中的 V=1,以显示每个实际命令。

GN 是作为 Chromium 项目的一部分开发的,用于取代旧版构建系统。Fuchsia 从这些分支继承了新属性,现在它在整个树中用作主要构建系统。

构建目录和 args.gn

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

GN 和 Ninja 都不在意您使用的 build 目录。通常的做法是使用源目录的子目录,由于文件路径通常会重新设置以相对于 build 目录,因此如果您将 build 目录放在其他位置,提供给编译器的文件名中会包含大量 ../;但应该可以正常运行。在 Chromium 中(早于 GN 本身)在源目录中使用 out/_something_ 早已成为一种常见的做法,而 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 命令配置构建参数。您可以使用 gn args 命令在 args.gn 文件上运行 $EDITOR,退出编辑器后,该命令将使用新参数重新运行 gn gen。您也可以随时只修改 args.gn,下次 Ninja 运行将重新生成 build 文件。

您还可以使用调用 gn genfx set 命令来设置参数。例如,通过 fx setfoxtrot 设置为 true

$ fx set <your configuration> --args 'foxtrot=true'

如需了解详情,请参阅 GN build 参数

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 是构建目录本身
  • $root_out_dir 是当前工具链的子目录(见下文)
    • 这是所有“顶级”目标的存储位置。在许多 GN build 中,所有可执行文件和库都位于此处。
  • $target_out_dir 是当前 BUILD.gn 文件中的目标构建的文件的 $root_out_dir 子目录。这就是目标文件所在的位置。
  • $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 目标依赖于相应目标,从而允许对测试二进制文件的最终位置进行一定程度的控制。

构建图片文件是由一个或多个 zbi() 目标驱动的。这将通过构建和使用 ZBI 主机工具来创建 ZBI。目标可以放置在此映像中的依赖关系图中,以便您可以赋予其依赖于内核以及映像中所需的任何驱动程序或可执行文件的依赖项。

请注意,获取 Ninja 文件中定义的目标的粒度是 BUILD.gn 文件的粒度,但默认目标或任何其他目标的依赖关系图是以单个目标的粒度计算的。因此,默认情况下,在图表的 BUILD.gn 文件中包含某个目标会使该文件中的所有目标(以及工具链,请参阅下文)作为目标在 Ninja 命令行中使用,即使这些目标并非默认构建也是如此。

更高级的概念

GN 表达式语言和 GN 范围

GN 是一种动态类型的简单命令式语言,其唯一目的就是生成声明性 Ninja 规则。一切都围绕作用域展开,作用域既是语言的词法绑定结构,也是一种数据类型。

GN 值可以采用以下任一类型:

  • 布尔值,truefalse
  • 整数,使用普通十进制语法语法有符号;不常用
  • 字符串,始终采用“英文双引号”(有关 $ 扩展的下文说明)
  • 用大括号括住的作用域:{ ... };请参阅下文。
  • 值列表(用方括号括起来):[ 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 中,有几种工具链:

  • 主机
  • Vanilla userland(使用默认 -fPIE 编译)
  • userland 中的共享库(使用 -fPIC 编译)
  • userboot
  • 内核
  • 适用于 ARM64 的内核物理地址模式(使用 -mstrict-align 编译)
  • x86 的 Multiboot(使用 -m32 编译)
  • 适用于 Gigaboot 的 UEFI
  • 工具链也用于“变体”方案,这就是我们允许为部分 userland 选择性地启用 ASan 或类似方案的方式。

每个工具链均由一个 GN 标签标识。目标标签的完整语法实际上是 //path/to/dir:name(//path/to/toolchain/label)。通常该工具链会被省略,并且它会扩展为 label($current_toolchain),也就是说,标签引用通常在同一工具链中。

所有 GN 文件在每个工具链中单独实例化。每个工具链都可以以不同的方式设置全局变量,因此 GN 代码可以使用 if (is_kernel)if (current_toolchain == some_toolchain) 等测试在不同环境中表现出不同的行为。这样,GN 代码会与它描述的源代码保持一致,但它仍然可以针对内核和用户等执行不同的共享源代码子集。