构建日志显示 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 官方资料。

01

先把工具链边界钉死

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_applerules_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 官方公告)

02

用 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 应用包、调用 xcrunxcodebuild、启动 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_majorapple_sdk 的含义;调度器和 Mac worker 必须对这些属性建立一致的匹配规则。平台属性和工具链选择应结合 Bazel 的平台与工具链参考 进行配置。

因此,远程 Mac 接入混合集群至少需要完成 5 个动作:

  1. 为 macOS 节点安装并固定目标 Xcode、Command Line Tools、Bazel 和规则版本。
  2. 将 macOS、CPU 架构和 Xcode 能力登记为明确的平台属性。
  3. 让调度器只把需要 Apple 工具链的 action 发往 Mac worker。
  4. 检查 action 输入是否包含所需 SDK、工具链和脚本,排除宿主机文件泄漏。
  5. 在构建日志中保存实际执行平台,而不是只保存 CI job 的名称。
03

用复现性与安全指标排除“能跑但不能交付”

可复现性首先要回答一个问题:同一份源码,在干净环境和空缓存环境中,是否仍能得到相同的构建结果。

建议把以下内容纳入版本控制或构建元数据:

  • MODULE.bazel 与模块锁定文件;
  • rules_applerules_swiftapple_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 隔离,工具不能依赖远程环境中碰巧存在的 PATHJAVA_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 文件。

04

缓存、队列与恢复决定架构规模

远程缓存、远程执行和远程 Mac 登录是三种不同的能力:

  • 远程缓存:复用已经生成的 action 输出,减少重复计算。
  • 远程执行:把 action 发给其他 worker 实际运行。
  • 远程 Mac 登录:通过 SSH、VNC 或网页控制台访问一台机器,解决运维与交互问题。

远程缓存不能提供 Xcode,远程执行也不等于可以通过 VNC 手动点击恢复。Bazel 官方说明,远程执行和远程缓存可以通过 gRPC 协议协同使用,但两者的目标不同:前者扩展执行节点,后者复用构建输出。(Bazel 远程构建执行说明)

建议连续记录 6 类指标:

  1. Linux 与 Mac action 的数量及失败率;
  2. 每类 action 的缓存命中与未命中情况;
  3. Mac 队列等待时间;
  4. 编译、链接、测试、签名和导出各阶段的耗时;
  5. 失败后的重试次数及失败原因;
  6. 节点重启、断线或替换后的恢复结果。

不要在没有实测数据时直接宣称“远程 Mac 会快多少”。如果缓存命中率低,瓶颈可能在依赖下载或输入树变化;如果 Mac 队列很长,问题可能是节点数量;如果 action 失败集中在签名和导出阶段,问题则更可能是凭据、钥匙串或工具链隔离。

对于小团队,单台远程 Mac 的优势是链路简单:Linux 可以运行普通检查,最终 Bazel iOS 构建、测试和归档全部进入同一台 macOS 节点,便于先完成闭环。对于中大型团队,Linux 加 Mac 的混合架构更合理,因为通用任务不必争抢稀缺的 Apple 节点,但必须把平台路由和缓存边界定义清楚。

05

三种架构的决策表

方案 适合条件 主要优点 主要风险 上线判断
单一远程 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 版本后,单一节点无法同时满足不同项目的兼容要求。
06

上线前验收与本周执行顺序

Bazel iOS 构建节点上线前,至少要完成下面 7 步:

  1. 锁版本:记录 Bazel 9 小版本、规则版本、Xcode、macOS 和 SDK。
  2. 做最小项目:项目必须包含 Swift 或 Objective-C 源码、资源、测试目标和真实依赖。
  3. 跑空缓存构建:清理本地缓存,确认依赖入口来自 MODULE.bazel 和可追踪仓库。
  4. 检查 action:使用 --subcommands 和 profile 确认 Linux、macOS 的实际分工。
  5. 跑 Simulator 测试:确认测试不是只完成编译,而是实际启动指定 Simulator 并生成结果。
  6. 跑无人值守签名:验证归档、签名、导出和产物校验,不依赖人工打开 Xcode。
  7. 做故障恢复:模拟 SSH 断线、worker 重启、Xcode 切换和节点替换,确认流水线可以从 CI 重新开始。

如果这 7 步中只有无签名编译成功,结论只能是“编译节点可用”,不能写成“iOS 发布节点可用”。如果 Simulator、签名或恢复测试失败,应暂缓迁移,并先修复工具链和凭据边界。

当前采用 Linux 加虚拟 macOS、临时 Hackintosh 或单纯 Linux 云主机,常见问题是 Apple SDK 不完整、Xcode 版本不可控、Simulator 与签名能力缺失,以及节点恢复经常依赖人工图形登录;这些方案可以用于局部验证,却不适合作为长期稳定的 Bazel iOS 交付底座。若需要先隔离一台真实 macOS 节点验证项目,NodeMini 的远程 Mac 可以作为临时构建环境,让你在不立即购买 Mac 硬件的情况下记录缓存、测试、签名和重启恢复证据,再决定长期租用规模或混合节点数量。

07

常见问题

FAQ 已覆盖 Xcode 依赖、Linux 可行范围、必须路由到 macOS 的 action、远程 Mac 接入方式,以及节点上线验收标准。真正上线前,仍应以目标项目的 action 日志和无人值守产物为最终依据。