数据点: Apple 官方文档明确显示,命令行测试可以生成包含会话结果、代码覆盖率和日志的
.xcresult结果包。
因此,xcodebuild exit code 65只能先被视为“任务未成功完成”的汇总状态,不能直接套用某一条清缓存、重装证书或重启模拟器的命令。正确做法是先保存完整日志和xcresult,再按失败阶段定位。
这篇文章适合三类人:图形界面构建成功,但通过 SSH 或 CI 执行 xcodebuild 失败的独立开发者;运行模拟器测试时遇到目标不可用、启动超时或测试失败的小团队;以及需要在远程 Mac 上执行 Archive、签名和无人值守打包的发布维护者。
[ SECTION_01 ] 先把 exit code 65 放回正确的位置
xcodebuild 的退出状态位于日志的最外层。它告诉调用方本次任务失败,却不负责解释失败发生在项目入口、源码编译、测试启动、代码签名还是导出阶段。
下面是一段已经脱敏的典型日志结构:
Command line invocation:
/usr/bin/xcodebuild -workspace Redacted.xcworkspace \
-scheme Redacted -destination 'platform=iOS Simulator,id=REDACTED' test
...
Test session results, code coverage, and logs:
/tmp/BuildResults.xcresult
** TEST FAILED **
xcodebuild: error: ...
Exit code: 65
如果日志末尾只有 Exit code: 65,真正有价值的信息通常在更早的位置。例如:
error: Scheme 'Redacted' is not shared
或者:
error: Unable to find a destination matching the provided destination specifier
也可能是:
error: No signing certificate "Apple Development" found
这三种错误都可能让自动化任务失败,但修复路径完全不同。Apple 对 Scheme 的作用、目标配置和构建动作有明确说明:Scheme 会决定构建、测试、运行和 Archive 时使用哪些 Target 与配置。可先阅读 Apple 关于自定义 Xcode Scheme 的说明。
四类任务的第一轮分流
| 任务动作 | 主要产物或阶段 | 首先检查什么 | 不应先做什么 |
|---|---|---|---|
build |
编译、链接和构建产品 | 项目入口、Scheme、Configuration、依赖和脚本 | 直接删除全部缓存 |
test |
构建测试产品并启动测试 | Destination、模拟器运行时、测试计划、启动日志 | 直接判断为签名错误 |
archive |
生成 .xcarchive |
Release 配置、Archive Scheme、签名设置 | 只检查 App Store 上传权限 |
exportArchive |
从 Archive 导出发布产物 | ExportOptions.plist、证书、Profile、导出方式 |
重新编译项目 |
archive 成功而 exportArchive 失败,说明项目至少已经走过了编译和 Archive 生成阶段。此时继续修改源码,通常不能解决导出问题。Apple 的发布文档也把 Archive 与 Export 拆成两个命令行动作,可参考 Apple 关于创建 Distribution-signed 代码的说明。
[ SECTION_02 ] 日志、Scheme 与远程会话要同时对照
很多“本地 Xcode 能构建、远程 xcodebuild 失败”的问题,并不是代码突然失效,而是两次执行使用了不同的入口或环境。
先在项目根目录确认实际文件:
find . -maxdepth 2 \( -name "*.xcworkspace" -o -name "*.xcodeproj" \) -print
如果项目通过 CocoaPods、某些依赖管理工具或工作区组织多个 Target,命令通常应使用 .xcworkspace,而不是继续调用原始 .xcodeproj。如果项目没有工作区,才使用 .xcodeproj。
随后列出可见 Scheme:
xcodebuild -list -workspace Redacted.xcworkspace
或:
xcodebuild -list -project Redacted.xcodeproj
项目名、Scheme、路径、Bundle ID 和 Team ID 均应在日志中脱敏。真正需要确认的是:
- Scheme 是否存在;
- Scheme 是否处于共享状态;
- CI 检出的提交是否包含共享 Scheme 文件;
- 使用的是
Debug、Release还是自定义 Configuration; - 远程 Mac 是否安装了对应 SDK 和模拟器运行时;
-destination是否指向当前主机真实存在的设备或模拟器。
Apple 文档指出,Scheme 不只保存一个名称,它还关联 Target、构建配置、运行参数和测试设置。若本地通过 Xcode 点击的是一个共享 Scheme,而远程命令调用的是另一个名称相近的 Scheme,最后都可能只表现为 exit code 65。
为什么 Xcode 能构建,但 xcodebuild 失败
图形界面与 SSH 会话经常存在以下差异:
| 对照项 | 图形界面执行 | SSH 或 CI 执行 | 常见后果 |
|---|---|---|---|
| 当前目录 | Xcode 自动打开项目目录 | 脚本可能从临时目录启动 | 相对路径、脚本和配置文件找不到 |
| 用户环境 | 已加载账户与图形化配置 | 环境变量可能更少 | 私有仓库、工具链或脚本参数缺失 |
| Scheme | 用户选择过可用 Scheme | 命令必须明确传入 | Scheme 不存在或未共享 |
| 会话类型 | 有完整图形会话 | 可能是无图形后台会话 | Simulator、Keychain 或授权提示无法正常工作 |
| 凭据访问 | 用户已解锁 Keychain | 后台进程未授权 | 签名身份不可用 |
对照时不要把完整命令直接复制到公开工单或文章中。可以保留命令结构,但替换用户名、路径、项目名、设备标识和证书名称。
建议在交互终端与自动化会话中分别输出:
pwd
xcode-select -p
xcodebuild -version
env | sort
env 的结果需要过滤密钥、令牌、私有仓库地址和 Apple 账户信息。xcode-select -p 用于确认活动开发者目录。若本地指向完整 Xcode,而远程指向 Command Line Tools,后续构建行为就不能视为同一环境。
[ SECTION_03 ] 依赖、脚本和编译错误会被汇总成 65
exit code 65 并不表示“代码没有问题,只是签名失败”。它也可能由依赖解析、Swift 编译、资源处理、链接器或 Run Script 阶段触发。
排查时要先找第一条有效错误,而不是最后一条错误。可以使用以下方式缩小日志:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme Redacted \
-configuration Release \
-destination 'generic/platform=iOS' \
build \
2>&1 | tee build.log
然后优先搜索:
grep -nE "error:|fatal error:|CodeSign|PhaseScriptExecution|No such module|unable to find" build.log
这些关键词不是根因分类器,但能帮助快速定位阶段。具体判断仍应回到上下文。
依赖解析层
检查锁定文件是否与当前提交一致,例如:
Package.resolved是否被提交;- 私有 Swift Package 仓库是否需要认证;
- 依赖版本是否在远程环境能够解析;
- 工作目录是否包含本地依赖;
- CI 是否在执行构建前清理了依赖目录;
- 远程环境是否拥有所需的网络访问权限。
如果日志显示的是依赖下载或解析失败,清理 Derived Data 并不能修复私有仓库认证。相反,如果依赖缓存被清除后问题消失,也不能立即判定“缓存就是根因”,还应在同一提交的干净检出中复现。
编译、资源和链接层
这三类错误常见表现不同:
| 日志特征 | 可能所在阶段 | 验证动作 |
|---|---|---|
cannot find type、no such module |
源码编译或模块导入 | 检查 Target Membership、依赖版本和编译条件 |
actool、资源目录或编译资源报错 |
资源处理 | 检查资源路径、文件大小写和目标成员关系 |
Undefined symbols、linker command failed |
链接 | 检查 Framework、库、架构和链接顺序 |
PhaseScriptExecution |
Run Script | 检查脚本解释器、工作目录、权限和环境变量 |
Apple 的构建系统会根据项目设置组织编译、资源和依赖任务。构建配置文件还会叠加项目与 Target 设置,因此同一个 Scheme 在不同 Configuration 下可能得到不同结果。需要覆盖配置时,应明确记录 .xcconfig 的来源和应用顺序,可参考 Apple 关于 Build Configuration 文件的文档。
⚠️ 经验提醒: “删除 Derived Data → 重试”只能作为验证手段,不能作为第一诊断结论。若不保留第一次失败的日志,清缓存后即使构建成功,也无法知道原问题是缓存、依赖、脚本还是环境差异。
[ SECTION_04 ] Test、Simulator 与 xcresult 要拆开看
测试任务最容易把多个阶段混在一起。xcodebuild test 可能先构建测试产品,再选择 Destination、启动测试进程,最后执行测试用例。失败发生在不同阶段,日志位置和修复方式并不相同。
Apple 的测试流程支持先执行 build-for-testing,再执行 test-without-building。这种拆分能帮助判断问题是在“测试产品没有构建出来”,还是“测试产品已经存在,但运行环境无法启动”。相关行为可参考 Apple 关于 Xcode Cloud 测试动作分阶段执行的说明。
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme Redacted \
-destination 'platform=iOS Simulator,id=REDACTED' \
-resultBundlePath "$PWD/results-build.xcresult" \
build-for-testing
构建测试产品成功后,再执行:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme Redacted \
-destination 'platform=iOS Simulator,id=REDACTED' \
-resultBundlePath "$PWD/results-test.xcresult" \
test-without-building
需要重点区分以下现象:
- 找不到 Destination: 设备标识无效、运行时未安装,或目标平台与项目不匹配;
- 构建测试产品失败: 更接近源码、依赖、编译配置或测试 Target 问题;
- 启动模拟器失败: 检查当前会话是否具备启动所需的服务和图形环境;
- 应用启动超时: 检查测试进程、模拟器状态、启动参数和测试本身;
- 测试断言失败: 构建已经完成,exit code 65 只是测试任务失败的外层结果。
保存结果包时,应把路径设为每次运行唯一的位置,避免并发任务互相覆盖:
RESULT_DIR="$PWD/artifacts/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RESULT_DIR"
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme Redacted \
-destination 'platform=iOS Simulator,id=REDACTED' \
-resultBundlePath "$RESULT_DIR/test.xcresult" \
test 2>&1 | tee "$RESULT_DIR/console.log"
Apple 文档说明,命令行测试生成的结果包包含测试会话结果、日志,以及启用时的代码覆盖率信息。环境变量文档还列出了测试结果包路径、模拟器设备类型、运行时和 UDID 等测试相关变量,可参考 Apple 的测试环境变量参考 与 运行测试并解读结果的文档。
[ SECTION_05 ] Archive、签名和导出是三道不同的门
发布任务中,以下四个状态不能混用:
- 源码是否编译成功;
- Archive 是否生成;
- Archive 是否包含正确的签名资产;
- Archive 是否能够按目标方式导出。
先单独验证 Archive:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme Redacted \
-configuration Release \
-archivePath "$PWD/artifacts/Redacted.xcarchive" \
archive
然后再执行导出:
xcodebuild \
-exportArchive \
-archivePath "$PWD/artifacts/Redacted.xcarchive" \
-exportOptionsPlist "$PWD/ExportOptions.plist" \
-exportPath "$PWD/artifacts/exported"
Apple 的归档文档明确将 archive 与 exportArchive 作为连续但独立的命令行动作。导出阶段还会读取 ExportOptions.plist,因此 Archive 成功并不代表导出一定成功。具体导出示例可参考 Apple 关于自定义 Xcode Archive 流程的说明。
签名排查至少要确认:
security find-identity -v -p codesigning
security list-keychains
重点不是只看证书名称,而是确认 Keychain 中存在带对应私钥的完整签名身份。Apple 文档指出,只有证书而没有对应私钥,不能用于代码签名或生成其他数字签名。可参考 Apple 关于同步团队签名身份的说明。
Provisioning Profile 还需要与 App ID、签名证书、设备或分发方式匹配。Apple 对 Profile 的说明指出,它会关联允许签名的主体、应用标识、运行设备、有效期和授权项。相关背景可参考 Apple 的 Provisioning Profile 技术说明 TN3125。
远程或 SSH 构建特别容易遇到 Keychain 权限问题。交互式 Xcode 会话可能已经解锁 Keychain,而后台任务没有完成解锁或授权。修改 Keychain 搜索列表、导入新的 .p12、替换 Profile 或删除旧证书前,应记录:
- 当前构建任务使用哪个用户;
- 哪个 Keychain 被调用;
- 证书是否带私钥;
- 修改会影响哪些项目;
- 失败后能否恢复到原来的签名资产。
不要把“删除证书后重新生成”当作默认步骤。它可能影响其他开发者、其他 CI 节点和已经发布的自动化流程。
[ SECTION_06 ] 用重复构建判断该修项目还是修环境
当第一次失败被定位后,下一步不是立刻更换 Mac,而是做一次可重复验证。固定以下条件:
- 同一个 Git 提交;
- 同一个项目入口;
- 同一个 Scheme;
- 同一个 Configuration;
- 同一个 Destination;
- 同一份依赖锁定文件;
- 同一套签名资产;
- 同一种会话方式。
完成后再做一次干净检出:
git clone --no-checkout REDACTED_REPOSITORY clean-checkout
cd clean-checkout
git checkout REDACTED_COMMIT
随后在新目录执行相同命令,并把日志、xcresult、Archive 或导出产物归档。不要只在已经被修改过、生成过缓存的工作目录里重试。
构建验收清单
- [ ] 记录
xcode-select -p和xcodebuild -version的输出。 - [ ] 明确本次调用的是
.xcworkspace还是.xcodeproj。 - [ ] 用
xcodebuild -list确认 Scheme 存在并可被远程环境读取。 - [ ] 固定 Git 提交、Configuration 和 Destination。
- [ ] 保存完整标准输出,而不是只保留最后一行退出状态。
- [ ] 为测试任务指定独立的
-resultBundlePath。 - [ ] 使用
build-for-testing与test-without-building区分构建失败和运行失败。 - [ ] 单独验证
archive,再验证exportArchive。 - [ ] 确认签名身份同时包含证书与私钥。
- [ ] 检查 SSH 或后台任务是否能够访问所需 Keychain。
- [ ] 在干净检出目录中重复同一任务。
- [ ] 在会话重连或主机重启后再次执行关键任务。
- [ ] 对日志中的项目名、用户名、路径、Bundle ID、Team ID、设备标识和证书名称完成脱敏。
可以用下面这张决策卡收束判断:
| 复现结果 | 更可能的处理方向 | 下一步 |
|---|---|---|
| 同一提交在所有环境都失败 | 项目、依赖或签名配置问题 | 回到首个有效错误修复项目 |
| 只有 SSH 失败,图形会话成功 | 会话、环境变量或 Keychain 权限问题 | 对照用户、目录、工具链和凭据 |
| 只有某台远程 Mac 失败 | 主机状态、SDK、模拟器或缓存问题 | 在隔离主机复测并重建环境 |
build-for-testing 成功,测试运行失败 |
Destination、Simulator 或测试启动问题 | 检查运行时、设备状态和会话能力 |
Archive 成功,exportArchive 失败 |
导出配置、证书或 Profile 问题 | 检查 ExportOptions.plist 和签名资产 |
| 更换主机后同一提交稳定成功 | 原构建环境存在持久差异 | 将稳定配置固化为常驻构建节点 |
这套判断比“再跑一次”更可靠。因为它把失败是否跟随代码提交、用户会话或主机状态拆开了。
如果排查结果显示错误只在原主机出现,可以把同一提交迁移到隔离的远程 Mac 做对照。对于需要长期运行 iOS 打包、测试或 Archive 的团队,远程 Mac 构建环境验收思路比在一台被多人反复修改的个人电脑上继续叠加补丁更容易维护;需要评估 Apple Silicon 节点时,也可以查看 Mac 远程方案配置信息。
[ SECTION_07 ] 当前构建方案与远程 Mac 的取舍
如果当前方案是个人 Mac 加 SSH 临时转发,常见缺点是:机器可能被日常开发占用,图形会话和后台会话的行为不一致,Keychain 授权难以审计,模拟器和 Xcode 组件也容易随着本地操作发生变化。若使用共享 CI 主机,还会增加工作目录、缓存和签名资产互相污染的问题。
当目标是临时复现、短期发布或需要一台独立的 iOS 打包环境时,NOVAKVM 的远程 Mac 可以作为隔离节点使用。它不替代所有本地开发场景:长期高负载且需要物理设备接口的团队,仍应评估自购 Mac;但对需要按周、按月使用真实 macOS 环境,或希望先验证远程构建链路的开发者,租用一台可独立维护的 Mac 往往更容易控制变量。可先在 NOVAKVM 的 Mac 远程租赁页面确认适合的使用方式,再用固定提交完成一次 Build、Test 或 Archive 对照任务。