Flutter 仓库架构详解:从多仓库集成点到 monorepo 的 DEPS、gclient 与引擎内容哈希
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
本篇基于 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.com、skia.googlesource.com、swiftshader.googlesource.comandroid.googlesource.com、fuchsia.googlesource.com、boringssl.googlesource.comchromium.googlesource.com与flutter.googlesource.com(二者各自又镜像了大量仓库)
文档总结:全部算下来,Flutter 的开发涉及数百个仓库(hundreds of repositories)。
核心原则:仓库边界代表集成点
多仓库架构的第一性原理是:仓库之间的边界,就是集成点(integration points)。文档给出的两个典型例子:
- Dart 与 Engine:Dart 被集成进 Flutter 引擎,但 Dart 还被用于其他场景,因此 Dart 独立成仓,与引擎分离;
- Engine 与工具链:Flutter 引擎是以预构建二进制的形式被 Flutter 工具(
flutterCLI)集成的,因此二者分处不同仓库。
按这个原则切分仓库,换来的是一组文档列出的关键能力:
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/plugins与flutter/packages两个仓库,最终又进一步合并为单一的flutter/packages仓库。原因是它们共享几乎完全相同的 CI 测试与开发工具链,合并更合理; flutter/buildroot与flutter/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.gclient 与 engine/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.com及chrome-infra-packages.appspot.com作为依赖来源——这几乎与架构文档列举的外部仓库清单一一对应;- 条件化 checkout:通过
download_android_deps、download_fuchsia_deps、download_windows_deps、download_linux_deps等变量,按宿主平台只拉取相关依赖(例如download_emsdk默认False,避免不为 Web 构建的 checkout 白白下载 Emscripten 工具链); upstream_*映射:为漏洞扫描(common ancestor 判定)登记每个第三方库的上游 URL,例如"upstream_skia": "https://skia.googlesource.com/skia.git";- hooks:
gclient sync完成后执行一系列钩子,如生成 Dart SDK 的.dart_tool/package_config.json(generate_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 指出,仓库合并后这套做法遇到三个问题:
- 每次引擎变更都要人工更新该文件,会造成频繁的 merge conflict;
- HEAD 不断变化,无法预先预测该文件的 hash 值;
- Git merge queue 会在引擎变更合入 main 之前就为其产出二进制。
因此需要一个对“产生引擎二进制的内容”做哈希的机制。该文档给出的推导过程:
- 基于提交内容而非工作区:用
git ls-tree -r HEAD对 index/树对象操作,得到一致快照,支持 A/B 测试(本地未提交改动不影响基线); - 哈希作用域限定到引擎:只纳入
engine/目录与根级DEPS(DEPS跟踪gclient sync管理的第三方依赖),命令git ls-tree -r HEAD engine DEPS恰好覆盖所有相关文件并排除third_party中不参与构建的内容; - 跨平台一致性:用
git hash-object --stdin得到稳定 hash; - 支持 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.sh 与 content_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”扩展为三项:DEPS、engine,以及bin/internal/release-candidate-branch.version(标记当前是否为 release-candidate 分支,作为“release marker”参与哈希); - 基线选择逻辑:默认基于
HEAD;但普通开发分支上会回退到与upstream(若不存在则origin)的master/main的merge-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/flutter、DEPS位于根目录”布局的由来;
- 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 可选择特定版本依赖 | DEPS 中skia_revision、dart_revision等精确钉版,配合gclient sync |
| 单一许可证覆盖框架仓库 | 引擎第三方依赖全部落在engine/src/flutter/third_party之下,由 DEPS 管理并计入.gitignore |
| 贡献者入门坡道 | 框架层(packages/flutter、packages/flutter_tools、dev/等)与引擎(engine/src/flutter)物理分层,新人可从框架层切入 |
| CI 与发布同构构建 | DEPS 中同一套依赖/工具链定义供 CI 与发布共用;RBE(rbe.gclient、use_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-engine(gclient sync -D等构建命令)以及 Engine-specific-Service-Protocol-extensions 等文档。
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考