同一个 Xcode 工程在长期使用的开发目录里可以编译,复制到新的 VPSGit 云端 Mac 工作区后却提示文件不存在、脚本找不到或配置值为空。此时不要先搬运旧缓存。真正需要验证的是:一次构建能否只依靠指定提交、明确声明的工具链和流水线注入的参数完成。
干净检出要隔离哪些状态
删除 DerivedData 只能排除一类缓存,不能证明工程没有本机依赖。一个长期使用的工作区通常还叠加了未提交文件、用户目录配置、全局安装工具、Shell 初始化脚本以及仓库外的绝对路径。
至少需要隔离以下五层:
- 使用独立克隆,而不是在原目录执行清理。
- 为任务创建临时
HOME和TMPDIR。 - 为 DerivedData 指定任务专属目录。
- 固定
DEVELOPER_DIR,避免默认 Xcode 版本漂移。 - 只向构建进程传入白名单环境变量。
干净检出验收的目标不是让构建“多清理一次”,而是证明任何没有进入版本库、依赖清单或流水线参数的状态都不会被悄悄使用。
首次排查时应保留一组对照:原工作区构建成功,隔离工作区构建失败。两者使用同一提交、Scheme、Configuration 和 Destination,这样失败差异才有诊断价值。
建立一次性隔离工作区
下面的脚本从现有仓库创建非本地硬链接克隆,检出指定提交,并把用户目录、临时目录和构建缓存都放进一次性目录。项目名和 Scheme 通过参数传入,避免把环境差异写死在脚本里。
#!/bin/bash
set -euo pipefail
SOURCE_REPO="${1:?source repository required}"
REVISION="${2:?revision required}"
PROJECT="${3:?project path required}"
SCHEME="${4:?scheme required}"
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/clean-checkout.XXXXXX")"
trap 'rm -rf "$ROOT"' EXIT
git clone --no-local --no-checkout "$SOURCE_REPO" "$ROOT/repo"
git -C "$ROOT/repo" checkout --detach "$REVISION"
git -C "$ROOT/repo" submodule update --init --recursive
mkdir -p "$ROOT/home" "$ROOT/tmp" "$ROOT/DerivedData"
export HOME="$ROOT/home"
export TMPDIR="$ROOT/tmp"
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
cd "$ROOT/repo"
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration Debug \
-derivedDataPath "$ROOT/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
build | tee "$ROOT/build.log"
CODE_SIGNING_ALLOWED=NO 只适合验证不需要签名的编译阶段。如果目标必须完成归档或签名,应由 CI 安全注入所需材料,不要为了让脚本通过而引用个人用户目录中的文件。
让依赖缺失尽早暴露
克隆完成后先执行 git status --porcelain,结果应为空。再检查子模块状态和大文件对象是否完整。若工程包含代码生成步骤,应明确区分两类产物:生成器源码与版本必须被锁定;生成结果则按团队约定提交,或在构建中稳定地产生。
不要把 ~/bin、/usr/local/bin 或某个成员的桌面目录加入补救路径。辅助工具应进入仓库工具目录、受版本约束的包管理清单,或由节点初始化流程按固定版本安装。
从失败日志定位隐性输入
隔离构建失败后,先找第一条真正的错误,而不是日志末尾的汇总。常见信号可以按输入类型分类:
| 失败信号 | 常见原因 | 修复方向 |
|---|---|---|
No such file or directory |
未提交文件或绝对路径 | 将文件纳入仓库,改用仓库相对路径 |
command not found |
依赖交互式 Shell 或全局工具 | 声明工具版本并显式设置 PATH |
| 配置值为空 | 参数只存在于本机配置 | 由 CI 注入,并在缺失时立即失败 |
| 模块在旧目录可见 | 共享缓存掩盖依赖缺失 | 修复依赖声明,不复制旧缓存 |
| 脚本权限错误 | 可执行位未进入提交 | 修正文件模式并重新提交 |
可用下面的命令检查当前提交是否真的包含目标文件及其权限:
git ls-tree -r HEAD -- path/to/file
git status --short
git diff --summary HEAD
若错误路径指向 /Users/某个名字/,通常是工程配置、脚本或生成文件记录了绝对路径。应回到产生该路径的源配置修复,而不是在新节点创建同名目录。
收紧环境而不制造假失败
直接使用 env -i 能快速发现环境依赖,但也可能移除构建必需的 PATH、区域设置和临时目录。更稳妥的做法是先记录当前构建实际读取的变量,再建立白名单。
建议保留 HOME、TMPDIR、PATH、DEVELOPER_DIR、LANG 和流水线明确注入的业务参数。对每个自定义变量使用必填检查,例如:
: "${API_BASE_URL:?API_BASE_URL must be provided by CI}"
: "${BUILD_FLAVOR:?BUILD_FLAVOR must be provided by CI}"
这样缺失配置会在脚本入口失败,不会等到编译后段才出现含糊错误。敏感值只判断是否存在,不要回显到日志。构建脚本也不应读取交互式 Shell 配置,因为非交互任务可能根本不会加载这些文件。
把验收接入日常流水线
完整独立克隆会增加磁盘和检出时间,不必阻塞每一次小提交。可以拆成两级:普通任务使用复用工作区完成快速反馈;干净检出任务在合并主要分支、依赖或 Xcode 变更、发布前运行。
验收通过应同时满足:
- 指定提交能在独立克隆中检出完整。
- 工作树在构建前后都没有意外修改。
- 构建不读取原工作区和个人用户目录。
- 所有辅助工具都有可追踪的版本来源。
- 清空 DerivedData 后仍能得到预期产物。
- 失败日志和环境摘要可以用于复现,但不包含敏感值。
当干净检出失败而普通构建成功时,应把它视为依赖声明缺陷。先修正输入边界,再讨论缓存恢复。持续保留这道验收,能够让新节点接管、并行扩容和工具链升级从“换机器试运气”变成可重复的工程流程。
常见问题
为什么不能只执行 git clean 后重新构建?
git clean 只能处理当前工作区中的未跟踪文件,无法隔离用户目录配置、全局工具、Keychain、共享缓存和当前仓库之外的绝对路径依赖。独立克隆与隔离 HOME 才能覆盖这些变量。
干净检出构建失败后应先检查什么?
先比较失败命令的输入路径和环境变量,再检查子模块、大文件对象、生成代码、配置文件及脚本工具是否已在仓库或锁定清单中声明,不要先复制旧缓存掩盖问题。
这项验收需要每次提交都运行吗?
不必占用每次提交的快速通道。可在合并主分支、工具链升级、依赖变更和发布前运行,并保留一个较短的日常冒烟版本。
为下一次构建或推理任务配置云端 Mac
从三档 Apple Silicon 配置、六个节点和固定租期中选择,实际可用状态以控制台实时返回为准。