跳到主要内容

使用远程 Mac 构建服务器进行 iOS 开发

iOS remote Mac build setup with Cursor

构建一个 iOS 应用需要在流程中的某处有 macOS,但这并不意味着您的编辑器必须在 Mac 上运行。借助 Cursor 的 Remote-SSH 支持,您可以在 Windows 或 Linux 上保持快速的本地编辑体验,同时让一台 Mac 机器处理编译、签名和模拟器运行。本指南将带您了解架构选项、设置、构建命令,以及最耗费时间的那些陷阱。

为什么需要 Mac​

Apple 的工具链只在 macOS 上运行,而 iOS 工作流的若干部分直接依赖它。

  • 代码签名:签名身份位于 macOS 钥匙串中,用于生成经过签名的工件。
  • 配置描述文件:将应用 ID 绑定到设备和证书的描述文件通过 Apple 工具管理。
  • iOS 模拟器:模拟器是一个 macOS 应用程序;没有受支持的 Linux 或 Windows 等效物能提供真实设备的保真度。
  • xcodebuild 工具链:编译器、链接器和 SDK 随 macOS 上的 Xcode 一起提供。
  • App Store 提交:上传到 App Store Connect 需要 Apple 的工具,通常通过 macOS 上的 Transporter 或 altool/notarytool。

由于所有这些都是 macOS 绑定的,实际的问题不是您是否需要一台 Mac,而是那台 Mac 位于哪里。

架构选项​

没有唯一的最佳答案;正确的选择取决于您的团队规模、您的预算,以及您构建的频率。

选项工作方式优点缺点
通过 Remote-SSH 连接到 MacCursor 通过 SSH 连接;文件和终端位于 Mac 上单一事实来源、完整的工具链访问、真实的模拟器需要一台持久、可访问的 Mac
rsync / git 推送到 Mac在本地编辑,推送到 Mac,在那里构建编辑时可离线工作,心智模型简单同步漂移、大文件传输缓慢、没有实时 LSP
云 CI macOS runner推送到 Git,CI 在托管的 macOS runner 上构建无需拥有硬件、可复现,适合发布非交互式、按分钟计费、冷启动
云 Mac 提供商通过 SSH 租用 Mac(MacStadium、AWS EC2 Mac)按需的真实硬件、可扩展小时级成本、配置和拆除开销

对于日常开发,通过 Remote-SSH 连接到 Mac 通常是这种取舍中最好的平衡。云 CI 适合发布流水线。云 Mac 提供商适合希望拥有一个机群却不想拥有办公桌位的团队。

让工具匹配任务

使用 Remote-SSH 进行交互式开发,使用云 CI 进行发布,使用云 Mac 实例进行扩展或短时突发。大多数成熟的团队会运行其中至少两种,而不是迫使一种选项做所有事情。

设置远程 Mac​

这些步骤在将充当构建服务器的那台 Mac 上运行一次。您需要管理员权限和一个稳定的网络连接。

1. 安装 Xcode 和命令行工具​

从 Mac App Store 安装 Xcode,然后安装命令行工具,以便 xcodebuild 及相关二进制文件可用:

xcode-select --install

# Point the toolchain at the full Xcode app, not the standalone CLT
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

2. 接受许可证​

在全新安装时,xcodebuild 会在许可证被接受之前拒绝运行:

sudo xcodebuild -license accept

3. 创建构建用户​

避免以个人账户进行构建。创建一个专用用户,使凭据和钥匙串保持隔离:

# Create a build user (adjust the short name and full name)
sudo dscl . -create /Users/builduser
sudo dscl . -create /Users/builduser UserShell /bin/zsh
sudo dscl . -create /Users/builduser RealName "Build User"
sudo dscl . -create /Users/builduser UniqueID 510
sudo dscl . -create /Users/builduser PrimaryGroupID 20
sudo dscl . -create /Users/builduser NFSHomeDirectory /Users/builduser
sudo passwd builduser

4. 启用远程登录​

打开 SSH,以便 Mac 接受连接:

sudo systemsetup -setremotelogin on

5. 安装 SSH 公钥​

将您本地的公钥添加到构建用户的 authorized_keys,这样您就可以无需密码连接:

mkdir -p /Users/builduser/.ssh
# Paste your public key (id_ed25519.pub) into authorized_keys
chmod 700 /Users/builduser/.ssh
chmod 600 /Users/builduser/.ssh/authorized_keys
chown -R builduser:staff /Users/builduser/.ssh
锁定 SSH 账户

一台暴露于互联网的构建 Mac 不应允许构建用户使用密码登录。禁用密码身份验证,尽可能限制允许的源地址,并将私钥只保留在您的本地机器上。

通过 Remote-SSH 连接 Cursor​

一旦该 Mac 可达,就通过 Remote-SSH 将 Cursor 连接到它。远程端将成为运行扩展、终端和构建的环境。

1. 安装 Remote-SSH 扩展​

