VMArm 技术记录

云端 Mac 上隔离 React Native 的 Metro 与 Watchman 缓存

云端 Mac 上隔离 React Native 的 Metro 与 Watchman 缓存

同一台云端 Mac 连续执行 React Native 构建时,最难查的往往不是编译错误,而是“代码已经改了,产物却像没改”。本地重跑正常,持续集成任务偶尔引用旧模块;删除整个仓库后问题暂时消失,过几天又出现。根因通常在四层状态没有分开:Git 工作区、Watchman 监听、Metro 转换缓存和 Xcode DerivedData。

先确认污染发生在哪一层

不要一遇到异常就删除所有缓存。全量清理会掩盖故障来源,也会让后续构建变慢。先用一个最小改动判断边界,例如修改只会进入开发包的字符串,再分别检查文件内容、Metro 模块图和最终应用产物。

建立任务证据

每次任务至少记录提交哈希、工作区绝对路径、Node 与 Xcode 版本、启动 Metro 的命令,以及 DerivedData 路径。还应保存以下命令输出:

set -euo pipefail

printf 'commit=%s\n' "$(git rev-parse HEAD)"
printf 'workspace=%s\n' "$PWD"
node --version
xcodebuild -version
watchman watch-list || true
find "$PWD" -maxdepth 2 -name metro.config.js -print

如果提交与源文件都正确,但 JavaScript 包仍旧,优先检查 Metro。若归档中原生代码未更新,则检查 DerivedData、构建配置和实际打开的工作区。Watchman 报错或监听了父目录时,先处理监听边界。

清理是一种修复动作,不是诊断结论。先确定哪一层失效,再删除那一层的状态。

为每个任务分配独立工作区

并行任务不能在同一检出目录内切换分支,也不要把多个任务放进同一个被 Watchman 监听的父目录。推荐路径包含流水线编号和任务编号,例如 /Users/ci/work/4821/ios-release。任务结束后整个目录可独立归档或删除。

工作区必须是实际目录而非不断改向的符号链接。某些脚本用固定路径指向“当前构建”,Watchman 与工具链却可能保留解析后的旧路径,结果是命令看似进入新目录,监听仍落在旧检出上。

JOB_ROOT="/Users/ci/work/${PIPELINE_ID}/${JOB_ID}"
WORKSPACE="${JOB_ROOT}/repo"
DERIVED_DATA="${JOB_ROOT}/DerivedData"
METRO_CACHE="${JOB_ROOT}/metro-cache"

mkdir -p "$WORKSPACE" "$DERIVED_DATA" "$METRO_CACHE"
export TMPDIR="${JOB_ROOT}/tmp/"
mkdir -p "$TMPDIR"

目录名只使用稳定的 ASCII 字符,避免空格和动态软链接。一个任务一个根目录,也让磁盘占用和清理责任更容易审计。

收紧 Watchman 的监听范围

进入工作区后执行 watchman watch-project "$WORKSPACE",并检查返回的 watch 是否就是预期项目根。如果它向上提升到共享父目录,通常说明仓库边界不清楚,或父目录存在影响识别的配置。

在仓库根放置 .watchmanconfig,排除不会参与 JavaScript 依赖图的重目录:

{
  "ignore_dirs": [
    ".git",
    "DerivedData",
    "build",
    "artifacts"
  ]
}

修改该文件后应重新建立当前项目的监听。不要在并发机器上习惯性执行 watchman watch-del-all,因为它会删除其他任务的监听。安全做法是让每个工作区成为独立监听根,任务结束时只执行:

watchman watch-del "$WORKSPACE" || true

如果该命令提示目标不是监听根,应先查看 watchman watch-list,不要猜路径后批量删除。

显式管理 Metro 与 Xcode 缓存

Metro 缓存应跟任务或代码基线绑定,不能依赖系统临时目录里来源不明的历史文件。启动脚本要把缓存位置作为显式参数传入;具体参数名称可能随 Metro 版本变化,应以项目锁定版本的帮助输出为准。无论采用命令参数还是项目配置,最终目标都是让缓存落在 ${METRO_CACHE},而不是多个任务共用默认目录。

--reset-cache 只用于确认转换缓存损坏后的单次恢复。若每个任务都重置,说明隔离方案尚未完成。对于 Xcode,同样把 DerivedData 固定到任务目录:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DATA" \
  build

Pods、Swift 包和 JavaScript 依赖可以使用经过校验的下载缓存,但“下载缓存”与“构建状态”要分开。前者可按锁文件键值复用,后者应由单个任务独占。恢复缓存后,至少核对锁文件哈希,不能只凭分支名命中。

用分层清理代替一键重置

出现异常时,从影响最小的层开始。先终止当前 Metro 进程,删除该任务的 Metro 缓存并重启;仍异常,再移除当前工作区的 Watchman 监听并重新注册;只有原生编译结果不一致时,才删除该任务的 DerivedData。最后才考虑重新检出仓库。

清理前确认没有进程仍占用目录:

pgrep -af "$WORKSPACE" || true
lsof +D "$JOB_ROOT" 2>/dev/null | head -n 50 || true

任务退出脚本还应验证工作区路径位于预期根目录下,避免变量为空时误删用户目录。可使用 case 对路径前缀做白名单判断,再执行 rm -rf -- "$JOB_ROOT"

验收时连续运行两次相同提交。第二次可以复用下载缓存,但工作区、Metro 状态和 DerivedData 仍应保持任务级边界。两次产物若不同,先比较生成文件、环境变量和构建日志,不要立即扩大清理范围。这样建立的流程不仅能修复一次缓存问题,也能留下足够证据解释下一次偏差。

常见问题

每次构建都需要使用 Metro 的 reset-cache 吗?

不需要。常规任务应依靠独立缓存目录和固定工作区边界;只有确认模块图或转换缓存失效时,才对单个任务执行重置。

多个 React Native 任务可以共享同一个 Watchman 监听根吗?

不建议。并发任务应使用互不嵌套的工作区,并分别注册监听根,否则文件事件和清理操作可能跨任务相互影响。

独享物理 Mac 节点

为下一次 Xcode 构建选择持续在线的云端 Mac

核对芯片、内存、存储、计费周期和节点区域后进入配置流程。实际可用状态以控制台实时返回为准。

选择云端 Mac 配置