硅基俊宁.
← 返回文章

三、构建、依赖与生成代码:源码如何成为当前浏览器

俊宁 · 日期未详
文章目录

从源码阅读入口、进程边界与验证条件理解构建、依赖与生成代码:源码如何成为当前浏览器。

本文讨论通用机制和设计判断;示意不对应任何具体产品的已交付状态。源码阅读坐标沿用原稿,本次整理不构成新的运行时验收。

Chromium 构建、依赖与生成代码封面

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

Chromium 源码如何经过依赖图成为可执行浏览器

一句话结论

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,却不共享同一份构建图。

这意味着以下两种说法都不可靠:

在写问题分析、提交 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 追踪。

源码验证路径

工程推演:为什么只改一行,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 修改为什么触发大范围重编”等源码搜索无法回答的问题。

面对一个源文件,可以按下面的顺序缩小范围:

  1. 在相邻 BUILD.gn 中找到它属于哪个 source_set、component、static_library 或 executable target;
  2. 用 gn desc out/Debug //path:target deps --tree 查看直接与传递依赖,区分 public dependency 和 implementation detail;
  3. 用 gn refs out/Debug //path:target 反查消费者,确认目标是否真的进入 chrome、某个 test 或 helper binary;
  4. 查看 defines、include_dirs、public_configs、all_dependent_configs 与平台条件,解释同一文件为什么在不同 output 中表现不同;
  5. 如果涉及 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 官方上游快照 定位。此入口保留原稿的上游阅读坐标,不包含任何产品定制代码,也不替代运行时验收。