一个 Swift Package 在本地预览正常,接入云端 Mac 的归档任务后却出现空白图标、默认文案或 JSON 读取失败,最麻烦的地方通常不是编译错误,而是流水线仍然显示成功。SwiftPM 会处理 Bundle.module,但它不会替团队判断资源是否声明完整、调用名称是否匹配,也不会证明资源已经进入最终应用包。解决办法是把资源当作构建产物的一部分验收,而不是等到测试人员打开页面后才发现缺失。
先识别资源丢失的三种位置
排查前先区分问题发生在哪一层。第一层是源码目录中有文件,但 Package.swift 的 target 没有声明对应资源;第二层是资源已经生成 .bundle,调用代码却使用了主应用的 bundle;第三层是文件进入了应用包,但文件名大小写、子目录或本地化目录与运行时查询不一致。
建议先保留一个最小复现入口,让它读取一张图片、一份 JSON 和一个本地化字符串。若三个类型同时失败,优先检查 bundle 选择;若只有单个文件失败,优先检查资源规则和路径。
“编译通过”只能证明编译器接受了当前输入,不能证明运行时需要的文件已经进入交付物。
在 Package.swift 中,明确选择 .process 或 .copy。图片、资源目录和本地化内容通常使用 .process;必须保留原始目录结构或原始字节的文件才使用 .copy。不要把整个仓库目录一次性复制进资源包,这会把测试样本、临时输出甚至不应交付的配置一起带入。
用清单固定资源契约
资源验收不能只比较文件数量。新增一个资源、删除一个旧资源或者改名时,数量可能完全不变。更稳妥的办法是在仓库中维护 ci/expected-resources.txt,每行记录相对于资源 bundle 根目录的路径:
Assets.car
Defaults/config.json
en.lproj/Localizable.strings
zh-Hans.lproj/Localizable.strings
清单应只包含运行时必须存在的文件。按需下载内容、测试夹具和开发预览资源不应混入。代码评审时,资源改名必须与清单变更同时出现,这样审阅者能看见交付契约发生了什么变化。
资源目录本身也要做反向检查:源码里存在但未被声明的文件应给出警告,清单要求但构建结果中不存在的文件则直接失败。前者帮助清理仓库,后者负责守住交付结果。
构建后直接检查应用包
每个任务使用独立的 DerivedData,避免另一条并行任务留下的旧 bundle 让检查误通过。以下脚本以模拟器构建为例;实际流水线只需替换 scheme,并把 bundle 名匹配规则改成项目的真实模块名。
set -euo pipefail
DERIVED="${RUNNER_TEMP:-/tmp}/resource-check-${BUILD_ID:-local}"
rm -rf "$DERIVED"
xcodebuild \
-scheme ExampleApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
build
APP=$(find "$DERIVED/Build/Products" -type d -name 'ExampleApp.app' -print -quit)
test -n "$APP"
COUNT=$(find "$APP" -type d -name '*FeatureKit*.bundle' | wc -l | tr -d ' ')
test "$COUNT" = "1"
BUNDLE=$(find "$APP" -type d -name '*FeatureKit*.bundle' -print -quit)
while IFS= read -r item; do
test -z "$item" && continue
test -e "$BUNDLE/$item" || {
printf 'missing resource: %s\n' "$item" >&2
exit 1
}
done < ci/expected-resources.txt
这里刻意先断言 bundle 数量为一。若直接取第一个匹配结果,旧产物、测试 bundle 或重名模块可能掩盖问题。检查对象也必须是 .app 内部的 bundle,而不是 DerivedData 中间目录,因为只有前者代表最终嵌入结果。
把高频误区变成门禁
不同资源类型需要不同验收方式,不能全部用 test -e 草草结束。
| 资源类型 | 最低检查 | 常见失败 |
|---|---|---|
| JSON | 文件存在并可被解析 | 复制了空文件或格式错误 |
| 本地化字符串 | 目标语言目录存在且键可读取 | 语言目录命名错误、回退到默认文案 |
| 图片与颜色 | 资源编译产物存在,冒烟页面可加载 | 名称大小写不一致或误用主 bundle |
| 模板文件 | 校验哈希或关键字段 | 生成步骤覆盖了仓库版本 |
macOS 常见工作卷对大小写不敏感,因此 IconDark 与 icondark 的错误可能在开发机上被掩盖。不要依赖文件系统碰巧容错,应从清单提取路径,与实际路径进行区分大小写的逐字比较。对 JSON 再运行解析器,对属性列表使用 plutil -lint,对字符串目录至少检查必需语言和关键键。
若 Package 中使用生成插件,先确认生成步骤发生在资源验收之前,并把生成目录限定到任务自己的临时路径。多个任务共享同一生成目录时,一个任务可能读取到另一个分支的文件,形成最难复现的假成功。
保存证据并保持任务可重跑
失败时不要上传整个 DerivedData。它体积大、噪声多,还可能包含不适合长期保存的路径信息。更实用的证据包包括:资源清单、实际 bundle 文件树、构建日志末段、解析器错误,以及当前提交标识。文件树可以这样生成:
find "$BUNDLE" -print \
| sed "s#^$BUNDLE/##" \
| LC_ALL=C sort \
> resource-bundle-tree.txt
日志与文件树归档前应移除访问令牌、用户目录和临时凭据。任务结束后删除独立 DerivedData 与临时生成目录,下一次运行必须从空目录开始。DPLYMAC 上的并行执行器也应遵守同一原则:任务可以复用下载缓存,但不能复用未经标识的构建输出。
最终门禁应回答三个问题:声明中要求什么、最终应用包里有什么、运行时能否按真实名称读到。只要这三层分别留下可检查结果,SwiftPM 资源缺失就会从偶发的界面故障,变成提交阶段即可定位的普通构建错误。
常见问题
为什么 SwiftPM 包能编译成功,运行时仍然找不到资源?
编译成功只说明源码和资源声明可以被处理,不代表调用路径、文件名大小写或最终应用包中的资源位置正确。应直接检查编译后的 .app 与对应 .bundle。
资源验收应该检查源码目录还是最终应用包?
两者都要检查,但最终结论以构建产物为准。源码检查用于发现未声明文件,应用包检查用于确认资源确实被复制、处理并嵌入交付产物。
把验证过的工作流放到独享物理节点
按任务选择 M4 或 M4 Pro、目标节点与计费周期,在下单前核对配置和附加选项。