打开 Cursor,转到扩展视图(Ctrl+Shift+X),并安装 Remote - SSH(Microsoft 打包的该扩展可在 Cursor 的市场中获得)。

2. 添加 SSH 配置条目​

编辑您本地的 SSH 配置,通常位于 ~/.ssh/config,并添加一个主机别名:

# ~/.ssh/config
Host mac-build
HostName 203.0.113.10
User builduser
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30
ServerAliveCountMax 4
TCPKeepAlive yes

保活设置能减少在不稳定网络上的断连。请注意,macOS 原生的 OpenSSH 不支持 mosh,因此保活是您的第一道防线。

3. 连接​

打开命令面板(Ctrl/Cmd+Shift+P)并运行 Remote-SSH: Connect to Host,然后选择 mac-build。或者,一步运行它:

cursor --remote ssh-remote+mac-build /Users/builduser/projects/MyiOSApp

4. 窗口重新加载​

Cursor 会重新加载到一个远程窗口。状态栏会显示远程主机名。所有终端、任务和语言服务器现在都在 Mac 上执行,而 UI 和渲染则留在本地。

5. 扩展安装在远程端​

当您打开一个 Swift 文件时,Cursor 会提示您在远程端安装语言扩展。接受它。Swift 工具、调试适配器和 Swift 扩展都属于 Mac,而不是您的本地机器。

本地扩展与远程扩展

仅 UI 扩展(例如主题)在本地运行。会生成本进程的扩展(包括 Swift 语言服务器和任何调试适配器)在远程 Mac 上运行。如果您让它来管理,Cursor 会为您处理这种划分。

本地编辑、远程构建​

高效的开发模式是:在 Cursor 中编辑,在集成终端中运行构建,而集成终端就是 Mac 上的一个终端。文件始终位于 Mac 上,因此没有会漂移的同步步骤。

基本构建​

为通用的 iOS 目标构建一个 scheme,这样可以避免需要特定设备:

xcodebuild \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug \
-destination 'generic/platform=iOS' \
build

如果您使用工作区(workspace)而不是项目(project),请将 -project 换成 -workspace:

xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -destination 'generic/platform=iOS' build

为发布打归档​

归档会生成一个 .xcarchive,导出步骤会将其转换为用于分发的 .ipa:

xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath build/MyApp.xcarchive \
archive

导出归档​

归档之后,使用一个描述分发方式的导出选项 plist 进行导出:

xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build/export \
-exportOptionsPlist ExportOptions.plist

一个用于 App Store 风格分发的 ExportOptions.plist 大致如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>ABCDE12345</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
在 CI 中优先使用通用目标

为 generic/platform=iOS 构建可以避免将构建绑定到某一特定设备或模拟器 UDID,这使得同一条命令可以在任何构建机器上工作,而无需更改配置。

运行测试和读取日志​

测试的运行方式与构建相同,只是使用 test 操作和一个具体的目标。

运行测试​

xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'

使日志可读​

原始的 xcodebuild 输出非常庞大且难以扫读。将其通过一个格式化器进行管道处理,以将其折叠为可读的行:

# xcbeautify: modern, fast formatter
xcodebuild test -workspace MyApp.xcworkspace -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' | xcbeautify

# xcpretty: older but widely available alternative
xcodebuild test -workspace MyApp.xcworkspace -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' | xcpretty

要获得一个持久的日志文件外加一个可读的流,请对输出进行 tee:

xcodebuild build -scheme MyApp -destination 'generic/platform=iOS' \
2>&1 | tee build.log | xcbeautify

由于终端是远程的,日志文件会落在 Mac 上。请使用 Cursor 的文件资源管理器在同一个远程窗口中读取它,或者如果您愿意,用第二个 SSH 会话在本地流式读取它。

获取模拟器截图​

由于模拟器在 Mac 上运行,您要通过 SSH 使用 xcrun simctl 来捕获它,而不是在本地截屏。

列出并启动模拟器​

# List available devices
xcrun simctl list devices available

# Boot a specific simulator by UDID or name
xcrun simctl boot "iPhone 16"

# Open the Simulator UI if a desktop session is available
open -a Simulator

安装并启动您的应用​

# Install a built .app onto the booted simulator
xcrun simctl install booted ./build/Build/Products/Debug-iphonesimulator/MyApp.app

# Launch it by bundle identifier
xcrun simctl launch booted com.example.MyApp

捕获截图​

# Save a screenshot to a file on the Mac
xcrun simctl io booted screenshot /Users/builduser/shots/home.png

由于该文件在远程端,请直接在 Cursor 远程窗口中打开它。图像预览可通过 Remote-SSH 工作,因此您不需要将其复制下来。

无头模拟器需要一个会话

simctl 可以在没有 GUI 的情况下启动一个模拟器,但一些由 UI 驱动的测试和更丰富的截图场景需要一个已登录的桌面会话。在无头的 Mac 上,请保持一个持久的已登录会话,或者预期模拟器 UI 功能会受到限制。

