从连接到第一份构建产物

拿到机器后的
第一个小时

把前半小时留给连通、工具链检查与首次编译,再用后半小时整理签名和流水线。你拿到的是独享物理 Mac,不是虚拟机;原有的 macOS 图形界面与命令行工作流可以继续使用。

已下单?先进入控制台读取实际主机地址、端口、用户名与交付信息。不要把示例变量当成连接凭据。

00–10 分钟

核对主机指纹,建立 SSH 连接。

10–20 分钟

确认 Xcode、项目依赖与磁盘空间。

20–30 分钟

先跑无签名检查,再验证 archive。

第一步 · 先验证连接

用 SSH 处理命令,用屏幕共享处理界面

先在本地生成密钥,只上传公钥。私钥保留在自己的设备上并设置口令。以下命令中的主机、端口和用户名均以控制台实际交付信息为准。

在本地 macOS 终端执行
read -r -p "主机地址: " MAC_HOST
read -r -p "SSH 端口: " MAC_PORT
read -r -p "用户名: " MAC_USER
ssh-keygen -t ed25519 -f "$HOME/.ssh/osxhost_build"
cat "$HOME/.ssh/osxhost_build.pub"
ssh -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"

公钥文件以 .pub 结尾。首次通过交付的登录方式连接后,将公钥追加到远端 ~/.ssh/authorized_keys,再从另一个本地终端验证密钥登录。

把公钥放到远端,再改密码

在远端创建 ~/.ssh,目录权限设为 700,公钥文件权限设为 600。用编辑器追加公钥,保留已有授权。随后运行 passwd 修改当前用户密码。

另开终端,用 ssh -i ~/.ssh/osxhost_build -p "$MAC_PORT" "$MAC_USER@$MAC_HOST" 验证成功后,再结束原会话。不要先关闭唯一可用的连接。

验收:新会话可用密钥登录

通过加密隧道使用屏幕共享

在本地运行下面的隧道命令并保持终端开启。macOS 打开“屏幕共享”,连接 vnc://127.0.0.1:5901;Windows 与 Linux 使用支持对应 VNC 认证方式的客户端,连接本地端口 5901。

远端屏幕共享须已启用并授权当前用户。图形登录凭据与 SSH 私钥是两种认证材料;不要把 VNC 端口直接开放给所有来源。

验收:能看到并操作 macOS 桌面
本地 SSH 隧道 · 目标是远端回环地址
ssh -N -L 5901:127.0.0.1:5900 \
  -i "$HOME/.ssh/osxhost_build" \
  -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"

第二步 · 本地 Mac → 云端 Mac

分三条路径迁移,不复制整套旧环境

先完成数据与工具链验收,再注册 CI 执行器。把“能登录”与“能无人值守构建”分开检查,避免把本地交互式配置误当成流水线配置。

路径 01 · 数据

搬工程,不搬明文秘密

  • 从钥匙串导出所需签名证书与私钥为加密 .p12,密码通过独立安全渠道传递。
  • 只迁移当前项目需要的描述文件,检查团队、应用标识与有效期。
  • 不要复制整个登录钥匙串。创建专用构建钥匙串,按需导入并授权签名工具。
  • 同步源代码与必要资源,排除依赖目录、DerivedData 和旧构建产物。

通过条件:工程版本一致,证书含私钥,敏感文件未进入 Git。

路径 02 · 工具链

锁版本,再安装依赖

  • 用 xcodes list 查看版本,在完成所需授权后安装目标 Xcode。
  • 用 xcode-select 明确选择当前工具链,不依赖图形界面的最近选择。
  • 确认 Homebrew 与 arm64 工具链,避免把 Intel 环境的依赖目录直接搬过来。
  • 通过 Ruby 版本管理工具和 Gemfile 固定 Ruby、CocoaPods 与 fastlane。

通过条件:终端和 CI 中的 Xcode、Ruby、Bundler 版本一致。

路径 03 · CI 接入

