Flutter 仓库 iOS 开发签名证书年度续期:mobileprovision CIPD 包的构建、打标签与 CI 版本迁移全流程
mobileprovision 是 Flutter 框架仓库中一套面向 iOS 真机测试基础设施的运维级流程:每年 iOS development signing certificate 到期后,需要把新签发的 Provisioning Profile 打成 flutter_internal/mac/mobileprovision 系列 CIPD 包,并完成 ref 指向与年度 tag 迁移。本文以仓库内 mobileprovision/README.md 为主线,结合目录中的两份 CIPD 包定义文件,完整还原这一年度续期操作的 14 个步骤,并逐条解读 cipd create / set-ref / set-tag 命令的语义与包内配置字段,帮助读者掌握这套面向 devicelab 等 CI bot 环境的签名凭据分发与版本管理方案。
一、背景:为什么 iOS 签名证书每年都需要一次“打补丁式”续期
Flutter 仓库使用 devicelab 与基于 Chrome 的测试框架(chrome bots)在物理 iOS 测试设备上运行真实应用测试。要让这些应用被设备信任并安装,需要用团队持有的 iOS development signing certificate 为应用签名,并配套一个 Provisioning Profile(描述文件)。
这里的核心约束是证书会过期,且续期有过渡窗口:
- iOS development signing certificate 大约每年到期一次,到期后必须由证书管理员签发新的证书;
- 新证书签发后,物理测试设备上新旧两代证书都需要保持可用,因为测试机上可能还残留着用旧证书签名的历史安装包;
- 因此需要生成一个新的 Provisioning Profile,使同时由旧证书和新证书签名的应用都能在这些真机上运行。
需要说明的是:完整续期还包含“签发新证书”等前置与后续环节,这部分流程记录在 Google 内部文档中;本 README(engine/src/flutter/tools/cipd/mobileprovision/README.md)专门负责其中“把新 Profile 制作成可分发、可追溯的 CIPD 包”这一个子步骤,并与 CI 配置中的
apple_signing依赖版本联动。
二、包结构:mobileprovision 目录里有什么
在仓库的 tools/cipd 下,CIPD 相关工具与包定义按主题分目录存放(同级的还有 android_embedding_bundle/、malioc/ 等)。其中 mobileprovision/ 目录结构如下:
engine/src/flutter/tools/cipd/mobileprovision/
├── README.md # 年度更新操作手册(本文主题)
├── mac-arm64.yaml # Apple Silicon Mac 测试机对应的包定义
└── mac-amd64.yaml # Intel Mac 测试机对应的包定义
为什么拆成两个包? 因为 devicelab / chrome bots 中的 macOS 测试宿主机既有 Apple Silicon(arm64)也有 Intel(amd64)两种架构,CI 在不同架构机器上以不同包名拉取,CIPD 的架构隔离能让两端互不干扰地独立发布与回滚。
两份 YAML 内容几乎一致,唯一的区别是 package 字段的后缀。以 mac-arm64.yaml 为例,完整定义如下:
# Comments are allowed.
# The package name is required. Third-party chromium dependencies should
# unsurprisingly all be prefixed with chromium/third_party/.
package: flutter_internal/mac/mobileprovision/mac-arm64
# The description is optional and is solely for the reader's benefit. It
# isn't used in creating the CIPD package.
description: iOS provisioning provide for "match Development" signing certificate
# The root is optional and, if unspecified, defaults to ".". It specifies the
# root directory of the files and directories specified below in "data".
#
# You won't typically need to specify this explicitly.
root: "."
# The install mode is optional. If provided, it specifies how CIPD should
# install a package: "copy", which will copy the contents of the package
# to the installation directory; and "symlink", which will create symlinks
# to the contents of the package in the CIPD root inside the installation
# directory.
#
# You won't typically need to specify this explicitly.
install_mode: "copy"
# The data is required and described what should be included in the CIPD
# package.
data:
- file: development.mobileprovision
YAML 字段逐一拆解
| 字段 | 必填 | 本仓库取值 | 语义说明 |
|---|---|---|---|
package |
是 | flutter_internal/mac/mobileprovision/mac-arm64(或 .../mac-amd64) |
CIPD 包全局唯一名。命名参考了 Chromium 系依赖统一使用 flutter_internal/ 前缀的惯例;这里的 mobileprovision 即“iOS 描述文件” |
description |
否 | iOS provisioning provide for "match Development" signing certificate |
仅给人看的说明文字,不参与建包。点明该 profile 是配合 match Development 方式管理的 development 签名证书使用 |
root |
否 | "." |
指定 data 中相对路径的基准目录,缺省就是当前目录,此处显式写 "." 通常无需改动 |
install_mode |
否 | "copy" |
安装方式二选一:copy 把包内容复制到安装目录;symlink 则在安装目录内的 CIPD root 下创建符号链接。这里用 copy 保证 bot 环境下拿到的是真实文件 |
data |
是 | - file: development.mobileprovision |
声明打进包里的内容,即被更新的描述文件本体,文件名固定为 development.mobileprovision |
从源码结构可以推断:CI 环境准备阶段会通过某种 cipd ensure/依赖拉取逻辑按 apple_signing 依赖读取该包,而包内始终只装一个固定文件名的 development.mobileprovision,因此每次更新只是“换文件、重新建包、移动版本指针”。
三、年度更新操作全流程(14 步)
下面是 README 定义的完整操作序列,按“前置授权 → 建包上传 → 移动 ref → 打年度 tag → CI 迁移”五个阶段重新梳理。
阶段 0:前置条件
Step 1–2:获取并等待 CIPD 写权限。 操作者需要先申请对相关 CIPD 包的读写访问权(Flutter 的 LUCI CIPD 实例管理页面中完成),随后等待约 5 分钟让权限系统同步生效。
说明:完整续期还包含“向 Apple 申请新证书”等其他环节,具体见仓库 README 中指向的 Google 内部流程文档;此处只执行与 mobileprovision 包相关的那一段。
阶段 1:更新描述文件并创建两个架构的 CIPD 包
Step 3:替换描述文件。 把新签发的 iOS Provisioning Profile 拷贝到本目录,并固定命名为 development.mobileprovision。文件名必须与两个 YAML 中 data: - file: 的声明严格一致,否则建包时 cipd create 会因找不到文件而失败。
# 在 engine/src/flutter/tools/cipd/mobileprovision/ 目录下执行
cp /path/to/new/profile.mobileprovision ./development.mobileprovision
Step 4:为 arm64 架构创建包。
cipd create --pkg-def mac-arm64.yaml
--pkg-def 会让 CIPD 按 mac-arm64.yaml 中的 package + data 声明收集文件并上传,生成一个新的不可变实例(instance)。
Step 5:验证并记录 arm64 实例 ID。 打开 CIPD 包管理页面中 flutter_internal/mac/mobileprovision/mac-arm64 包的最新一次上传记录,复制页面上的 Instance_ID 值(即这次上传对应的唯一实例 ID,后续 ref/tag 操作都指向它)。这一步是保证后续命令可追溯的关键,务必先复制再继续。
Step 6:为 amd64 架构创建包。
cipd create --pkg-def mac-amd64.yaml
Step 7–9:验证并记录 amd64 实例 ID。 同样在 flutter_internal/mac/mobileprovision/mac-amd64 页面找到最新上传,复制其 Instance_ID。
阶段 2:把 latest ref 指向最新实例
latest 是包的一个移动指针(ref),CI 默认按 latest 或版本 tag 取包,因此必须先把 ref 拨到新实例,旧实例仍保留在历史中可回滚。
Step 10:arm64 的 latest 指向新实例。 用上面复制的 ARM64_INSTANCE_ID 替换命令中的占位符:
cipd set-ref flutter_internal/mac/mobileprovision/mac-arm64 -ref latest -version ARM64_INSTANCE_ID
Step 11:amd64 的 latest 指向新实例:
cipd set-ref flutter_internal/mac/mobileprovision/mac-amd64 -ref latest -version AMD64_INSTANCE_ID
语义提示:
set-ref相当于给实例“改名贴标”,-version既可以是实例 ID,也可以是其他 ref 名。只用实例 ID 指向是最严谨的写法。
阶段 3:为新实例打年度版本 tag
为了后续能在 .ci.yaml 里稳定引用“某一年的证书方案”,惯例是给当年的两个实例打上 version:to_<年份> 格式的 tag(例如 2025 年即为 version:to_2025)。
Step 12:给 arm64 实例打 tag。 把 YOUR_NEW_TAG 换成 version:to_2025(或对应年份):
cipd set-tag flutter_internal/mac/mobileprovision/mac-arm64 -tag YOUR_NEW_TAG -version ARM64_INSTANCE_ID
Step 13:给 amd64 实例打同样的 tag:
cipd set-tag flutter_internal/mac/mobileprovision/mac-amd64 -tag YOUR_NEW_TAG -version AMD64_INSTANCE_ID
阶段 4:在 CI 配置中完成版本迁移
Step 14:更新 .ci.yaml。 找到 CI 工作流中所有 apple_signing 依赖步骤,把 version 从上一年的 tag 改为今年新打的 tag:
# 迁移前
{"dependency": "apple_signing", "version": "version:to_2024"}
# 迁移后
{"dependency": "apple_signing", "version": "version:to_2025"}
这一步的作用是让后续 CI 在准备签名环境时显式使用新一年的 profile,而不是依赖会随时间变化的 latest 指针,从而保证历史任务可复现(回看旧提交时仍能拉到与之匹配的旧 profile)。
四、命令速查表
| 阶段 | 命令 | 作用 |
|---|---|---|
| 建包 | cipd create --pkg-def mac-arm64.yaml |
依 YAML 声明打包上传 development.mobileprovision(arm64) |
| 建包 | cipd create --pkg-def mac-amd64.yaml |
同上(amd64) |
| 移动指针 | cipd set-ref <pkg> -ref latest -version <INSTANCE_ID> |
让 latest ref 指向本次上传的新实例 |
| 打 tag | cipd set-tag <pkg> -tag version:to_2025 -version <INSTANCE_ID> |
用年度 tag 标记新实例,供 .ci.yaml 稳定引用 |
| CI 迁移 | 修改 .ci.yaml 中 apple_signing 的 version |
让后续 CI 使用新年度 profile |
五、实操注意事项与排障要点
综合 README 流程与包定义文件,可以总结出以下容易踩坑的点:
- 文件命名必须精确匹配。 包体内只收录
development.mobileprovision一个文件,两个 YAML 的data都指向该固定文件名;拷贝错误或大小写不一致都会导致cipd create失败。 - Instance ID 随手记录。 建包完成后网页端展示的
Instance_ID是唯一不可变标识,后续 4 条 ref/tag 命令都依赖它;建议按 arm64 / amd64 分两组保管,避免混用。 - 两个架构都要操作,不可只更一个。 建包、设
latest、打 tag 都必须对mac-arm64与mac-amd64各执行一遍,否则会在某类 Mac 测试宿主机上出现签名凭据未更新的现象。 - tag 迁移需要与建包同批完成。 若先更新了
.ci.yaml而 tag 尚未就绪,CI 解析version:to_2025会失败;建议严格按“先建包打 tag、再改.ci.yaml”的顺序推进。 - 权限同步有延迟。 新申请写权限后需要等待约 5 分钟再执行
cipd create,否则可能被服务端拒绝。
六、从仓库继续深入
- 更新操作手册全文:engine/src/flutter/tools/cipd/mobileprovision/README.md
- arm64 包定义(含全部字段注释):mac-arm64.yaml
- amd64 包定义:mac-amd64.yaml
- 同目录下的其他 CIPD 包定义可参考:engine/src/flutter/tools/cipd/(如
android_embedding_bundle/、malioc/),用于了解 Flutter 如何系统性地用 CIPD 管理跨平台二进制与测试凭据。
通过以上流程,读者即可理解 Flutter 仓库如何在“证书年度到期”这一周期性约束下,用 CIPD 的 instance / ref / tag 三层模型实现 iOS 签名描述文件的平滑换发,并保证 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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00