开发任务笔记

云端 Mac CI 文件时间戳漂移排查实战

云端 Mac CI 文件时间戳漂移排查实战

代码没有变化,云端 Mac 上的第二次 Xcode 构建却仍然编译大量目标;同一提交换到新工作区后又恢复正常。这类问题不一定来自 DerivedData,也可能是源码、生成文件或缓存产物带着错误的修改时间。只清缓存能暂时消除症状,但如果同步和恢复流程没有改变,时间戳漂移还会再次进入工作区。

先确认是不是时间戳问题

先固定提交、Scheme、Xcode 路径和构建参数,连续执行两次相同构建。第一次允许生成完整中间产物,第二次观察哪些任务仍在运行。若同一批 Swift、资源或脚本阶段反复执行,再比较干净工作区与复用工作区的结果。

排查时不要只看构建总时长。重点记录重复执行的输入路径、脚本输出和生成目录,并检查构建开始前是否有工具改写文件。常见信号包括:

增量构建依赖的不只是文件内容。输入与输出的时间关系一旦失真,构建系统就可能持续把已经完成的任务判定为需要重做。

扫描未来时间与异常目录

先检查系统时间和时区,再扫描比当前时间晚五分钟以上的文件。五分钟容差可以避开极短的采集误差,同时足以发现明显漂移。

#!/bin/zsh
set -euo pipefail

root="${1:-$PWD}"
limit=$(( $(date +%s) + 300 ))

find "$root" -type f -print0 |
while IFS= read -r -d '' file; do
  modified=$(stat -f '%m' "$file")
  if (( modified > limit )); then
    printf '%s\t%s\n' \
      "$(date -r "$modified" '+%Y-%m-%d %H:%M:%S %z')" \
      "$file"
  fi
done

扫描范围至少应覆盖源码、项目文件、脚本、资源、代码生成目录和恢复后的缓存。不要一开始就扫描整个用户目录,否则包管理器缓存、日志与系统文件会制造大量噪声。对命中的文件继续运行 stat -x 文件路径,核对修改时间、变更时间和所属卷。

如果异常集中在一个目录,通常可以直接缩小到某个下载、解压、同步或代码生成步骤;如果散布在整个仓库,则应检查工作区复制方式以及任务开始前执行的初始化脚本。

按来源判断修复方式

不同来源需要不同处理,不能统一用 touch 掩盖。

现象 常见来源 建议处理
少量源码时间在未来 压缩包或同步来源异常 重新取得文件,并核对来源机器时间
生成文件每轮更新时间 生成器无条件覆盖 内容变化后再原子替换
缓存目录时间关系混乱 恢复时保留错误元数据 废弃该批缓存并重建缓存键
整个工作区时间接近复制时刻 复制参数改变元数据 统一检出方式,不混用多种同步策略
输出早于输入且长期不变 缓存包内部时间错误 重新生成并验证缓存清单

对于 Git 管理的源码,最稳妥的修复通常是确认没有未提交修改后,重新检出受影响目录,而不是重写整个仓库的时间。对于生成文件,应让生成器先写临时文件,比较内容后再替换:

generate_config > Config.generated.swift.tmp
if ! cmp -s Config.generated.swift.tmp Config.generated.swift; then
  mv Config.generated.swift.tmp Config.generated.swift
else
  rm Config.generated.swift.tmp
fi

这样内容不变时,原文件的修改时间也不会变化,依赖它的编译任务便不会被无意义地唤醒。

检查缓存与同步边界

缓存恢复前要明确哪些目录可以跨任务复用。构建产物、模块缓存和包管理器缓存的生命周期不同,不应打成一个无法追踪的大包。缓存清单至少记录缓存键、Xcode 版本、架构、生成命令和文件数量;恢复后先做时间戳预检,再允许构建进入主阶段。

同步工具也要明确是否保留修改时间。保留时间有利于增量判断,但来源机器时间错误时会原样传播;不保留时间则可能让全部文件看起来刚被修改。关键不是固定选择某个参数,而是让团队只采用一种经过验证的策略,并把策略写进任务脚本。

若代码由压缩包交接,可先在隔离目录解压,完成未来时间扫描、文件数量核对和 Git 状态检查,再原子切换到正式工作区。不要直接覆盖一个仍含旧中间产物的目录。

把时间基线放进任务入口

最终应把检查放到依赖解析和 Xcode 构建之前。发现未来时间文件时立即失败,并输出有限数量的路径;不要自动修改后继续构建,因为自动修正会抹掉故障证据。

建议为每次任务保留以下记录:

  1. date 与 systemsetup -gettimezone 的输出;
  2. 当前提交、Xcode 路径和 SDK 信息;
  3. 未来时间文件数量及前若干条路径;
  4. 缓存键与恢复结果;
  5. 首次构建和第二次构建的任务差异。

修复后用同一提交连续运行三轮:第一轮建立缓存,第二轮验证增量效果,第三轮确认结果不是偶然。若第二、三轮仍重复同一任务,就沿着该任务的输入文件继续追踪,而不是再次全量清理。时间戳治理的目标不是让所有文件拥有相同时间,而是让源码、生成物和缓存之间保持可解释、可复核的先后关系。

常见问题

发现未来时间文件后,可以直接对整个仓库执行 touch 吗?

不建议。全量 touch 会同时改写所有文件的修改时间,通常会触发一次大范围重编译。应先确认来源,再只修复异常文件或重新检出受影响目录。

为什么干净构建正常,增量构建却持续变慢?

干净构建不依赖旧的依赖图和中间产物,而增量构建会比较输入、输出及缓存状态。未来时间文件、回拨时间或恢复错误的缓存都可能让依赖长期被判定为过期。

独享开发环境

为下一项开发任务配置云端 Mac

选择 Oak M4、Oak M4 Plus 或 Oak M4 Pro,并按团队位置从五个物理节点中配置租用方案。

配置云端 Mac