让执行器只接可信任务

  • GitHub Actions:按仓库设置注册 self-hosted runner,选择 macOS ARM64 包。
  • GitLab Runner:注册项目级执行器,按需求选择 shell 执行方式并限制项目范围。
  • Jenkins:创建 macOS agent,使用隔离工作目录和受控的凭据注入。
  • 先以单任务并发跑通签名构建,再根据峰值内存增加并发。

通过条件:推送一次受信任提交,可回收日志与构建产物。

在本地同步工程 · 保留远端已有文件
rsync -avz \
  --exclude='.git/' \
  --exclude='build/' \
  --exclude='DerivedData/' \
  --exclude='Pods/' \
  --exclude='node_modules/' \
  -e "ssh -p $MAC_PORT -i $HOME/.ssh/osxhost_build" \
  ./ "$MAC_USER@$MAC_HOST:~/build-workspace/"

这个同步命令不删除远端文件。正式流水线优先从版本库检出固定提交,缓存键至少包含工具链版本、依赖锁文件与目标架构。不要允许不受信任的外部合并请求读取签名凭据。

第三步 · 签名与产物分开验收

用一份 Fastfile 串起 match、gym 与 pilot

先在工程根目录运行 bundle install,并提交 Gemfile.lock。下面的流程假定签名仓库已经初始化、应用已建立,且 CI 已安全注入环境变量。它不负责创建新的签名资产。

远端预检 · 先确认工具链
xcode-select -p
xcodebuild -version
xcodebuild -showsdks
ruby --version
bundle --version
bundle exec fastlane --version
security find-identity -v -p codesigning
df -h "$HOME"

将签名仓库地址放入 MATCH_GIT_URL,解密口令放入 MATCH_PASSWORD。上传凭据使用服务端 API 密钥:密钥文件路径由 ASC_KEY_PATH 提供,密钥标识与发行者标识分别由 ASC_KEY_ID、ASC_ISSUER_ID 提供。密钥文件应由 CI 秘密存储在运行时落盘,不提交到工程仓库。

设置 IOS_APP_IDENTIFIER、IOS_TEAM_ID、IOS_WORKSPACE 与 IOS_SCHEME。签名仓库中的分发类型需与 appstore 匹配;先验证共享 scheme,再执行完整流水线。

fastlane/Fastfile · 单个应用的 TestFlight 流水线
default_platform(:ios)

platform :ios do
  desc "Archive a signed build and upload it to TestFlight"
  lane :release do
    setup_ci

    api_key = app_store_connect_api_key(
      key_id: ENV.fetch("ASC_KEY_ID"),
      issuer_id: ENV.fetch("ASC_ISSUER_ID"),
      key_filepath: ENV.fetch("ASC_KEY_PATH"),
      in_house: false
    )

    match(
      type: "appstore",
      readonly: true,
      git_url: ENV.fetch("MATCH_GIT_URL"),
      app_identifier: ENV.fetch("IOS_APP_IDENTIFIER"),
      team_id: ENV.fetch("IOS_TEAM_ID"),
      api_key: api_key
    )

    gym(
      workspace: ENV.fetch("IOS_WORKSPACE"),
      scheme: ENV.fetch("IOS_SCHEME"),
      configuration: "Release",
      clean: true,
      export_method: "app-store",
      output_directory: "build/artifacts",
      output_name: "release.ipa"
    )

    pilot(
      api_key: api_key,
      ipa: "build/artifacts/release.ipa",
      skip_waiting_for_build_processing: true
    )
  end
end
执行与确认 · 环境变量由 CI 注入
bundle exec fastlane ios release
find build/artifacts -maxdepth 1 -type f -print

成功上传不等于处理完成。这份示例跳过上传后的处理等待。请在分发后台确认构建处理结果,并让每次上传使用唯一的构建号。readonly: true 会阻止 CI 修改签名仓库;证书或描述文件到期时,应在受控环境更新后重新运行。

