SureVM 工程笔记

云端 Mac CI 临时目录隔离:治理 TMPDIR、缓存与退出清理

云端 Mac CI 临时目录隔离:治理 TMPDIR、缓存与退出清理

同一台云端 Mac 连续执行多个 CI 任务时,最难复现的故障往往不是代码错误,而是上一个任务留下的临时文件。旧套接字、未释放的锁、半写入缓存或重名导出目录,都可能让下一次构建在相同提交上得到不同结果。独享物理节点避免了其他账户干扰,却不会自动隔离同一账户内的并发任务,因此文件系统边界仍要由流水线明确建立。

先识别会跨任务泄漏的目录

不要把“工作区已清空”等同于环境已恢复。macOS 工具会把状态分散到工作区之外,排查时至少检查以下四类位置:

先在一次正常构建前后分别执行 env | sort,记录实际使用的 HOMETMPDIR 和工作区路径。再用 du -sh 比较相关目录体积,而不是一开始就递归删除。若失败只在并发时出现,应优先搜索固定文件名、固定端口和固定输出路径。

隔离的目标不是每次删除所有缓存,而是让任务明确拥有自己的可写目录,并且只清理自己创建的内容。

为每个任务创建唯一根目录

任务标识应来自 CI 系统提供的唯一编号;没有可靠编号时,可以由 mktemp 生成。不要仅使用分支名,因为同一分支可能同时触发多次构建。

#!/bin/bash
set -euo pipefail

job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"

export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"

mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"

cleanup() {
  local status=$?
  rm -rf "$job_root"
  exit "$status"
}

trap cleanup EXIT INT TERM

mktemp 创建的目录默认不会与另一个任务重名。清理函数先保存退出码,再删除目录并用原状态退出,避免测试失败被清理命令掩盖。路径变量要加引号,防止工作区名称包含空格时被拆成多个参数。

不要盲目覆盖 HOME

HOME 指向任务目录看似隔离彻底,但可能切断当前用户的 Keychain、工具偏好和已完成的系统初始化。普通编译可为特定包管理工具指定独立缓存;涉及签名时,通常保留 HOME,只隔离构建产物和明确可迁移的缓存。

把 Xcode 的写入路径显式接入

仅设置 TMPDIR 不会自动迁移 DerivedData。构建命令应把关键路径作为参数传入,确保日志能够直接显示本次任务使用了哪个目录。

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
  COMPILER_INDEX_STORE_ENABLE=NO \
  CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
  test \
  -resultBundlePath "$RESULT_BUNDLE"

CI 不需要代码索引时,可关闭索引存储以减少无关写入。测试结果应放在任务根目录内,但失败后若要上传附件,必须先完成归档,再触发清理。更稳妥的流程是“执行测试—复制诊断文件到流水线产物区—校验复制结果—退出”。

共享缓存只读,任务缓存可写

确实需要复用依赖时,可把经过校验的共享缓存视为种子:任务开始时复制或恢复到自己的目录,构建过程只写任务副本,成功后再由单独步骤更新共享版本。不要让两个 xcodebuild 进程同时写同一模块缓存或构建数据库。

让异常退出也能留下证据

直接使用 trap 'rm -rf ...' EXIT 会在失败瞬间删除现场。实践中应先生成一份小型诊断清单,包括退出码、磁盘余量、目录大小和未结束的相关进程,再决定保留哪些文件。日志中不要输出环境变量全集,因为其中可能含令牌或签名相关值。

可以在清理前执行:

{
  echo "exit_status=$status"
  df -h "$job_root"
  du -sh "$job_root"/* 2>/dev/null || true
  find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"

ARTIFACT_DIR 应位于任务根目录之外,并由流水线负责上传和定期清理。若需要保留失败现场,可将诊断目录压缩后再删除原目录,且要对日志和路径做脱敏。

用残留审计验证隔离是否生效

改造完成后,连续运行两次相同提交,并在第二次开始前确认不存在上一任务标识对应的目录。并发运行时,再检查两个任务的 TMPDIR、DerivedData 和结果包路径是否完全不同。

验收清单可以固定为五项:每个任务拥有唯一根目录;Xcode 路径全部显式指定;共享缓存没有并发写入;成功与失败都执行退出钩子;诊断产物先复制、后清理。若磁盘仍持续增长,可按任务标识反查未纳入边界的工具目录,而不是扩大删除范围。

在 SureVM 的独享物理 Mac 上,这套方法尤其适合长期运行的构建节点:保留机器级工具链,同时把每次任务的可变状态限制在可追踪、可删除的目录中。最终判断标准很简单——前一个任务无论成功、失败或被中止,都不应改变下一个任务的输入条件。

常见问题

为什么不能让所有 CI 任务共用系统临时目录?

系统临时目录可以作为上级目录,但每个任务应创建独立子目录。直接共用固定路径会让并发任务覆盖文件,也会让失败任务留下的套接字、锁文件和中间产物影响后续构建。

隔离 TMPDIR 后还需要单独设置 DerivedData 吗?

需要。Xcode 的 DerivedData、模块缓存和临时文件并不完全使用同一位置。应通过命令行参数或环境变量分别指定目录,才能形成完整的任务边界。

签名任务是否应该覆盖 HOME?

通常不应直接覆盖。签名流程可能依赖当前用户的 Keychain 与既有安全配置,更稳妥的做法是只隔离临时目录、构建目录和明确可迁移的工具缓存。

SureVM 云端 Mac

为构建、开发与实验选择独享物理 Mac 节点

比较两档 Mac mini M4 配置、四种租期和五个可订购节点,再按实际工作流完成选择。

选择租用方案