三、构建、依赖与生成代码:源码如何成为当前浏览器
俊宁 · 日期未详文章目录
从源码阅读入口、进程边界与验证条件理解构建、依赖与生成代码:源码如何成为当前浏览器。
本文讨论通用机制和设计判断;示意不对应任何具体产品的已交付状态。源码阅读坐标沿用原稿,本次整理不构成新的运行时验收。

阅读坐标沿用原稿:Chromium
153.0.8010.37,上游 commitb75a5a95ea1a1b55bdbfd6d9f42d47be7507fb8b。本次同步未重新进行源码与目标平台运行时验收。

一句话结论
Chromium 的可复现构建由 checkout revision、DEPS、GN args、toolchain、target 和 generated outputs 共同决定;“源码在”“某次编过”或“目录没改”都不足以证明当前 binary 包含并运行了某项能力。
读 Chromium 时有一个非常实用的原则:源文件是候选实现,GN target 才是构建单元,最终 binary 与运行证据才决定它是否真的参与当前行为。
这条原则解释了许多看似矛盾的现象:同一个类在源码中存在却没有链接进当前程序;某个 feature 的实现被编进 binary 却默认关闭;Windows 与 macOS 的同名 target 依赖不同源文件;一段看起来没改的 C++,却因 generated header、GN args 或 action 输出变化而重新编译。
从 checkout 到 binary 的五步
DEPS / gclient 同步
-> 源码、第三方依赖与工具进入 checkout
-> GN 根据 args 生成 Ninja build graph
-> Ninja 执行编译、链接、代码生成与资源处理
-> 目标 binary / pak / helper / test 进入 out 目录
-> 启动时再受 feature、policy、实验和运行环境影响
前四步回答“能不能做出制品”,最后一步才回答“制品运行时会不会走到某条功能路径”。把两类问题分开,是理解 Chromium 构建问题和功能问题的前提。
DEPS:checkout 的依赖边界
DEPS 描述 Chromium checkout 在某一 revision 上需要哪些外部仓库、CIPD 包或条件依赖。gclient sync 根据它把依赖同步到工作区;因此,依赖不完整、条件不满足、hook 未执行或工具包版本不一致时,后面的 GN/Ninja 错误往往只是表象。
这里要区分三个对象:
| 对象 | 它解决的问题 | 不应从它推导出的结论 |
|---|---|---|
DEPS | 当前源码 revision 依赖哪些输入 | 某个 target 一定会使用全部依赖 |
.gclient | 这个工作区如何被 gclient 管理 | Chromium 产品或编译行为本身 |
gclient sync 的结果 | 某次本地 checkout 是否已按规则同步 | 当前浏览器已经成功编译或可运行 |
GN:把目录里的文件变成可推导的图
GN 读取各目录的 BUILD.gn,在指定 args 和 toolchain 下生成 Ninja 文件。一个 GN label,例如 //chrome:chrome,比一个文件路径更接近“构建时真正的模块”:它有 sources、deps、configs、条件、可见性和输出类型。
常见 target 类型的作用可以先这样理解:
| 类型 | 典型作用 | 阅读时要追问的问题 |
|---|---|---|
source_set | 编译一组源文件,供更上层 target 汇总 | 谁最终把它链接进 binary? |
static_library | 形成静态库输入 | 哪些链接目标消费它,是否受平台条件影响? |
shared_library / component | 形成动态组件或 component build 输入 | 当前 is_component_build 下边界是否变化? |
executable | 产生程序、helper 或测试可执行文件 | 启动的是否就是这个 target 的输出? |
group | 聚合多个依赖,通常本身不产生代码 | 它代表产品集合还是只是构建便利? |
action / action_foreach | 执行代码生成、资源处理或其他构建动作 | 输出文件是什么,谁声明依赖它? |
不能仅靠类型判断运行关系。例如 source_set 可能跨很多产品目标复用,group 也可能是开发者构建入口。真正需要看的仍是 target 的 deps、public_deps、条件表达式和最终反向依赖。
依赖字段不是同义词
deps 表示当前 target 的构建依赖;public_deps 还会把依赖要求传播给消费当前 target 的下游;data 通常表达运行或测试时需要复制的文件,而非 C++ 链接关系;configs 与 public_configs 会影响编译选项和宏的传播。它们混用时,代码可能“能编过”却在另一个 target、平台或 component build 下失效。
因此,阅读 BUILD.gn 时至少记录:当前 target 的输出类型、直接依赖、向下游传播的依赖与 config、平台 condition、生成输入输出。不要只从 sources = [...] 取走几个文件名。
out/<dir>:一份构建配置就是一个世界
GN args 和生成结果通常位于 out/<dir>。不同 output directory 可以对应不同 target OS/CPU、debug/release、sanitizer、component build 或 feature 配置。它们共享源码 checkout,却不共享同一份构建图。
这意味着以下两种说法都不可靠:
- “我在
out/Default编过,所以 Windows/Release 也一定可以。” - “这个文件在当前 checkout 里,所以它一定会进入正在运行的浏览器。”
在写问题分析、提交 patch 或做复现时,应把 git revision、out/<dir>、GN args、目标 label 和平台一起记录。缺少其中任意一项,别人通常无法重建同一个构建世界。
代码生成是依赖图的一部分
Chromium 大量使用 action 生成头文件、协议绑定、资源、JavaScript、IDL 产物或打包输入。生成结果往往不在源目录,而在 output tree;因此搜索一个符号时,既要查源码,也要确认它是否由生成规则产生。
一个常见排障误区是直接修改生成文件。它可能在本机暂时有效,却会在下一次 GN/Ninja 执行时被覆盖,也无法让其他 checkout 重现。正确顺序是:找到生成规则和源输入,确认 action 的 declared outputs,修改源输入后让 Ninja 重跑依赖边。
用 GN 回答源码阅读问题
下面几类命令适合把“我猜它被编进去了”变成可检查的问题;label 与 output directory 应替换为当前实际值:
# 查看一个 target 的直接或树状依赖。
gn desc out/Default //chrome:chrome deps --tree
# 查看某个 target 的 sources、defines、configs 等声明。
gn desc out/Default //chrome:chrome
# 反查一个文件或 target 被哪些目标引用。
gn refs out/Default path/to/file.cc --all
# 让 Ninja 构建某个明确 target;实际命令可用 autoninja 包装。
autoninja -C out/Default chrome
这些命令能说明构建图,不能单独说明运行时路径。若目标是“为什么某个 feature 没触发”,还需要继续检查 feature flag、policy、启动参数、Profile 状态和 trace;若目标是“为什么链接失败”,才应优先沿 target、toolchain 和 generated output 追踪。
源码验证路径
- 在基线 checkout 中记录
//chrome:chrome及 Browser、Renderer、GPU 相关 target 的最小依赖树。 - 选一个 Mojo 或资源生成规则,展示源输入、GN action、输出文件和消费者之间的完整关系。
- 以一次真实编译为例,保留
gn args --list、Ninja explanation 或编译数据库证据,而不是只截取“build succeeded”。
工程推演:为什么只改一行,Ninja 却做了很多工作
“只改一行为什么重新编了几千个 target”是最适合验证构建模型的问题。先不要把它归因于 Ninja 不够聪明,而要按依赖图回溯:改动是否位于广泛消费的 public header,是否改变了一个向下游传播的 config/define,是否触发 IDL、Mojo、resource 或 grit action,是否切换 GN args 导致整个 output directory 的图发生变化,以及当前是否真的在复用同一 out/<dir>。
| 现象 | 优先检查 | 常见错误动作 |
|---|---|---|
| 大量 C++ 重新编译 | header 反向依赖、public config、PCH/Unity 边界 | 直接清空 out/,丢失可解释证据 |
| generated file 每次变化 | generator 输入、时间戳、排序和 declared outputs | 手改 gen/ 下产物 |
| 本机可编、CI 缺符号 | target deps、visibility、平台 condition | 追加全局 include path 掩盖依赖 |
| macOS 正常、Windows 失败 | toolchain、SDK、source condition、link boundary | 认为同名 GN label 必然等价 |
| feature 编入但不生效 | runtime FeatureList、policy、Profile 与 command line | 继续修改 BUILD.gn |
深入一层:把构建图当作可查询的架构
很多人只在构建失败时看 GN,成功后就回到文件搜索。实际上,GN graph 是理解 Chromium 静态架构最可靠的入口之一。它能回答“谁消费这个实现”“为什么只有某个平台包含它”“一次 header 修改为什么触发大范围重编”等源码搜索无法回答的问题。
面对一个源文件,可以按下面的顺序缩小范围:
- 在相邻
BUILD.gn中找到它属于哪个source_set、component、static_library或 executable target; - 用
gn desc out/Debug //path:target deps --tree查看直接与传递依赖,区分 public dependency 和 implementation detail; - 用
gn refs out/Debug //path:target反查消费者,确认目标是否真的进入chrome、某个 test 或 helper binary; - 查看
defines、include_dirs、public_configs、all_dependent_configs与平台条件,解释同一文件为什么在不同 output 中表现不同; - 如果涉及 generated header、Mojom、GRIT、protobuf 或 snapshot,继续追 action target 的 inputs、outputs 和调用脚本。
这套查询尤其适合判断代码放置是否合理。如果一个底层 target 为了调用产品逻辑反向依赖 //chrome,GN 往往会先暴露分层问题;如果只能通过扩大 visibility 或引入循环依赖完成修改,通常说明 owner 或接口层还没有找对。
增量构建为什么会“看起来不增量”
Ninja 根据输入、输出、命令行和依赖图判断任务是否需要重跑。一次很小的修改可能触发大量工作,常见原因不止“缓存失效”:
| 变化 | 为什么影响面大 | 更合适的处理 |
|---|---|---|
| 公共 header 或 template | 大量 translation units 直接展开或编译它 | 先用反向依赖确认范围,避免为试错频繁改公共头 |
| GN config / args | 编译命令本身发生变化 | 使用独立 output,不在同一目录反复切配置 |
| Mojom/GRIT/protobuf 输入 | generated outputs 与下游 binding/resource 同时变化 | 先构建对应 action/小 target,检查生成差异 |
| toolchain、DEPS 或 CIPD | 编译器、SDK、第三方输入改变 | 重新 runhooks/sync,并把输入变化写进交接记录 |
| restat 不成立或输出时间异常 | Ninja 无法确认 action 没有实质变化 | 检查脚本是否稳定生成、系统时钟与输出声明 |
“删掉整个 out/ 重来”能绕开部分状态,却同时删除了最有价值的证据。更稳妥的顺序是先读失败 command、.ninja_log 或 Siso trace,确认变更从哪个 edge 扩散;只清理确实损坏的 generated output 或 target,最后才重建完整 output。
构建成功后还要记录 binary 身份
这份身份记录解决的是“你运行的到底是哪一个浏览器”。如果本机同时存在 out/Debug、out/Release、App bundle、安装版和系统 Chrome,只截图一个 About 页面不足以证明命中了刚改的代码。可以在受控开发构建中加入 build id/log,或用 executable 路径、binary hash、启动命令与 symbol 对应关系交叉确认。
失败推演:generated code 改了,为什么手改文件无效
当工程师在 out/Debug/gen/ 中修补 generated header 或资源后构建成功,下一次 GN/Ninja 运行往往会覆盖它。根因不在“构建系统不尊重修改”,而是 generated output 的 owner 是上游 schema/template/action。正确路径是找到生成输入和 action target,修改 source-of-truth,再验证生成差异和消费者。
同样,某个 feature 的声明出现在生成文件,不代表改 generated constant 就能完成产品配置。应先追它来自 GN arg、feature declaration、resource definition 还是 Distribution stage;否则本地现象无法被 patch 重放,也无法进入干净 checkout。
工程结论
把源码、target、产物和运行状态分开记录后,构建系统就从黑盒变成了可查询的图。需要长期维护的不是一组“清缓存后再试”的命令,而是一条从固定输入到最终 binary 可被另一台机器重建的证据链。
继续阅读
- 二、Chromium 开发环境与调试实战:从源码拉取到多进程断点: 相邻章节的前置机制。
上游源码入口
文中的相对源码路径可从 Chromium 官方上游快照 定位。此入口保留原稿的上游阅读坐标,不包含任何产品定制代码,也不替代运行时验收。