首页
/ Flutter Engine CI 实践指南:Pre-Submit 与 Post-Submit 双阶段测试如何守护引擎代码树

Flutter Engine CI 实践指南:Pre-Submit 与 Post-Submit 双阶段测试如何守护引擎代码树

2026-09-06 16:05:58作者:翟萌耘Ralph

本篇基于 Flutter 仓库中 Engine pre-submits and post-submits 文档 及配套 CI 配置文件展开,讲解 Flutter 引擎仓库在合并前(pre-submit)与合并后(post-submit)两阶段运行测试与检查的完整机制:.ci.yamlpresubmitrunIf 等关键字段的作用、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_jdk version 21)、操作系统版本(Ubuntu、Mac-15.7、Windows-10)、核心数等;
  • targets:真正的检查清单,每个 target 通过 recipe 字段绑定一个构建配方(如 engine_v2/engine_v2engine_v2/builderengine_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_debugci/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_archiveflutter/build/archives:artifactsflutter/impeller/toolkit/interop:sdk 等;
  • archives:需要上传到 GCS 的产物清单,包括 dart-sdk-linux-x64.zipflutter_patched_sdk.zipartifacts.zipfont-subset.zipimpeller_sdk.ziplinux-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_tidyL186-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_tidyL456-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_tidyci/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 缓存 buildergit 目录,附带 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 仓库中可用):

  1. 给 PR 添加标签 test: all
  2. 推送一次 PR(或追加一个提交)——原文档以加粗强调:调度器不会识别"没有伴随新提交而被添加的标签",加标签后必须再推一次提交,否则不会生效;
  3. 生效后,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_v2engine_v2/builderengine_v2/cache
presubmit: false 目标不参与 PR 阶段的检查 各平台 builder_cachelinux_benchmarks
runIf 按变更文件 glob 决定是否运行 Linux linux_clang_tidyLinux mac_clang_tidy
enabled_branches 限定目标运行的分支 多个 DDM/Fuchsia 目标仅在 master 运行
timeout 目标超时(分钟) 常规引擎构建 120,Mac 目标 240
bringup: true 标记试验性/不稳定目标 local_engine_buildswindows_unopt(附 TODO 注释说明 dashboard 不稳定)
drone_dimensions / dimensions 控制调度到何种机器 os=Linuxcores: "8" 防止大核机器错配

整个机制的设计哲学是分层取舍:pre-submit 追求"快速且足以拦截大多数问题",通过 runIf 与 Clang Tidy 的变更文件限定来提速;post-submit 兜住剩余风险(缓存维护、全量 lint、基准构建);test: all 标签则给开发者一个"我认这笔算力开销,请帮我提前排雷"的显式开关。理解了这三层,再配合 ci/builders/ 下每个 config_name 对应的 JSON 定义(gn 参数、ninja 目标、产物清单),就能完整还原任何一个 Flutter 引擎 CI 目标从触发到产出的全过程。

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