Chrome学习小记2:GN构建系统小记
Chrome学习小记2:GN构建系统小记
前言
很快,我们就要自己动手试试看自己写一个简单的小demo来驱动我们理解Arua等子系统的相互协作了。为此,咱们就需要进一步的动手写代码,自己调试出来一个可以跑的小demo,这样我们才能更加理解浏览器内部的工作原理。
为此,咱们首先需要理解Chrome的构建系统GN。
GN
GN的全称是Generate Ninja,我有干工程的好哥们说过,Ninja比起来还是更适合作为一个生成出来的中间编译指导文件而不是编写他本身,就像基于Makefile生成的CMake与Makefile的关系一样。谷歌的GN就是基于这个目的而产生的。
GN(Generate Ninja)是 Google/Chromium 社区开发的一个 meta-build system,它不直接执行编译,而是把工程元信息生成为 Ninja 可执行的构建文件(
build.ninja
),然后由ninja
来做实际的并行/增量构建。GN 的设计目标是:清晰、可维护、快速(生成阶段快),适合大型多平台项目。
在 Chromium(以及派生项目)早期使用的是 GYP(Python 实现的 meta-build)。为了提高生成速度、简化语义并更好地适应 Chromium 的多目标、多平台需求,Google 提出了 GN:它用 C++ 实现、专注于生成 Ninja 文件并且在实际工程中替代了 GYP。相对 GYP(Python 实现),GN 在生成速度和可维护性上被认为有明显优势(GN 的 README / 社区文档和多篇介绍都提到 GN 被设计为比 GYP 更快、更简洁)。
GN 的定位与架构(高层)
我们会编写一系列的一组 GN 构建脚本(通常以 BUILD.gn
和 .gni
为主)以及 build args(args.gn
/ gn args
),得到build.ninja
(以及其他中间文件),接着你运行 ninja -C out/...
来实际构建。
您如果手头没有Chrome源码,我建议您去克隆一份下来(毕竟,GN就是为了以Chrome源码代码的正确编译而产生的,看看源码可以更加直观的感受到为什么我们需要GN这样的构建系统来生成Ninja)
GN是一个声明式的构建系统。我们所有构建的目标都写在了BUILD.gn
(或简单 .gn
文件)里面,他们用于声明具体的 target(例如 executable
、static_library
、shared_library
、source_set
、group
、action
等)。.gni
文件用于放模板、通用配置或 declare_args()
(可被 import()
的文件),常用于复用。最佳实践是把模板放到 .gni
,把具体 target 写在 BUILD.gn
中。
构建系统的主题是离不开构建项目的依赖链的,我们很容易就想到了一个数据结构,那就是图(依赖图),GN 通过标签(如 //dir:target
)来引用目标;构建图以这些目标为节点,只有被 root target 或其依赖链引用的目标才会被评估(没有隐式通配)
另外,熟悉CMAKE的朋友知道,我们都会传递构建需要的参数来特质化我们的构建,GN中,使用的是declare_args()
,declare_args()
在 .gni
中用来声明构建参数默认值,用户可通过 gn args out/dir
或命令行 --args="..."
来覆盖。注意 declare_args
的默认值语义(文档中有细节说明)。
当然,CMake有函数,GN就有模板:模板让你把重复逻辑封装成函数式的声明,config
用于定义编译器/链接器片段可重用的配置。
以Chrome为例子:常用命令与工作流程(最实用的那几条)
创建/生成构建目录:
gn gen out/Default
这条命令根据当前的 BUILD.gn
/ args.gn
等生成 build.ninja
。所以这也就不难理解为什么Chrome源码编译的时候我们首先要做这一步
设置/查看构建参数:
gn args out/Default # 交互式编辑 args.gn
gn args out/Default --list
gn gen out/Default --args='is_debug=true target_cpu="x64"'
查看或诊断目标:
gn desc out/Default //base:base # 显示某个 target 的变量(sources, deps, include_dirs...)
gn ls out/Default "//base/*" # 列出匹配的 targets
gn format path/to/BUILD.gn # 按照规范格式化 GN 文件
GN 自带一套子命令(gen
, args
, desc
, ls
, format
, refs
, analyze
等),非常适合在大型代码库中排查问题。
运行构建(由 Ninja 执行):
ninja -C out/Default <target-name>
优点 / 局限(何时选用 GN)
大部分的大项目(嗯,虽然都是Google家的项目),比如说Chromium / V8 / Fuchsia 等项目都广泛使用 GN。GN 专注生成 Ninja 文件,语法简洁,生成器本身(C++ 写成)在大树上性能很好。
但是问题也很麻烦,
- 绑定到 Ninja:GN 输出的是 Ninja 输入,无法直接产生比如 Makefile 或 MSBuild(也就是说 Ninja 是必需的后端)。(Gn)
- 不像 Bazel 那样提供高度可重复/远程执行的内置分布式缓存/远程执行特性;如果你的目标是跨语言、跨团队的可重复构建 + 高级分布式缓存/执行(Bazel 的强项),需要权衡。
- 学习曲线:GN 的语义(declare_args、import、template 的写法、依赖模型)需要一点时间才能写出健壮的 BUILD.gn。文档和社区示例多数来源于 Chromium/Fuchsia,因此在独立项目中要做一些约定。
太干了,上一个例子
笔者在这里,就准备写上一个GN构建文本,下面的片段是笔者自己写一个基于Chrome库的一个小demo使用的构建片段文本。
executable("wallpaper_demo") {testonly = truesources = ["wallpaper_example/main.cc",]deps = ["//base","//ui/views","//ui/gfx","//ui/aura","//ui/wm",":views_examples_main_header",":views_examples_proc","//base/test:test_support","//build/win:default_exe_manifest",]# 设定 windows GUI 程序,不需要 console main# if (is_win) {# configs += [ "//build/config/win:windowed" ]# configs -= [ "//build/config/win:console" ]# }
}
下面我一个一个说到底是怎么回事
executable("wallpaper_demo") { … }
executable
是 GN 中的一种 target 类型,用来生成可执行程序(binary executable)。"wallpaper_demo"
是这个可执行程序的名字/label(在当前 BUILD.gn 文件所在目录下的 target 名称)。构建出来通常会是一个可执行文件名为wallpaper_demo
(在 Windows 上可能带.exe
,或者受到 toolchain / configs 的影响)。
testonly = true
- 这个字段表明这个 target 是 仅用于测试目的 的。
- 当
testonly = true
时,它告诉 GN:这个可执行不应该被非测试(production)目标作为依赖来使用;也就是说,它不应该成为生产构建的一部分。 - 用途包括:把测试相关的二进制或工具或者示例程序区分开来,以减少在正式发布/部署中的体积/安全/依赖风险。
- 注意:如果你写 template 或者复用逻辑时,最好“forward”(转发)
testonly
,以避免用户设了testonly = true
后其中某些内含 target 没有继承这个属性导致不一致或泄漏到 production。参见 Fuchsia 的 best practices。
sources = [ "wallpaper_example/main.cc", ]
- 列出本 target 所需要编译的源文件。这里仅有一个源文件
main.cc
。 - 路径通常是相对于这个 BUILD.gn 所在目录。
- 如果有多个源文件的话,用数组形式列出所有。也可以用 GN 的路径展开工具(如
glob()
)来批量指定。
deps = [ ... ]
deps
是 “依赖”(dependencies)的意思,对于可执行文件来说,表示这个可执行在链接时或编译时所需要依赖的其他 target(库、模块等)。- 列表里可以是以下几类:
- 库/模块目标:例如
"//base"
,"//ui/views"
等。这些是项目中其他的 GN targets。GN 在生成 Ninja 文件时,会把这些依赖正确地加入编译/链接步骤里。 - 本地同 BUILD.gn 的子目标:像
":views_examples_main_header"
、:views_examples_proc
表示同目录下或相对路径里定义的 target。前面的:
表示相对 label。 - 测试支持库:
"//base/test:test_support"
可能提供一些 helper/test 框架或 mock 等。 - 平台/系统特定依赖:比如
"//build/win:default_exe_manifest"
,可能是 Windows 下的 manifest 文件 target,用来配置可执行文件的特性(图标、依赖、权限等)。
- 库/模块目标:例如
- 注意区别
deps
vspublic_deps
vsdata_deps
:deps
是私有依赖,仅供链接/编译;依赖的 public header 或 public_configs 不会自动暴露给使用这个 target 的其他 target(除非那 target 用public_deps
)。public_deps
用来做接口暴露或公共依赖,使得使用者也继承配置/头文件等。data_deps
则是运行时需要的依赖,但不用于链接——比如插件、资源文件等。
可选/注释部分:针对 Windows GUI 程序的配置
# if (is_win) {
# configs += [ "//build/config/win:windowed" ]
# configs -= [ "//build/config/win:console" ]
# }
- 这段是一个条件语句(
if (is_win) { … }
),意思是如果当前编译环境是 Windows(is_win
为真),就做一些特定配置。 configs
是 GN 中一个列表变量,里面通常放一些配置块(config)label,用来定义编译器/链接器flags、预处理宏、平台特性等。configs += [ ... ]
表示给这个 target 添加某个 config。configs -= [ ... ]
表示移除某个 config。
- 具体这里是:将
//build/config/win:windowed
加入,以表示这是一个 GUI 窗口程序(windowed)而非控制台程序(console)。同时移除console
类型的 config。这样做的效果通常是 Windows 链接器不会为可执行程序创建一个控制台窗口,而是按 GUI 程序的方式运行(不自动弹控制台窗)。 - 注意:这部分被注释掉了(
#
开头),可能是正在调试或者还没决定是否启用。如果启用,要确保相关的 config label 定义正确。
条件判断与平台变量
- GN 提供一系列内置布尔变量来检查平台/目标类型,例如
is_win
,is_linux
,is_mac
,is_host
,is_official_build
等。你可以用这些变量在 BUILD.gn 中做if
判断来启用或禁用某些源码、依赖或 config。常见用法就在注释部分。 (Fuchsia) - 当项目支持多个平台时,很常见在顶层的 BUILD.gn 或者共享
.gni
文件中进行平台分支。
configs 与工具链(toolchains)
configs
是指一个或多个 “config 节点”(config target),这些节点定义了一些可复用的构建设置,比如编译选项(编译器 flags)、链接选项、预处理宏、include 目录等等。使用 config 可以避免在每个 target 中重复写这些设置。- 工具链(toolchain/current_toolchain)与
configs
常常配合使用,特别是在交叉编译或支持多平台编译时。 public_configs
是一个相关特性,允许一个 target 的依赖者自动继承某些 config。
Reference
官方文档是第一人,这里枚举一下我参考的部分:
GN Quick Start(如何
gn gen
、gn args
等)GN Reference(语言/函数/内置 target 详细手册)