一个原本只面向 iPhone 和 iPad 的工程启用 Mac Catalyst 后,最常见的误判是“iOS 能编译,Mac 端应该也能过”。实际进入云端 Mac 的自动化任务,问题往往集中在依赖平台声明、资源归属、条件编译和缓存串用。可靠做法不是在原有命令后补一个 destination,而是把两个平台当作共享源码、独立验收的构建产品。
先确认工程真的支持双目标
先在 Xcode 的目标设置中启用 Mac Catalyst,再检查项目文件是否提交了对应变更。不要只看图形界面的勾选状态;自动化任务应从构建设置读取最终值:
xcodebuild \
-project DemoApp.xcodeproj \
-scheme DemoApp \
-showBuildSettings |
grep -E 'SUPPORTS_MACCATALYST|PRODUCT_BUNDLE_IDENTIFIER|SDKROOT'
SUPPORTS_MACCATALYST 应为 YES。如果工程使用工作区,就把 -project 换成 -workspace。随后执行 xcodebuild -showdestinations,确认共享 scheme 同时暴露 iOS Simulator 与 Mac Catalyst destination。若命令行找不到 scheme,先检查 scheme 是否设为 Shared,而不是反复修改 destination 字符串。
还要逐项审查 Swift Package、二进制依赖与内部模块的平台范围。某个包能在 iOS 编译,不代表它声明了 Mac Catalyst 支持。对闭源二进制依赖,应检查 XCFramework 是否包含 Catalyst 对应切片;缺少切片时,链接阶段才会失败。
隔离缓存与结果目录
iOS 与 Catalyst 会生成不同的模块缓存、中间对象和链接产物。持续集成若共用一个 DerivedData,失败可能随着任务顺序变化,清理后又暂时消失。建议按平台设置固定目录,并让结果包一并归档:
set -euo pipefail
xcodebuild \
-workspace DemoApp.xcworkspace \
-scheme DemoApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build/derived-ios \
-resultBundlePath build/results-ios.xcresult \
CODE_SIGNING_ALLOWED=NO \
build-for-testing
xcodebuild \
-workspace DemoApp.xcworkspace \
-scheme DemoApp \
-destination 'platform=macOS,variant=Mac Catalyst,arch=arm64' \
-derivedDataPath build/derived-catalyst \
-resultBundlePath build/results-catalyst.xcresult \
CODE_SIGNING_ALLOWED=NO \
build-for-testing
这里先关闭签名,是为了让门禁专注于源码、依赖和链接是否成立。归档与交付签名应放在后续任务中,避免把平台兼容错误和签名配置错误混成一条日志。
不要把删除整个
~/Library/Developer/Xcode/DerivedData当作默认修复。精准删除当前任务目录,既不会干扰同机其他作业,也能保留失败现场。
收拢平台差异而不是散落判断
Catalyst 的界面生命周期、菜单、窗口行为和部分系统能力与 iOS 不同。条件编译应集中在适配层,不要让 targetEnvironment(macCatalyst) 散落在业务代码中。
enum PlatformLayout {
static var usesDesktopNavigation: Bool {
#if targetEnvironment(macCatalyst)
return true
#else
return false
#endif
}
}
平台专属 API 还要配合可用性检查。条件编译只说明当前编译目标,不代表运行系统版本一定提供该 API。对公共协议先定义稳定接口,再由 iOS 与 Catalyst 分别实现,可让单元测试直接验证两端行为。
资源也要单独核对。字体、隐私说明文件、本地化资源和数据模型可能因 Target Membership 漏选而只进入一个产品。可在构建后查看产物目录,确认关键文件存在,而不是等到运行时才发现空白界面。
| 检查项 | iOS | Mac Catalyst |
|---|---|---|
| 依赖可链接 | 模拟器切片可用 | Catalyst 切片可用 |
| 资源归属 | App 包内可见 | Catalyst App 包内可见 |
| 条件编译 | 移动端分支 | 桌面适配分支 |
| 结果证据 | 独立 xcresult | 独立 xcresult |
把失败拆成可定位的阶段
双目标任务不应只有一个总的“构建失败”。建议依次拆成依赖解析、无签名编译、单元测试、归档和交付验收。前一阶段通过后再进入下一阶段,日志也按平台命名。
编译失败先看四类信号
遇到 No such module,先查依赖的平台声明和构建目标;遇到架构不匹配,检查二进制切片,不要直接添加全局排除架构;遇到 unavailable API,回到适配层补充条件编译与可用性判断;遇到重复资源或缺失资源,则检查 Copy Bundle Resources 与 Target Membership。
构建脚本同样可能带有平台假设。例如脚本把 SDK 写死为 iphoneos,Catalyst 任务就会读取错误路径。脚本应使用 Xcode 注入的 PLATFORM_NAME、SDKROOT 和 TARGET_BUILD_DIR,并对未知平台立即退出。
建立合并前检查清单
最小门禁应保证两个目标都能从干净目录完成依赖解析和无签名编译,并分别保存 .xcresult。核心模块的单元测试应覆盖平台适配接口;涉及窗口、菜单或拖放的行为,再安排 Catalyst 专属测试。
提交前可按以下顺序复核:
- scheme 已共享,命令行能列出两个 destination;
- iOS 与 Catalyst 使用独立 DerivedData;
- 依赖明确支持两个平台,二进制切片完整;
- 平台判断集中在适配层,公共逻辑没有重复实现;
- 关键资源同时进入两个产品;
- 失败日志、结果包和构建设置快照按平台归档;
- 编译门禁与归档签名任务分开执行。
在 G-Mini 或其他远程构建环境中,首次落地时先连续运行两次,再交换两个目标的执行顺序。若结果随顺序变化,优先排查共享缓存、脚本临时目录和未清理的生成文件。双目标流程稳定的标志不是偶尔同时通过,而是从干净工作区、任意执行顺序都得到一致结果。
常见问题
iOS 与 Mac Catalyst 可以共用同一个 DerivedData 目录吗?
本地临时构建可以共用,但持续集成建议分开。独立目录能避免中间产物和索引状态互相污染,也便于分别清理、归档与定位失败。
双目标门禁需要一开始就执行完整测试吗?
不需要。先让两个目标稳定通过无签名编译与基础单元测试,再逐步增加界面测试、归档和签名验收,能更快区分平台兼容问题与交付配置问题。
Mac Catalyst 构建失败时应先检查什么?
先检查目标是否启用 Catalyst、依赖是否支持该平台、条件编译分支是否完整,以及资源和构建阶段是否同时归属于对应目标,最后再排查签名与归档配置。
为下一条构建队列准备云端 Mac
比较两档 M4 配置与五个节点,按实际工作周期选择日、周、月或季租用。