首页
/ Flutter 仓库架构详解:从多仓库集成点到 monorepo 的 DEPS、gclient 与引擎内容哈希

Flutter 仓库架构详解:从多仓库集成点到 monorepo 的 DEPS、gclient 与引擎内容哈希

2026-09-06 09:07:20作者:虞亚竹Luna

本篇基于 Flutter 官方文档 Flutter's repository architecture 展开,系统讲解 Flutter “仓库边界即集成点”的架构设计原则、当前仓库中引擎源码与依赖清单的实际布局,以及支撑框架/引擎同仓协作的 DEPS 依赖锁定、gclient sync 同步机制和引擎二进制内容哈希方案。读完后,你将能够解释 Flutter 为何采用这种仓库划分方式、如何在当前 monorepo 中定位引擎代码与第三方依赖、并理解 bin/internal 中内容哈希脚本的工作机制。

一、架构演进:从“数百个仓库”到框架与引擎合并

Flutter 文档明确指出,Flutter 早期使用一种深度多仓库(deeply multi-repository)架构,围绕 flutter/flutter 这一核心仓库,涉及至少以下仓库(引自 docs/about/Flutter's-repository-architecture.md):

  • flutter/flutter(本文所在仓库)
  • flutter/engine(渲染与服务层引擎)
  • flutter/packages(官方插件与包)
  • dart-lang/sdk(Dart SDK)
  • llvm.googlesource.comskia.googlesource.comswiftshader.googlesource.com
  • android.googlesource.comfuchsia.googlesource.comboringssl.googlesource.com
  • chromium.googlesource.comflutter.googlesource.com(二者各自又镜像了大量仓库)

文档总结:全部算下来,Flutter 的开发涉及数百个仓库(hundreds of repositories)

核心原则:仓库边界代表集成点

多仓库架构的第一性原理是:仓库之间的边界,就是集成点(integration points)。文档给出的两个典型例子:

  1. Dart 与 Engine:Dart 被集成进 Flutter 引擎,但 Dart 还被用于其他场景,因此 Dart 独立成仓,与引擎分离;
  2. Engine 与工具链:Flutter 引擎是以预构建二进制的形式被 Flutter 工具(flutter CLI)集成的,因此二者分处不同仓库。

按这个原则切分仓库,换来的是一组文档列出的关键能力:

  • flutter/packages 可以在一个干净的集成点上,针对不同版本的 Flutter 做测试;
  • flutter/flutter 与 Engine 之间具备无歧义的集成关系,发布分支(release branch)上同样成立
  • flutter/flutter 仓库的代码完全由单一许可证覆盖——这正是它不像 flutter/engine 那样需要 license 脚本的原因;
  • 交付给开发者的 flutter/flutter 可以让用户在调试器中逐步执行代码,而不会因仓库中混杂大量无关部分而分心;
  • flutter/flutter 是新贡献者容易上手的“入门坡道(easy on-ramp)”,社区可以由此深入,例如进而参与 engine 开发;
  • flutter/engine 可以选择特定版本的依赖(例如 Skia),而不必把依赖代码合并进来;
  • flutter/engine 的二进制可以为 CI 测试和正式发布以相同方式构建

历史仓库的合并轨迹

文档还记录了仓库体系“由分到合”的另一面:一些历史上被拆开的仓库已经或计划被合并——

  • 各个插件与包仓库先合并为 flutter/pluginsflutter/packages 两个仓库,最终又进一步合并为单一的 flutter/packages 仓库。原因是它们共享几乎完全相同的 CI 测试与开发工具链,合并更合理;
  • flutter/buildrootflutter/engine 曾被计划合并(buildroot 当初独立出来是为了方便与 Fuchsia 集成)。

而当前仓库的实际状态表明,演进已经走到了**框架与引擎同仓(monorepo)**的一步:本仓库根目录下已包含 engine/ 目录(引擎源码位于 engine/src/flutter),并且根目录存在引擎依赖清单 DEPS

二、当前仓库的实体布局:引擎代码、DEPS 与 gclient 引导

2.1 目录层面:engine/ 与根级 DEPS

从当前仓库结构看(与 docs/engine/monorepo/history_strategy.md 中记录的迁移目标一致):

  • 引擎源码被放置在 engine/src/flutter 下(迁移时通过 git filter-repo --to-subdirectory-filter engine/src/flutter 将历史重写进该目录);
  • 唯一例外是 DEPS 文件:它保留在仓库根目录(迁移脚本 git filter-repo --path-rename engine/src/flutter/DEPS:DEPS 将其移回根目录);
  • 引导 gclient 的模板文件位于 engine/scripts/standard.gclientengine/scripts/rbe.gclient

2.2 gclient 引导流程

engine/scripts/standard.gclient 顶部注释说明了用法:“把此文件复制到你的 Flutter checkout 根目录以引导 gclient,或者在一个空目录中带着此文件直接运行 gclient sync”。其核心内容:

solutions = [
  {
    "custom_deps": {},
    "deps_file": "DEPS",
    "managed": False,
    "name": ".",
    "safesync_url": "",
    # If you are using SSH to connect to GitHub, change the URL to:
    # git@github.com:flutter/flutter.git
    "url": "https://github.com/flutter/flutter.git",
    # Uncomment the custom_vars section below if you plan to build the web engine.
    # "custom_vars": {
    #   "download_emsdk": True,
    # },
  },
]

即:.gclient 声明根仓库地址,并通过 "deps_file": "DEPS" 指向根级依赖清单;构建 Web 引擎(需要 Emscripten 工具链编译 CanvasKit)时,取消注释 download_emsdk 变量即可。官方引擎开发环境文档 docs/engine/contributing/Setting-up-the-Engine-development-environment.md 描述了同样流程:把 engine/scripts/*.gclient 复制为仓库根目录的 .gclient(Google 内部人员用 rbe.gclient 启用 RBE 加速),然后在根目录执行 gclient sync

三、DEPS 依赖清单:以“固定 revision”实现依赖版本选择

根目录 DEPS 文件头部注释说明:它“引用了 Flutter Engine 的依赖,被 checkout 根目录的 .gclient 文件所引用;要预览依赖变更,修改本文件后运行 gclient sync;新增依赖时需同步更新顶层 .gitignore 以列出依赖的目标目录”。

这个文件正是文档所说“engine 可以选择特定版本依赖(例如 Skia)”的具体落地方式——依赖不是 merge 进仓库,而是以 URL + revision 的形式精确锁定:

vars = {
  'android_git': 'https://android.googlesource.com',
  'chromium_git': 'https://chromium.googlesource.com',
  'dart_git': 'https://dart.googlesource.com',
  'flutter_git': 'https://flutter.googlesource.com',
  'skia_git': 'https://skia.googlesource.com',
  'llvm_git': 'https://llvm.googlesource.com',
  ...
  'skia_revision': 'b6b00df360e5bc0366e747be84bb530ea866a7e6',
  ...
  'dart_revision': '5501d02b583d1717b800ada2ac4967e85ead15d8',
}

deps 段将每个目标目录钉在某个 revision 上,例如(摘取自 DEPS):

deps = {
  # Dart SDK 源码,checkout 到引擎的 third_party 目录
  'engine/src/flutter/third_party/dart':
   Var('dart_git') + '/sdk.git' + '@' + Var('dart_revision'),

  # Skia 图形库,checkout 到 third_party/skia
  'engine/src/flutter/third_party/skia':
   Var('skia_git') + '/skia.git' + '@' +  Var('skia_revision'),

  # 依赖的依赖也逐一固定版本(binaryen、devtools、pub 等)
  'engine/src/flutter/third_party/dart/third_party/binaryen/src':
   Var('chromium_git') + '/external/github.com/WebAssembly/binaryen.git' + '@' + Var('dart_binaryen_rev'),

  # prebuilt 工具链:各平台 Clang(版本统一由 clang_version 控制)
  'engine/src/flutter/buildtools/mac-x64/clang': {
    'packages': [ { 'package': 'fuchsia/third_party/clang/mac-amd64',
                   'version': Var('clang_version') } ],
    'condition': 'host_os == "mac"',
    'dep_type': 'cipd',
  },
}

DEPS 中还体现了几个工程细节,均可作为“仓库边界=集成点”原则的佐证:

  • allowed_hosts 白名单:只允许 boringssl/chromium/dart/flutter/llvm/skia.googlesource.comchrome-infra-packages.appspot.com 作为依赖来源——这几乎与架构文档列举的外部仓库清单一一对应;
  • 条件化 checkout:通过 download_android_depsdownload_fuchsia_depsdownload_windows_depsdownload_linux_deps 等变量,按宿主平台只拉取相关依赖(例如 download_emsdk 默认 False,避免不为 Web 构建的 checkout 白白下载 Emscripten 工具链);
  • upstream_* 映射:为漏洞扫描(common ancestor 判定)登记每个第三方库的上游 URL,例如 "upstream_skia": "https://skia.googlesource.com/skia.git"
  • hooksgclient sync 完成后执行一系列钩子,如生成 Dart SDK 的 .dart_tool/package_config.jsongenerate_package_config.py)、生成 sdk/version、Windows 工具链更新、Linux sysroot 安装、pub get --offline、Fuchsia 构建规则生成等;
  • 自动化滚版:文件注释提示,更新 Dart revision 时必须同步更新 Dart 自身 DEPS 中的依赖,可用 //tools/dart/create_updated_flutter_deps.py 生成新版本号列表——对应文档所说的“autoroller”式的依赖自动更新实践。

四、引擎二进制的内容哈希:monorepo 下“engine.version”的替代方案

多仓库时代,框架通过仓库内一份 bin/internal/engine.version 文件(内容是产生线上引擎二进制的 Git commit hash)来确定要下载哪个预构建引擎。文档 docs/engine/monorepo/engine_binary_hashing.md 指出,仓库合并后这套做法遇到三个问题:

  1. 每次引擎变更都要人工更新该文件,会造成频繁的 merge conflict;
  2. HEAD 不断变化,无法预先预测该文件的 hash 值;
  3. Git merge queue 会在引擎变更合入 main 之前就为其产出二进制。

因此需要一个对“产生引擎二进制的内容”做哈希的机制。该文档给出的推导过程:

  1. 基于提交内容而非工作区:用 git ls-tree -r HEAD 对 index/树对象操作,得到一致快照,支持 A/B 测试(本地未提交改动不影响基线);
  2. 哈希作用域限定到引擎:只纳入 engine/ 目录与根级 DEPSDEPS 跟踪 gclient sync 管理的第三方依赖),命令 git ls-tree -r HEAD engine DEPS 恰好覆盖所有相关文件并排除 third_party 中不参与构建的内容;
  3. 跨平台一致性:用 git hash-object --stdin 得到稳定 hash;
  4. 支持 A/B 测试:在开发分支上,用分支点的 merge-base 作为基线,使哈希反映分支时的引擎状态:
# 推荐公式
git ls-tree -r $(git merge-base HEAD master) engine DEPS | git hash-object --stdin

文档同时讨论了取舍:把路径、权限纳入哈希意味着重命名/移动文件也会触发引擎重建(初期可接受),并预留了仅哈希 blob 内容(--object-only)的未来细化方向。

4.1 当前仓库中的实际实现

上述“推荐公式”已在仓库中落地为 bin/internal 下的一对脚本 content_aware_hash.shcontent_aware_hash.ps1(注释明确要求两者逻辑保持一致以覆盖所有平台)。以 Bash 版为例(见 bin/internal/content_aware_hash.sh):

# Cannot use '*' for files in this command
# DEPS: tracks third party dependencies related to building the engine
# engine: all the code in the engine folder
# bin/internal/release-candidate-branch.version: release marker
TRACKEDFILES=(DEPS engine bin/internal/release-candidate-branch.version)
BASEREF="HEAD"

与文档公式相比,实现有两点演进,值得注意:

  • 跟踪文件集合从“engine + DEPS”扩展为三项:DEPSengine,以及 bin/internal/release-candidate-branch.version(标记当前是否为 release-candidate 分支,作为“release marker”参与哈希);
  • 基线选择逻辑:默认基于 HEAD;但普通开发分支上会回退到与 upstream(若不存在则 origin)的 master/mainmerge-base——避免每改一行引擎代码就重建整个世界。例外情况(直接用 HEAD)包括:main/master/stable/beta 等发布分支、GitHub merge queue 的临时分支(gh-readonly-queue/master/pr-*)、release-candidate 分支(flutter-*-candidate.*)、shallow clone,以及无当前分支的 CI 场景(LUCI_CONTEXT 存在时)。

最后一步哈希计算为:

git ls-tree "$BASEREF" -- "${TRACKEDFILES[@]}" | git hash-object --stdin

并针对 Apple Git 的 multi-pack-index 兼容性问题加了 -c core.multiPackIndex=false 的规避(见 bin/internal/content_aware_hash.sh)。

五、monorepo 迁移的工程细节:历史裁剪与引擎目录重写

docs/engine/monorepo/history_strategy.md 记录了 flutter/engine 并入 flutter/flutter 时的完整操作规程,是理解当前仓库形态(为什么引擎在 engine/src/flutter、为什么 DEPS 在根目录)的直接依据。要点如下:

  • 动机:引擎仓库 .git 约 780MB 历史,其中包含不再使用的二进制、近十年前 checkout 后又移除的第三方库、被搬走的示例;
  • Step 1 安全准备:全新 clone 引擎仓库并 git remote remove origin,避免改写历史时影响远端;可选用 git filter-repo --analyze --force 分析大文件分布(分析结论是一张约 50 项的“路径 × 大小 × 删除日期”表,如 ci/licenses_golden/licenses_third_party 约 112MB、third_party/android_platform 约 27MB);
  • Step 2 裁剪历史:用 git filter-repo --force --invert-paths 配合大量 --path/--path-glob 从全部历史中移除废弃目录与二进制(*.jar*.dll*.ttc 字体等),随后 git reflog expire && git gc --prune=now --aggressive.git 从约 780MB 降到约 110MB;
  • Step 3 目录重写
# Move files to engine/src/flutter, update tags so they don't collide,
# and move DEPS back to root.
git filter-repo  --to-subdirectory-filter engine/src/flutter --tag-rename '':'engine-' --force
git filter-repo --path-rename engine/src/flutter/DEPS:DEPS

这就是当前仓库“引擎位于 engine/src/flutterDEPS 位于根目录”布局的由来;

  • Step 4 重写 PR 链接:在合并进 flutter/flutter 之前,用 --message-callback 仅改写 commit message 首行的 PR 编号链接,避免与框架历史冲突;
  • 最终合并:clone flutter/flutter,添加引擎历史为 remote,git merge --no-commit --allow-unrelated-histories engine-upstream/main 提交后再次 gc,.git 约 234MB。

六、小结:仓库结构如何服务于工程目标

回到架构文档的主线,可以把当前仓库形态与原始设计原则逐一对应:

架构原则(文档) 当前仓库中的体现
仓库边界 = 集成点 引擎以内容哈希(content_aware_hash.*)为集成点交付给框架;Dart/Skia 等以 DEPS 中的 revision 为集成点
engine 可选择特定版本依赖 DEPSskia_revisiondart_revision 等精确钉版,配合 gclient sync
单一许可证覆盖框架仓库 引擎第三方依赖全部落在 engine/src/flutter/third_party 之下,由 DEPS 管理并计入 .gitignore
贡献者入门坡道 框架层(packages/flutterpackages/flutter_toolsdev/ 等)与引擎(engine/src/flutter)物理分层,新人可从框架层切入
CI 与发布同构构建 DEPS 中同一套依赖/工具链定义供 CI 与发布共用;RBE(rbe.gclientuse_rbe)进一步统一构建环境

对阅读者而言,理解这份架构文档的关键落点有三个:第一,Flutter 的仓库划分不是随意的,而是以“集成点”为边界切分的,多仓库与 monorepo 都是该原则的不同阶段产物;第二,当前仓库中 engine/src/flutter + 根级 DEPS + gclient sync 的组合,正是“引擎代码与依赖版本选择”的实体化表达;第三bin/internal/content_aware_hash.{sh,ps1} 的内容哈希机制,是仓库合并后替代 engine.version 文件、维持框架/引擎无歧义集成的基础设施。如需继续深入,可参阅 Setting-up-the-Engine-development-environment(gclient 引导细节)、Compiling-the-enginegclient sync -D 等构建命令)以及 Engine-specific-Service-Protocol-extensions 等文档。

登录后查看全文
热门项目推荐
相关项目推荐