构建日志显示 Bazel 命令已经执行成功,但归档、Simulator 测试或签名阶段仍然失败。
最快的判断是:Bazel 9 iOS 构建不能让完整交付链路脱离 Mac。Linux 适合承载平台无关的检查与部分测试;涉及 Apple SDK、Xcode 26、Simulator、签名和应用打包的任务,应保留在真实 macOS 节点。小团队先用单台远程 Mac 跑通闭环,中大型团队再采用 Linux 加远程 Mac 的混合架构。
本周建议动作:先不要急着扩容。选一个最小但包含真实依赖的 iOS 项目,记录每个 Bazel action 的执行平台、工具链版本、缓存状态、签名结果和重启恢复结果,再决定哪些任务迁移到 Linux。
这篇文章适合正在接入 Bazel 9 的 iOS 构建工程师、维护 Linux CI 集群的 DevOps 工程师,以及准备扩容 Apple 平台构建能力的技术负责人。如果只需要普通 Linux 编译或通用测试,部分内容可以直接跳过;如果目标是稳定生成可测试、可签名、可发布的 iOS 产物,则应完整执行后面的验收步骤。
最后更新于 2026 年 8 月 29 日,版本与兼容性信息核实自 Bazel、rules_apple、rules_swift 和 Apple 官方资料。
先把工具链边界钉死
Bazel 负责分析依赖图、生成和调度构建 action,但它不是 Apple SDK,也不能替代 Xcode。rules_apple 负责 Apple 平台目标的打包、测试和相关规则,rules_swift 负责 Swift 编译规则;真正提供 SDK、编译器、链接器、Simulator 运行环境和 Apple 平台命令行工具的,仍然是 Xcode 与 macOS。
Bazel 官方当前的发布模型显示,Bazel 9 处于 Active LTS 阶段,最新版本为 9.2.0,支持周期到 2028 年 12 月。这说明 Bazel 9 适合作为 CI 基线,但不代表任意旧版规则、Xcode 或 SDK 都能直接组合使用。具体生命周期以 Bazel 官方发布模型 为准。
rules_apple 的发布说明已经列出 Bazel 9.x 支持范围,当前发布信息还明确提到支持 Bazel 9 所需的变更。因此,升级 Bazel 时必须同时检查 rules_apple 与 rules_swift 的 Release 说明,不能只替换 Bazel 二进制。(rules_apple 官方 Releases)
Xcode 26 包含 Swift 6.2,以及 iOS 26、iPadOS 26、tvOS 26、watchOS 26、macOS Tahoe 26 和 visionOS 26 SDK;Apple 文档还注明,Xcode 26 要求运行在 macOS Sequoia 15.6 或更高版本上。也就是说,所谓“有一台 Mac”还不够,节点上的 macOS、Xcode、SDK 和规则版本必须组成可追踪的组合。(Apple 的 Xcode 26 Release Notes)
Apple 还公布了 App Store Connect 的后续构建要求:自 2026 年 4 月 28 日起,上传到 App Store Connect 的应用需要使用 Xcode 26 或更高版本,并采用对应的 iOS 26 等 SDK 构建。(Apple 的平台构建要求说明)
可以先用下面的命令把 Mac 节点的工具链状态固化到构建日志中:
xcode-select -p
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-path
xcrun --sdk iphonesimulator --show-sdk-path
sw_vers
uname -m
示例输出应至少能让 CI 记录以下字段:
Developer directory: /Applications/Xcode.app/Contents/Developer
Xcode: 26.x
iPhoneOS SDK: 26.x
iPhoneSimulator SDK: 26.x
ProductName: macOS
Architecture: arm64
这里的 26.x 只是输出格式示例,不应在配置文件中写成未经核实的具体小版本。每次切换 Xcode,都应重新生成一份环境清单。
⚠️ 注意:“Bazel 命令能启动”只证明 Bazel 本身能运行,不能证明 iOS 应用可以完成链接、Simulator 测试、签名、归档和导出。
Bazel 9 还带来一个容易被忽略的迁移边界:官方说明显示,Bazel 9 已完成对旧式 WORKSPACE 支持的移除,Bzlmod 成为依赖管理入口。项目如果仍依赖隐式的 WORKSPACE 仓库、旧式外部规则或本地脚本,升级后应单独检查 MODULE.bazel、模块扩展和锁定结果。(Bazel 9 官方公告)
用 action 平台判断 Linux 与 Mac 的分工
混合 CI 的关键不是把某个“iOS 脚本”整体搬到 Mac,而是判断每个 action 需要什么执行环境。Bazel 官方将运行位置区分为 host platform、execution platform 和 target platform;远程执行时,真正运行 action 的是 execution platform,而不是提交任务的机器。(Bazel 远程执行规则文档)
建议先按下面的边界拆分:
- Linux 可承载:代码格式检查、静态分析、依赖图检查、通用代码生成、与 Apple SDK 无关的工具编译,以及明确支持 Linux 的 Swift 或非平台测试。
- 通常应放到 macOS:读取 Apple SDK 的编译与链接、生成 iOS 应用包、调用
xcrun或xcodebuild、启动 Simulator、执行设备相关测试、代码签名、归档和导出。 - 必须谨慎验证:混合语言目标、Swift Package 依赖、宏或插件、资源处理、XCFramework 生成,以及通过
PATH调用本地二进制的自定义规则。
rules_swift 官方文档明确区分了 macOS 与 Linux 的工具链:Apple 用户需要安装 Xcode;Linux 用户可以安装独立 Swift 工具链,但 Linux 上还需要正确配置 Clang 等依赖。对于 Apple 平台应用,这个区别意味着“Swift 能在 Linux 编译”不等于“iOS 应用能在 Linux 完成交付”。(rules_swift 官方文档)
不要按脚本名称猜测 action 的执行位置。应在 CI 中保留 Bazel 的详细日志,并检查 action 的命令、输入文件、工具链和平台属性:
bazel build //app:ios_app \
--announce_rc \
--subcommands \
--verbose_failures \
--profile=/tmp/bazel-profile.json
对于远程执行,还应在平台定义中表达 macOS 节点能力,例如:
platform(
name = "macos_xcode_26_arm64",
constraint_values = [
"@platforms//os:osx",
"@platforms//cpu:arm64",
],
exec_properties = {
"os_family": "macos",
"xcode_major": "26",
"apple_sdk": "iphoneos",
},
)
上面的属性名只是项目内部约定,远程执行服务不会自动理解 xcode_major 或 apple_sdk 的含义;调度器和 Mac worker 必须对这些属性建立一致的匹配规则。平台属性和工具链选择应结合 Bazel 的平台与工具链参考 进行配置。
因此,远程 Mac 接入混合集群至少需要完成 5 个动作:
- 为 macOS 节点安装并固定目标 Xcode、Command Line Tools、Bazel 和规则版本。
- 将 macOS、CPU 架构和 Xcode 能力登记为明确的平台属性。
- 让调度器只把需要 Apple 工具链的 action 发往 Mac worker。
- 检查 action 输入是否包含所需 SDK、工具链和脚本,排除宿主机文件泄漏。
- 在构建日志中保存实际执行平台,而不是只保存 CI job 的名称。
用复现性与安全指标排除“能跑但不能交付”
可复现性首先要回答一个问题:同一份源码,在干净环境和空缓存环境中,是否仍能得到相同的构建结果。
建议把以下内容纳入版本控制或构建元数据:
MODULE.bazel与模块锁定文件;rules_apple、rules_swift、apple_support的版本;- Bazel 版本与启动参数;
- macOS、Xcode 和 SDK 版本;
- 构建所需的环境变量;
- 自定义 Starlark 规则使用的工具版本;
- 签名方式、证书标识和 provisioning profile 的来源。
可以设计两次互相独立的构建:
git clone <REPOSITORY_URL> /tmp/ios-build-a
cd /tmp/ios-build-a
bazel clean --expunge
bazel build //app:ios_app \
--disk_cache= \
--repository_cache=/tmp/empty-repository-cache \
--verbose_failures
然后在另一台 Mac 节点或重建后的工作目录中重复执行。重点不是只比较退出码,还要检查:
- 生成的
.app、.ipa或归档是否存在; - 二进制架构和最低系统版本是否符合目标;
- 资源、嵌入框架和符号文件是否完整;
- 构建过程是否读取了工作区外的文件;
- 是否依赖某个用户的
PATH、钥匙串或图形登录状态。
Bazel 远程执行要求 action 隔离,工具不能依赖远程环境中碰巧存在的 PATH、JAVA_HOME 或宿主机文件。官方远程执行规则文档将隐式依赖、平台相关二进制和工具链调用列为重点风险。
签名要单独验收,不能把“无签名编译成功”当作发布链路通过。Apple 对 provisioning profile 的说明指出,profile 会约束签名者、App ID、可运行设备、有效期和 entitlements;设备运行时还会校验这些关系。(Apple 的代码签名与 provisioning profile 技术说明)
因此,CI 应把证书、钥匙串、profile 和构建缓存分开管理:
security find-identity -v -p codesigning
codesign --verify --deep --strict --verbose=2 <APP_PATH>
codesign --display --entitlements - --xml <APP_PATH>
security cms -D -i <PROFILE_PATH> -o /tmp/profile.plist
这些命令只用于验收和排错,敏感证书内容不应写入普通构建日志。更稳妥的方式是让签名步骤只在受控的 macOS 节点运行,并在任务结束后清理临时钥匙串和 profile 文件。
缓存、队列与恢复决定架构规模
远程缓存、远程执行和远程 Mac 登录是三种不同的能力:
- 远程缓存:复用已经生成的 action 输出,减少重复计算。
- 远程执行:把 action 发给其他 worker 实际运行。
- 远程 Mac 登录:通过 SSH、VNC 或网页控制台访问一台机器,解决运维与交互问题。
远程缓存不能提供 Xcode,远程执行也不等于可以通过 VNC 手动点击恢复。Bazel 官方说明,远程执行和远程缓存可以通过 gRPC 协议协同使用,但两者的目标不同:前者扩展执行节点,后者复用构建输出。(Bazel 远程构建执行说明)
建议连续记录 6 类指标:
- Linux 与 Mac action 的数量及失败率;
- 每类 action 的缓存命中与未命中情况;
- Mac 队列等待时间;
- 编译、链接、测试、签名和导出各阶段的耗时;
- 失败后的重试次数及失败原因;
- 节点重启、断线或替换后的恢复结果。
不要在没有实测数据时直接宣称“远程 Mac 会快多少”。如果缓存命中率低,瓶颈可能在依赖下载或输入树变化;如果 Mac 队列很长,问题可能是节点数量;如果 action 失败集中在签名和导出阶段,问题则更可能是凭据、钥匙串或工具链隔离。
对于小团队,单台远程 Mac 的优势是链路简单:Linux 可以运行普通检查,最终 Bazel iOS 构建、测试和归档全部进入同一台 macOS 节点,便于先完成闭环。对于中大型团队,Linux 加 Mac 的混合架构更合理,因为通用任务不必争抢稀缺的 Apple 节点,但必须把平台路由和缓存边界定义清楚。
三种架构的决策表
| 方案 | 适合条件 | 主要优点 | 主要风险 | 上线判断 |
|---|---|---|---|---|
| 单一远程 Mac | 项目数量少,需要先跑通完整 iOS 闭环 | 平台简单,问题定位路径短,Xcode 与签名集中 | Mac 节点故障会阻塞全部 Apple 任务,队列扩展能力有限 | 适合作为小团队起步方案 |
| Linux 加远程 Mac 混合架构 | Linux CI 已有规模,Apple action 占比可识别 | 通用检查与部分测试留在 Linux,Mac 专注 Apple 专属任务 | 平台选择、缓存隔离、凭据管理更复杂 | 适合多数中大型团队 |
| 暂缓迁移 | 规则、依赖或 Xcode 组合尚未稳定,无法完成无人值守签名 | 避免把不成熟链路直接接入发布流程 | 继续承担本地 Mac 或旧 CI 的维护成本 | 兼容性或复现性不过关时应回退 |
如果正在设计节点平台,可以先参考 远程 Mac 构建节点上线验收 的运维思路,再把 Bazel action、Xcode 工具链和签名流程作为单独验收对象。需要估算 Linux 与 Mac 的任务比例时,也可以结合 Mac 计算资源方案 评估节点隔离、周期和扩容方式。
扩容触发条件应来自记录,而不是来自主观感觉。以下任一情况持续出现,就应重新评估 Mac 节点数量或任务路由:
- Mac 队列等待已经明显高于单个构建阶段本身;
- 多个独立 iOS action 长时间排队,而 Linux 节点仍有空闲;
- 重试主要由节点重启、登录状态或钥匙串异常触发;
- 缓存命中率不高,但大量 action 使用相同工具链和输入;
- 新增 Xcode 版本后,单一节点无法同时满足不同项目的兼容要求。
上线前验收与本周执行顺序
Bazel iOS 构建节点上线前,至少要完成下面 7 步:
- 锁版本:记录 Bazel 9 小版本、规则版本、Xcode、macOS 和 SDK。
- 做最小项目:项目必须包含 Swift 或 Objective-C 源码、资源、测试目标和真实依赖。
- 跑空缓存构建:清理本地缓存,确认依赖入口来自
MODULE.bazel和可追踪仓库。 - 检查 action:使用
--subcommands和 profile 确认 Linux、macOS 的实际分工。 - 跑 Simulator 测试:确认测试不是只完成编译,而是实际启动指定 Simulator 并生成结果。
- 跑无人值守签名:验证归档、签名、导出和产物校验,不依赖人工打开 Xcode。
- 做故障恢复:模拟 SSH 断线、worker 重启、Xcode 切换和节点替换,确认流水线可以从 CI 重新开始。
如果这 7 步中只有无签名编译成功,结论只能是“编译节点可用”,不能写成“iOS 发布节点可用”。如果 Simulator、签名或恢复测试失败,应暂缓迁移,并先修复工具链和凭据边界。
当前采用 Linux 加虚拟 macOS、临时 Hackintosh 或单纯 Linux 云主机,常见问题是 Apple SDK 不完整、Xcode 版本不可控、Simulator 与签名能力缺失,以及节点恢复经常依赖人工图形登录;这些方案可以用于局部验证,却不适合作为长期稳定的 Bazel iOS 交付底座。若需要先隔离一台真实 macOS 节点验证项目,NodeMini 的远程 Mac 可以作为临时构建环境,让你在不立即购买 Mac 硬件的情况下记录缓存、测试、签名和重启恢复证据,再决定长期租用规模或混合节点数量。
常见问题
FAQ 已覆盖 Xcode 依赖、Linux 可行范围、必须路由到 macOS 的 action、远程 Mac 接入方式,以及节点上线验收标准。真正上线前,仍应以目标项目的 action 日志和无人值守产物为最终依据。