把公钥放到远端,再改密码
在远端创建 ~/.ssh,目录权限设为 700,公钥文件权限设为 600。用编辑器追加公钥,保留已有授权。随后运行 passwd 修改当前用户密码。
另开终端,用 ssh -i ~/.ssh/osxhost_build -p "$MAC_PORT" "$MAC_USER@$MAC_HOST" 验证成功后,再结束原会话。不要先关闭唯一可用的连接。
从连接到第一份构建产物
把前半小时留给连通、工具链检查与首次编译,再用后半小时整理签名和流水线。你拿到的是独享物理 Mac,不是虚拟机;原有的 macOS 图形界面与命令行工作流可以继续使用。
已下单?先进入控制台读取实际主机地址、端口、用户名与交付信息。不要把示例变量当成连接凭据。
核对主机指纹,建立 SSH 连接。
确认 Xcode、项目依赖与磁盘空间。
先跑无签名检查,再验证 archive。
第一步 · 先验证连接
先在本地生成密钥,只上传公钥。私钥保留在自己的设备上并设置口令。以下命令中的主机、端口和用户名均以控制台实际交付信息为准。
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,再从另一个本地终端验证密钥登录。
$MacHost = Read-Host "主机地址"
$MacPort = Read-Host "SSH 端口"
$MacUser = Read-Host "用户名"
ssh-keygen -t ed25519 -f "$HOME/.ssh/osxhost_build"
Get-Content "$HOME/.ssh/osxhost_build.pub"
ssh -p $MacPort "$MacUser@$MacHost"
使用 Windows 的 OpenSSH 客户端。若系统提示找不到 ssh,先在系统可选功能中启用 OpenSSH 客户端,再重新打开 PowerShell。
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"
使用发行版提供的 OpenSSH 客户端。先核对主机指纹,不要为绕过提示而关闭主机密钥检查;系统重装后指纹变化也需要重新核实。
在远端创建 ~/.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 -N -L 5901:127.0.0.1:5900 \
-i "$HOME/.ssh/osxhost_build" \
-p "$MAC_PORT" "$MAC_USER@$MAC_HOST"
第二步 · 本地 Mac → 云端 Mac
先完成数据与工具链验收,再注册 CI 执行器。把“能登录”与“能无人值守构建”分开检查,避免把本地交互式配置误当成流水线配置。
.p12,密码通过独立安全渠道传递。通过条件:工程版本一致,证书含私钥,敏感文件未进入 Git。
xcodes list 查看版本,在完成所需授权后安装目标 Xcode。xcode-select 明确选择当前工具链,不依赖图形界面的最近选择。通过条件:终端和 CI 中的 Xcode、Ruby、Bundler 版本一致。
通过条件:推送一次受信任提交,可回收日志与构建产物。
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/"
这个同步命令不删除远端文件。正式流水线优先从版本库检出固定提交,缓存键至少包含工具链版本、依赖锁文件与目标架构。不要允许不受信任的外部合并请求读取签名凭据。
第三步 · 签名与产物分开验收
先在工程根目录运行 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,再执行完整流水线。
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
bundle exec fastlane ios release
find build/artifacts -maxdepth 1 -type f -print
成功上传不等于处理完成。这份示例跳过上传后的处理等待。请在分发后台确认构建处理结果,并让每次上传使用唯一的构建号。readonly: true 会阻止 CI 修改签名仓库;证书或描述文件到期时,应在受控环境更新后重新运行。
跨平台工程 · 保留锁文件
Flutter 和 React Native 的 iOS 阶段都依赖 Xcode、CocoaPods 与签名环境。先通过平台诊断,再接入上传步骤。不要因为缓存命中就跳过依赖一致性检查。
固定 Flutter SDK 版本并保留 pubspec.lock。复用 Pub 下载缓存;Pods 以 Podfile.lock 为键,DerivedData 以 Xcode 版本和工程配置为键。首次缓存为空时,下载与编译会比后续运行更久。
在工程根目录运行下面的命令。签名配置通过后,flutter build ipa --release 才能完成真机构建导出。
提交 JavaScript 包管理器锁文件,按项目约定使用对应安装命令。下面以 npm 为例;Ruby 依赖由 Bundler 管理,避免系统级 gem 与项目版本相互覆盖。
仅在新增依赖确实需要更新索引时,才使用 pod install --repo-update。日常执行 bundle exec pod install,不要每次删除 Pods 和锁文件。
flutter --version
flutter doctor -v
flutter pub get
flutter build ios --simulator
flutter build ipa --release
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 的工程中执行下面的测试。首次先跑一个目标,再增加到两个,观察内存压力与磁盘写入。
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 构建分工
节点本身是独享物理 Mac。Docker Desktop 或 OrbStack 提供的 Linux 容器环境,仍通过 Linux 运行环境工作;不要把“物理节点”理解为容器与 macOS 共用同一个内核。
| 任务 | 建议运行位置 | 需要注意 |
|---|---|---|
| 测试数据库、缓存与内部 API | Linux 容器 | 优先选择 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 并联是单独的配置项。实际设备名称、挂载路径、接口支持与连接拓扑以订单交付信息为准;没有核对清楚之前,不执行格式化或分区命令。
使用 diskutil list external 识别外接设备,再查看具体卷的信息。若卷已格式化但未挂载,确认标识后执行 diskutil mount "$DISK_IDENTIFIER"。不要凭磁盘编号推断它一定是附加 SSD。
建议放置依赖缓存、DerivedData 和暂存产物。将实际路径写入 CI 变量 BUILD_CACHE_ROOT,检查构建用户可写,并验证重启后路径仍有效。独立保留源代码与签名材料的备份。
并联不会把多台机器的 CPU 或统一内存自动合并成一个进程可用的资源池。需要由应用或 CI 调度层拆分任务,每台机器独立配置工具链、执行器与工作目录。
若交付方案使用 Thunderbolt 网络连接,应逐台确认网络接口、地址与路由,再验证点对点访问。采购前通过工单说明台数、机型与传输用途,不要自行假定所有机型接口能力一致。
分布式任务需要应用自身支持diskutil list external
ls -la /Volumes
df -h
system_profiler SPThunderboltDataType
networksetup -listallhardwareports
扩展项目的四种租期价格见方案页。已有订单需要变更时,先提交工单确认实施方式与数据迁移安排,不要直接在生产构建期间改动磁盘结构。
最后一步 · 带着证据定位问题
记录订单号、节点、发生时间及其时区、最近一次正常构建的提交号。保留首个报错和退出码,不要只提供日志最后一行。下面的命令用于缩小范围,不会自动修复所有问题。
先核对地址、端口、用户名和私钥路径。连接超时通常先查网络可达性;认证失败则先查用户名、公钥授权和权限。主机指纹变化应先通过工单核实,不能直接忽略。
ssh -vv -o ConnectTimeout=10 \
-i "$HOME/.ssh/osxhost_build" \
-p "$MAC_PORT" "$MAC_USER@$MAC_HOST"
检查证书是否含私钥、是否过期、描述文件是否与应用和团队一致。CI 专用钥匙串还需要在构建会话中可访问。先在受控环境修复签名资产,再让只读 match 同步。
security find-identity -v -p codesigning
security list-keychains -d user
bundle exec fastlane match appstore --readonly
先区分系统卷与附加卷,定位 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
先看索引进程是否仍在工作,再核对依赖安装与磁盘剩余空间。图形界面的索引和命令行构建不是同一个完成条件;可用日志确认 archive 是否确实被阻塞。
ps -axo pid,%cpu,%mem,etime,comm | grep -E 'Xcode|SourceKit|swift'
xcode-select -p
xcodebuild -list -workspace "$IOS_WORKSPACE"
df -h "$HOME"
工单里不要附私钥、证书密码、访问令牌或完整签名仓库内容。提交前遮盖日志中的凭据与敏感路径。无法登录控制台时,发邮件至 support@osxhost.com,说明订单号与故障现象;邮件和控制台工单是公开支持渠道。
把工具链版本、签名注入、缓存键与产物目录写进流水线。需要机器就下单,卡在某一步就带诊断结果提交工单。