代码签名​

签名是在一台全新机器上最可能失败的部分,因此请有意地设置它。

钥匙串中的证书​

签名身份位于构建用户的钥匙串中。导入一个从您 Apple 开发者账户导出的 .p12:

security import /path/to/distribution.p12 \
-k /Users/builduser/Library/Keychains/login.keychain-db \
-P "p12-password" \
-T /usr/bin/codesign \
-T /usr/bin/security

-T 标志授予 codesign 和 security 对导入身份的访问权限,从而避免构建因访问提示而停滞。

配置描述文件​

将描述文件放到构建用户预期 Xcode 能找到它们的位置:

mkdir -p /Users/builduser/Library/MobileDevice/Provisioning\ Profiles
cp ~/profiles/MyApp_AppStore.mobileprovision \
/Users/builduser/Library/MobileDevice/Provisioning\ Profiles/

自动签名与手动签名​

  • 自动签名:更简单,要求构建用户登录到一个 Apple 开发者账户,并能通过网络访问 Apple 的服务。适用于单台受信任的 Mac。
  • 手动签名:对 CI 更具可复现性。您指定 CODE_SIGN_STYLE=Manual 并指向特定的身份和描述文件,避免依赖账户的行为。
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'generic/platform=iOS' \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution: Example Corp (ABCDE12345)" \
PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore Profile" \
-archivePath build/MyApp.xcarchive
在 CI 中解锁钥匙串

在一台无人值守的 Mac 上,请在构建之前解锁登录钥匙串,这样 codesign 无需 GUI 提示即可访问该身份。将解锁密码保存在密钥存储中,绝不要放在已提交的脚本里。

成本比较​

价格会变化,因此请将以下内容视为用于规划的粗略范围,而非当前的报价。

选项大致每月成本说明
拥有一台 Mac mini 作为构建服务器硬件购置,之后经常性成本几乎为零稳定使用时最便宜;维护由您负责
云 Mac 提供商(MacStadium、EC2 Mac)按小时计费,24/7 使用时常为每月数百可向上或向下扩展;配置开销
云 CI macOS runner按分钟计费,仅在构建时计费偶尔构建时最便宜;冷启动有代价
rsync / git 到共享 Mac仅 Mac 的成本对个人没问题,对团队则不方便

对于全天都在构建的小型团队,一台专用的 Mac mini 通常胜出。当构建不频繁或严格发生在发布时,云 CI 胜出。当您需要按需容量而不想拥有硬件时,云 Mac 实例胜出。

常见陷阱​

大多数远程构建的痛苦来自一小组反复出现的错误。在调试任何奇特问题之前,请先检查这些。

许可证未被接受​

xcodebuild 会立即以许可证错误失败。用以下命令一次性修复它:

sudo xcodebuild -license accept

选择了错误的 Xcode 版本​

多个 Xcode 安装会让工具链混淆。请显式地选择想要的那一个:

sudo xcode-select -s /Applications/Xcode-16.app/Contents/Developer
xcodebuild -version

缺少签名身份​

一个能归档但导出失败的构建,通常是没有匹配的身份或描述文件。确认钥匙串实际持有什么:

security find-identity -v -p codesigning

如果列表中您需要的身份为空,请再次导入 .p12 并验证描述文件与该应用的 bundle identifier 匹配。

缓慢的同步​

如果您是在同步代码而不是通过 SSH 编辑,请优先使用 git 而非 rsync。Git 传输增量并为您提供历史记录和冲突处理;rsync 移动整棵树并无声地漂移。当您直接通过 Remote-SSH 编辑时,完全没有同步,这是最快的方式。

网络中断破坏 SSH​

在一条不稳定的链路上进行长时间构建可能会掉线并终止构建。通过在配置中设置 SSH 保活来缓解,并在 Mac 上于 tmux 或 screen 内运行长时间构建,以便它们在断连后仍然存活:

tmux new-session -s build 'xcodebuild archive -workspace MyApp.xcworkspace -scheme MyApp -destination "generic/platform=iOS" -archivePath build/MyApp.xcarchive'

稍后重新连接,并用 tmux attach -t build 重新附加。

模拟器无法无头运行​

一些测试流程假定有一个可见的模拟器。在无头的 Mac 上,用 simctl 启动模拟器,并要么保持一个已登录的桌面会话,要么接受模拟器 UI 覆盖范围减少。

从第一天起就将构建包裹在 tmux 中

如果您在一个您不完全控制的网络上构建,请把每一个长时间的 xcodebuild 都放在 tmux 内运行。无论您的 SSH 会话是否存活,构建都会在 Mac 上继续运行,而您可以重新附加以观察它完成。

远程构建将一个硬性的平台约束转变为一个工作流选择。挑选适合您团队的架构,一次性设置好 Mac,并让 Cursor 的 Remote-SSH 保持编辑体验快速,同时让 Mac 完成 macOS 所要求的工作。