Flutter Engine CI 实践指南:Pre-Submit 与 Post-Submit 双阶段测试如何守护引擎代码树
本篇基于 Flutter 仓库中 Engine pre-submits and post-submits 文档 及配套 CI 配置文件展开,讲解 Flutter 引擎仓库在合并前(pre-submit)与合并后(post-submit)两阶段运行测试与检查的完整机制:.ci.yaml 中 presubmit、runIf 等关键字段的作用、Clang Tidy 仅检查变更文件的边界与风险,以及如何通过 test: all 标签让 post-submit 检查提前介入。读完后,你将能够读懂 engine/src/flutter/.ci.yaml 中每个构建目标的调度语义,并在贡献引擎代码时合理选择测试策略。
双阶段测试总览:一切由 .ci.yaml 定义
Flutter 引擎仓库同时运行 pre-submit(合并前)与 post-submit(合并后)两套测试与检查,二者都定义在 .ci.yaml 中。该文件头部注释说明了它的职责:
Describes the targets run in continuous integration environment. Flutter infra uses this file to generate a checklist of tasks to be performed for every commit.
之所以强调"合并前尽量跑完所有测试",原文档给出了一条非常务实的理由:如果变更没有运行到相应的测试,引擎代码树就会变红,进而引发开发者被阻塞、排查投入、回滚(revert)与重新 roll-forward 的昂贵连锁反应。因此凡是在合并前能跑的测试,都应该尽量跑掉。
从 .ci.yaml 的实际内容看,整个调度配置分三层:
enabled_branches:顶层正则控制哪些分支启用 CI,当前包括master、release candidate 分支(flutter-\d+\.\d+-candidate\.\d+)以及 Fuchsia 分支(fuchsia_f\d+[a-z]*);platform_properties:为 linux / mac / windows 三类平台提供默认属性,例如 CIPD 依赖(open_jdkversion 21)、操作系统版本(Ubuntu、Mac-15.7、Windows-10)、核心数等;targets:真正的检查清单,每个 target 通过recipe字段绑定一个构建配方(如engine_v2/engine_v2、engine_v2/builder、engine_v2/cache)。
一个典型的 pre-submit 目标是 Linux linux_host_engine:
- name: Linux linux_host_engine
recipe: engine_v2/engine_v2
timeout: 120
properties:
add_recipes_cq: "true"
release_build: "true"
config_name: linux_host_engine
dependencies: >-
[
{"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"}
]
drone_dimensions:
- os=Linux
dimensions:
cores: "8"
这里可以看到几个值得注意的细节:add_recipes_cq: "true" 让该构建在持续队列(CQ)上也被添加运行;release_build: "true" 表明它产出 release 构建;goldctl 是用于黄金文件(golden)比对的依赖;dimensions: cores: "8" 旁边有一段注释说明——该维度用于避免"只派生子构建的编排器被分配到 32 核大机器上执行 release 构建",是防止算力错配的手段。
config_name 指向的构建细节,则写在 ci/builders/ 目录下对应的 JSON 中。以 ci/builders/linux_host_engine.json 为例,它定义了 ci/host_debug 与 ci/host_release 两个构建:
gn:GN 生成参数,如--target-dir ci/host_debug --runtime-mode debug --prebuilt-dart-sdk --no-lto --rbe --no-goma(host_release 则改为 release 模式);ninja.targets:要编译的 Ninja 目标,例如flutter/build/archives:dart_sdk_archive、flutter/build/archives:artifacts、flutter/impeller/toolkit/interop:sdk等;archives:需要上传到 GCS 的产物清单,包括dart-sdk-linux-x64.zip、flutter_patched_sdk.zip、artifacts.zip、font-subset.zip、impeller_sdk.zip、linux-x64-embedder.zip;generators.tasks:构建后执行的附加任务,如符号导出校验(flutter/testing/symbols/verify_exported.dart)、API 文档生成(flutter/tools/gen_docs.py)和 engine stamp 生成(flutter/tools/engine_tool/bin/et.dart)。
该文件顶部的 _comment 字段还声明了一个清晰的约定:release 构建定义文件里只放"产出 release 制品所必需的构建",测试类构建应放到其他 linux_* 定义文件中。
Pre-Submit:合并的硬性门槛
Pre-submit 是必须通过才能合并 PR 的检查。原文档中附有一张 PR 检查项截图(此处因图片为外部资源不再引用),其要点是:PR 页面列出的 checks 就是 .ci.yaml 中相应 target 的实例化。
除常规"每个 PR 都跑"的目标外,pre-submit 有两类例外需要特别注意:
例外一:带 runIf 的目标会按变更文件跳过
runIf: ... 是一个"用可预测性换速度"的功能:当 PR 没有修改特定文件(或文件集合)时,该 target 会被直接跳过。原文档明确将其标注为 powerful (but dangerous)。
当前 .ci.yaml 中实际配置了 runIf 的目标有两个。Linux linux_clang_tidy(L186-L202):
- name: Linux linux_clang_tidy
recipe: engine_v2/engine_v2
timeout: 120
properties:
config_name: linux_clang_tidy
runIf:
- DEPS
- engine/src/flutter/.ci.yaml
- engine/src/flutter/tools/clang_tidy/**
- engine/src/flutter/ci/builders/**
- engine/src/flutter/ci/clang_tidy.sh
- "engine/src/flutter/**.h"
- "engine/src/flutter/**.c"
- "engine/src/flutter/**.cc"
- "engine/src/flutter/**.fbs"
- "engine/src/flutter/**.frag"
- "engine/src/flutter/**.vert"
以及 Linux mac_clang_tidy(L456-L474),其 runIf 用一条合并的 glob 覆盖 DEPS、.ci.yaml 与 "engine/src/flutter/**.(h|c|cc|fbs|frag|vert|m|mm)"。
两者语义一致:只有当 PR 触碰了 C/C++/Objective-C 源码、着色器、FlatBuffers 定义、DEPS 或 Clang Tidy 自身工具链时,对应 lint 目标才会运行。这意味着如果你的改动只是 Dart 脚本或文档,Clang Tidy 目标会在 pre-submit 中被跳过——这是节省算力的直接收益。
例外二:Clang Tidy 只检查 PR 中变更的文件
这是比 runIf 更隐蔽的一条规则:Clang Tidy 只会运行在"当前 PR 中被修改过的文件"上。原文档给出的例子非常能说明问题:
// impeller/a.h
struct A {}
如果 impeller/a.h 被修改,而导入它的文件(如 impeller/foo/bar/baz.cc)不会在 pre-submit 中被 Clang Tidy 检查。由此可以推断出一个关键结论:修改头文件(或更新提供头文件的库的 DEPS)并不是"安全变更"——编译期或 lint 层面的破坏无法在 pre-submit 被发现。原文档引用了一次真实事故:某次 PR 通过了全部 pre-submit 检查,却因 Clang Tidy 的 post-submit 检查捕获到失败而被回滚。
从仓库源码可以印证这条检查链的实现:CI 侧的执行入口是 ci/clang_tidy.sh,它先确保 out/host_debug/compile_commands.json 存在(不存在则先跑 tools/gn 生成),然后调用 flutter/tools/clang_tidy/bin/main.dart,传入 --src-dir 与可选的 --clang-tidy 路径(在 arm64 Mac 上会使用 buildtools/mac-arm64/clang/bin/clang-tidy)。脚本还支持通过环境变量 FLUTTER_LINT_PRINT_FIX=1 开启 --fix --lint-all 自动修复模式,失败时打印 git diff 便于启用新 lint 规则。而 post-submit 的分片执行细节定义在 ci/builders/mac_clang_tidy.json:它先构建两个"仅供 lint 使用"的变体(ci/host_debug_clang_tidy、ci/ios_debug_sim_clang_tidy),再以 --shard-id=0..3 将 host 变体拆成 4 个分片并行 lint,iOS simulator 变体单独一个测试任务。可见"只查变更文件"是刻意的分片提速策略,而非疏漏。
对于头文件/DEPS 这类高风险变更,原文档的建议是:参考下一节的机制,把 post-submit 检查提前到 pre-submit 运行。更完整的 Clang Tidy 规则背景见同目录的 Engine Clang Tidy Linter 文档。
Post-Submit:合并后才运行、但同样能让树变红
部分 target 被显式配置为 presubmit: false,例如原文档中的示例:
- name: Mac mac_clang_tidy
recipe: engine_v2/engine_v2
presubmit: false
这类 target 在 PR 页面不会出现、也不会执行,但它们仍然会随合并后提交运行,并且可以把代码树弄红。
在当前 .ci.yaml 中,presubmit: false 共出现 4 处:
| 目标 | 配方 | 用途(从属性推断) |
|---|---|---|
Linux builder_cache |
engine_v2/cache |
缓存 builder、git 目录,附带 gclient 变量(下载 emsdk、android deps、JDK) |
Windows builder_cache |
engine_v2/cache |
同上(Windows 侧,不下载 emsdk) |
Mac builder_cache |
engine_v2/cache |
同上(另忽略 Xcode SDK/Library 缓存路径) |
Linux linux_benchmarks |
engine_v2/builder |
基准构建,timeout: 60 |
其中三个 builder_cache 目标体现了 post-submit 的另一类价值:它们不为 PR 提供"合并信号",而是维护构建缓存这类基础设施性任务——放到 post-submit 既不影响 PR 合入体验,又能在合并后持续预热缓存。linux_benchmarks 则属于"重要但不阻塞合并"的构建验证。
主动把 Post-Submit 提前:test: all 标签
Flutter 引擎有意选择了一种权衡:让 PR 更容易落地,代价是代码树会周期性地因 post-submit 检查捕获到开发者没有预料的(甚至完全不知道的)问题而变红。作为补偿手段,原文档提供了 test: all 标签(注意:该标签仅在 flutter/engine 仓库中可用):
- 给 PR 添加标签
test: all; - 推送一次 PR(或追加一个提交)——原文档以加粗强调:调度器不会识别"没有伴随新提交而被添加的标签",加标签后必须再推一次提交,否则不会生效;
- 生效后,PR 将运行全部检查,包括通常是 post-submit 的目标(例如
mac_clang_tidy分片)。原文档引用的一次 PR 运行了所有检查,包含常规 post-submit 目标,即为例证。
原文档同时给出容量警示:这会额外消耗 worker/算力配额,不建议在所有 PR 上滥用该标签。结合前文的两类风险,可以总结出一条实用决策:改动是普通的 .cc/.dart 实现文件时,常规 pre-submit 足够;一旦触碰头文件、DEPS、着色器或 lint 工具链本身,就应加上 test: all,把 Clang Tidy 的 post-submit 检查前移到合并前。
关键字段速查与结语
阅读 .ci.yaml 时,以下字段是理解调度行为的核心:
| 字段 | 作用 | 实例 |
|---|---|---|
recipe |
绑定 CI 配方 | engine_v2/engine_v2、engine_v2/builder、engine_v2/cache |
presubmit: false |
目标不参与 PR 阶段的检查 | 各平台 builder_cache、linux_benchmarks |
runIf |
按变更文件 glob 决定是否运行 | Linux linux_clang_tidy、Linux mac_clang_tidy |
enabled_branches |
限定目标运行的分支 | 多个 DDM/Fuchsia 目标仅在 master 运行 |
timeout |
目标超时(分钟) | 常规引擎构建 120,Mac 目标 240 |
bringup: true |
标记试验性/不稳定目标 | local_engine_builds、windows_unopt(附 TODO 注释说明 dashboard 不稳定) |
drone_dimensions / dimensions |
控制调度到何种机器 | os=Linux、cores: "8" 防止大核机器错配 |
整个机制的设计哲学是分层取舍:pre-submit 追求"快速且足以拦截大多数问题",通过 runIf 与 Clang Tidy 的变更文件限定来提速;post-submit 兜住剩余风险(缓存维护、全量 lint、基准构建);test: all 标签则给开发者一个"我认这笔算力开销,请帮我提前排雷"的显式开关。理解了这三层,再配合 ci/builders/ 下每个 config_name 对应的 JSON 定义(gn 参数、ninja 目标、产物清单),就能完整还原任何一个 Flutter 引擎 CI 目标从触发到产出的全过程。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00