跨平台工程 · 保留锁文件

让 iOS 段独立运行,让缓存可失效

Flutter 和 React Native 的 iOS 阶段都依赖 Xcode、CocoaPods 与签名环境。先通过平台诊断,再接入上传步骤。不要因为缓存命中就跳过依赖一致性检查。

Flutter:先跑通模拟器,再导出 IPA

固定 Flutter SDK 版本并保留 pubspec.lock。复用 Pub 下载缓存;Pods 以 Podfile.lock 为键,DerivedData 以 Xcode 版本和工程配置为键。首次缓存为空时,下载与编译会比后续运行更久。

在工程根目录运行下面的命令。签名配置通过后,flutter build ipa --release 才能完成真机构建导出。

不要跨 Xcode 大版本复用构建缓存

React Native:锁定 Node、Ruby 与 Pods

提交 JavaScript 包管理器锁文件,按项目约定使用对应安装命令。下面以 npm 为例;Ruby 依赖由 Bundler 管理,避免系统级 gem 与项目版本相互覆盖。

仅在新增依赖确实需要更新索引时,才使用 pod install --repo-update。日常执行 bundle exec pod install,不要每次删除 Pods 和锁文件。

缓存键包含架构与依赖锁文件
Flutter 工程根目录
flutter --version
flutter doctor -v
flutter pub get
flutter build ios --simulator
flutter build ipa --release
React Native 工程根目录 · 使用 npm 的项目
node --version
npm ci
bundle install
(cd ios && bundle exec pod install)
xcrun simctl list devices available

并行模拟器测试要有上限。从可用设备列表获取两个实际 UDID,设置为 SIM_UDID_A、SIM_UDID_B;在已配置 IOS_WORKSPACE、IOS_SCHEME 的工程中执行下面的测试。首次先跑一个目标,再增加到两个,观察内存压力与磁盘写入。

Xcode 测试 · 使用工程支持的两个模拟器目标
xcodebuild test \
  -workspace "$IOS_WORKSPACE" \
  -scheme "$IOS_SCHEME" \
  -destination "platform=iOS Simulator,id=$SIM_UDID_A" \
  -destination "platform=iOS Simulator,id=$SIM_UDID_B" \
  -parallel-testing-enabled YES \
  -maximum-concurrent-test-simulator-destinations 2 \
  -resultBundlePath "build/TestResults-$(date +%s).xcresult"

容器边界 · Linux 服务与 macOS 构建分工

容器可以跑配套服务,不能替代 Xcode 主机

节点本身是独享物理 Mac。Docker Desktop 或 OrbStack 提供的 Linux 容器环境,仍通过 Linux 运行环境工作;不要把“物理节点”理解为容器与 macOS 共用同一个内核。

任务建议运行位置需要注意
测试数据库、缓存与内部 APILinux 容器优先选择 arm64 镜像,限制内存与监听范围。
Xcode archive 与模拟器测试macOS 宿主系统依赖 macOS SDK、签名工具与模拟器运行时。
仅提供 amd64 镜像的服务先做兼容性验证转译可能增加开销,不能直接套用 arm64 性能预期。
长期运行的配套服务按资源隔离需求决定与构建共享物理内存、SSD 和网络,需限制资源并保留备份。
安装并启动所选容器工具后检查
uname -m
docker version
docker info --format '{{.Architecture}}'
docker system df
docker stats --no-stream

任选 Docker Desktop 或 OrbStack 中的一套环境管理容器,避免不清楚当前 Docker context 指向哪里。增加服务前先记录空闲内存与磁盘空间;不要把清理容器数据当作默认排障动作。

扩展能力 · 先识别设备再写入

把缓存移到扩展盘,不把多机当成一台

附加 SSD 和 Thunderbolt 5 并联是单独的配置项。实际设备名称、挂载路径、接口支持与连接拓扑以订单交付信息为准;没有核对清楚之前,不执行格式化或分区命令。

附加 SSD:确认挂载点与权限

