归档在本地能够完成,换到云端 Mac 却在签名阶段报出 resource fork, Finder information, or similar detritus not allowed。这类故障通常不是证书或描述文件失效,而是某个图片、脚本产物或复制目录携带了 macOS 扩展属性。它会随着资源复制进入 App 包,直到签名工具执行严格检查时才暴露。
先确认失败属于哪一层
不要看到“签名失败”就立即重建证书。先从完整构建日志中找到第一条 codesign 错误,记录失败对象是主 App、Framework、扩展还是资源包。若日志明确出现 FinderInfo、ResourceFork 或 detritus,排查重点应转向文件元数据。
扩展属性常见于以下路径:
- 从图形文件管理器复制进仓库的图片、字体或压缩包;
- 从共享目录解压后直接提交的资源;
- 构建脚本用
cp -R搬运的预生成目录; - 未纳入版本控制、但长期留在工作目录里的本地产物;
- 缓存恢复时连同文件元数据一起写回的依赖。
删除 DerivedData 只能清掉构建输出。如果污染位于源码或外部输入中,下一次构建仍会稳定复现。
先保存失败命令、目标路径和日志,再做清理。否则即使构建偶然恢复,也无法判断真正的污染源。
建立只读扫描基线
检查仓库输入
xattr -lr 会递归列出路径及其属性。先把结果写入日志,不要边扫描边删除:
set -euo pipefail
ROOT="${SRCROOT:-$PWD}"
REPORT="${TMPDIR:-/tmp}/ios-xattr-source.log"
xattr -lr "$ROOT" > "$REPORT" 2>&1 || true
grep -E \
'com\.apple\.(FinderInfo|ResourceFork)' \
"$REPORT" || true
仓库体积较大时,可排除 .git、DerivedData 和依赖缓存,再逐项检查实际参与打包的目录。重点不是命中数量,而是回答三个问题:属性附着在哪个文件、文件由谁产生、它通过哪个 Build Phase 进入包内。
| 命中位置 | 优先检查 | 处理原则 |
|---|---|---|
| Assets 或资源目录 | 文件导入方式、解压工具 | 修复源文件 |
| 脚本输出目录 | cp、ditto 参数 |
重建暂存目录 |
| DerivedData | 缓存恢复、旧产物 | 清缓存后复现 |
| Framework 内部 | 下载与解包步骤 | 重新获取并验收 |
检查归档包
若归档已经生成,直接扫描 .app 能确认污染是否进入交付物:
ARCHIVE_PATH="$PWD/build/App.xcarchive"
APP_PATH="$ARCHIVE_PATH/Products/Applications/App.app"
REPORT="$PWD/build/xattr-app.log"
test -d "$APP_PATH"
xattr -lr "$APP_PATH" > "$REPORT" 2>&1 || true
if grep -Eq 'com\.apple\.(FinderInfo|ResourceFork)' "$REPORT"; then
echo "forbidden extended attributes found"
exit 1
fi
这里使用固定示例路径。实际流水线应从归档目录解析唯一的 .app,并在找不到或找到多个候选项时直接失败,避免检查错包。
清理时保留可追溯性
不建议一上来对整个工作区执行 xattr -cr。它会移除所有扩展属性,既扩大修改范围,也可能掩盖下载、解压或缓存步骤中的问题。更稳妥的方法是只删除已经确认会破坏签名的两类属性:
TARGET="$PWD/StagingPayload"
find "$TARGET" -print0 |
while IFS= read -r -d '' item; do
xattr -d com.apple.FinderInfo "$item" 2>/dev/null || true
xattr -d com.apple.ResourceFork "$item" 2>/dev/null || true
done
TARGET 最好是一次性暂存副本,而不是开发者的原始工作区。完成清理后重新扫描;仍有命中就停止构建,不要把 || true 放到最终验收条件上。
若属性来自压缩包,应修复解包后的验收步骤;若来自构建脚本,应让脚本每次创建空暂存目录,再复制明确的文件清单。仅在末尾补一条递归清理命令,会让污染继续存在并在其他任务中复发。
把检查接入归档流程
构建前检查输入
构建前门禁只扫描会进入 App 的资源目录,不必扫描整个主目录。命中时输出相对路径和属性名,然后以非零状态退出。这样可以在耗时归档开始前失败,也便于定位最近变更。
检查脚本应纳入版本控制,并固定运行环境:
- 使用脚本自身位置解析仓库根目录;
- 对包含空格的路径使用空字符分隔;
- 将报告写入本次任务独立的产物目录;
- 不在扫描阶段修改文件;
- 对不存在的目标目录直接报错。
归档后验证签名
扩展属性扫描通过后,再让系统签名工具验证归档:
APP_PATH="$PWD/build/App.xcarchive/Products/Applications/App.app"
codesign --verify \
--strict \
--verbose=2 \
"$APP_PATH"
如果 App 内包含独立 Framework 或扩展,还应遍历这些嵌套代码对象分别验证。不要只检查主包后就默认内部组件安全。验证日志、归档路径和源码提交号应一起保存,便于区分“源码相同但输入缓存不同”的情况。
用验收清单结束排查
一次可靠修复应同时满足以下条件:
- 源码与外部输入的扫描结果已经留档;
- 污染文件的产生或复制路径已经确定;
- 清理只作用于明确属性或一次性暂存副本;
- DerivedData 与相关缓存清除后仍能重新归档;
- 最终 App 包内没有 FinderInfo 和 ResourceFork;
- 主 App 与嵌套代码对象均通过严格签名验证;
- 同一提交在全新工作目录中可以重复通过。
扩展属性故障难排,不是因为命令复杂,而是它横跨源码、文件系统元数据、缓存和签名四层。把“先记录、再定位、后清理、终验收”固化为门禁后,这类问题就不再依赖人工登录节点临时处理,也不会因为更换工作目录而失去线索。
常见问题
为什么删除 DerivedData 后签名错误仍会出现?
如果 FinderInfo 或 ResourceFork 已附着在源码资源、脚本复制目录或下载文件上,重新构建仍会把它带入 App 包。应先扫描源码与外部输入,再清理 DerivedData 并重新归档。
可以直接对整个仓库执行 xattr -cr 吗?
不建议。递归清除会同时删除与故障无关的属性。更稳妥的做法是先记录命中项,只删除 FinderInfo 和 ResourceFork,或在一次性暂存副本中执行全量清理。
清理后怎样确认归档可以交付?
重新生成归档后,先扫描 App 包内的目标扩展属性,再运行 codesign --verify --strict。两项都通过,并保存日志与归档校验值,才算完成验收。
在独享物理节点上运行下一项任务
使用 M4、16GB RAM 与 256GB SSD 的云端 Mac,按任务周期选择新加坡、东京、首尔或香港节点。