使用 diskutil list external 识别外接设备,再查看具体卷的信息。若卷已格式化但未挂载,确认标识后执行 diskutil mount "$DISK_IDENTIFIER"。不要凭磁盘编号推断它一定是附加 SSD。

建议放置依赖缓存、DerivedData 和暂存产物。将实际路径写入 CI 变量 BUILD_CACHE_ROOT,检查构建用户可写,并验证重启后路径仍有效。独立保留源代码与签名材料的备份。

扩容不等于备份

TB5 并联:逐台检查连接拓扑

并联不会把多台机器的 CPU 或统一内存自动合并成一个进程可用的资源池。需要由应用或 CI 调度层拆分任务,每台机器独立配置工具链、执行器与工作目录。

若交付方案使用 Thunderbolt 网络连接,应逐台确认网络接口、地址与路由,再验证点对点访问。采购前通过工单说明台数、机型与传输用途,不要自行假定所有机型接口能力一致。

分布式任务需要应用自身支持
远端只读检查 · 不修改磁盘与网络
diskutil list external
ls -la /Volumes
df -h
system_profiler SPThunderboltDataType
networksetup -listallhardwareports

扩展项目的四种租期价格见方案页。已有订单需要变更时,先提交工单确认实施方式与数据迁移安排,不要直接在生产构建期间改动磁盘结构。

最后一步 · 带着证据定位问题

先收集最小诊断信息,再改配置

记录订单号、节点、发生时间及其时区、最近一次正常构建的提交号。保留首个报错和退出码,不要只提供日志最后一行。下面的命令用于缩小范围,不会自动修复所有问题。

01

SSH 连不上

先核对地址、端口、用户名和私钥路径。连接超时通常先查网络可达性;认证失败则先查用户名、公钥授权和权限。主机指纹变化应先通过工单核实,不能直接忽略。

本地终端 · 输出中可能包含主机信息
ssh -vv -o ConnectTimeout=10 \
  -i "$HOME/.ssh/osxhost_build" \
  -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"
提交连接工单
02

证书失效或找不到签名身份

检查证书是否含私钥、是否过期、描述文件是否与应用和团队一致。CI 专用钥匙串还需要在构建会话中可访问。先在受控环境修复签名资产,再让只读 match 同步。

远端终端 · 查看签名身份与钥匙串列表
security find-identity -v -p codesigning
security list-keychains -d user
bundle exec fastlane match appstore --readonly
提交签名工单
03

磁盘满,archive 无法写入

先区分系统卷与附加卷,定位 DerivedData、模拟器与构建产物占用。仅清理已确认可重建的缓存;不要批量删除钥匙串、签名文件或正在使用的工作目录。

远端终端 · 只查看占用
df -h
du -sh "$HOME/Library/Developer/Xcode/DerivedData" \
  "$HOME/Library/Developer/CoreSimulator" \
  "$HOME/build-workspace" 2>/dev/null
xcrun simctl list devices unavailable
提交磁盘工单
04

Xcode 长时间停在 indexing

先看索引进程是否仍在工作,再核对依赖安装与磁盘剩余空间。图形界面的索引和命令行构建不是同一个完成条件;可用日志确认 archive 是否确实被阻塞。

远端终端 · 查看进程与构建配置
ps -axo pid,%cpu,%mem,etime,comm | grep -E 'Xcode|SourceKit|swift'
xcode-select -p
xcodebuild -list -workspace "$IOS_WORKSPACE"
df -h "$HOME"
提交 Xcode 工单

工单里不要附私钥、证书密码、访问令牌或完整签名仓库内容。提交前遮盖日志中的凭据与敏感路径。无法登录控制台时,发邮件至 support@osxhost.com,说明订单号与故障现象;邮件和控制台工单是公开支持渠道。

先跑通一份 archive,再增加并发

把工具链版本、签名注入、缓存键与产物目录写进流水线。需要机器就下单,卡在某一步就带诊断结果提交工单。