一个图像分类模型在本地调试正常,归档也显示成功,交付后的应用却在首次推理时找不到资源。常见原因不是模型算法,而是模型没有进入目标、被脚本复制到错误目录,或同名旧版本仍留在应用包里。云端 Mac 流水线需要验证的因此不只是“能否构建”,还包括模型源码能否独立编译,以及最终交付包里到底装了什么。
把模型验收拆成两道门
第一道门直接处理 .mlmodel 或 .mlpackage,排除模型格式、工具链和生成目录问题。第二道门在 Xcode 归档完成后检查 .app,确认编译后的 .mlmodelc 确实进入交付目标。两道门必须分开:若只运行 xcodebuild,失败时很难迅速判断问题来自模型编译、目标成员关系,还是资源复制阶段。
开始前固定开发者目录,并记录工具版本:
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcodebuild -version
xcrun --find coremlcompiler
swift --version
ArmVMS 上若安装了多个 Xcode 版本,应由任务显式设置 DEVELOPER_DIR,不要依赖交互式终端中的 xcode-select 状态。流水线和人工登录会话的环境变量可能不同,显式路径更容易复现。
模型验收的对象应是“指定 Xcode 工具链生成的指定应用包”,而不是某台机器上一次偶然成功的构建目录。
先独立编译 Core ML 模型
为每个模型准备独立输出目录,禁止多个任务共用同一个临时目录。下面的脚本同时兼容 .mlmodel 与 .mlpackage 输入:
MODEL_PATH="${1:?model path required}"
OUTPUT_ROOT="${2:-$PWD/build/coreml}"
MODEL_NAME="$(basename "$MODEL_PATH")"
MODEL_NAME="${MODEL_NAME%.*}"
MODEL_OUTPUT="$OUTPUT_ROOT/$MODEL_NAME"
rm -rf "$MODEL_OUTPUT"
mkdir -p "$MODEL_OUTPUT"
xcrun coremlcompiler compile "$MODEL_PATH" "$MODEL_OUTPUT"
find "$MODEL_OUTPUT" -type f -print0 |
sort -z |
xargs -0 shasum -a 256 > "$MODEL_OUTPUT.files.sha256"
du -sk "$MODEL_OUTPUT"
命令成功后至少检查三件事:输出目录非空、文件清单可生成、大小不是零。不要把编译目录直接当长期缓存,因为它与 Xcode 版本、模型内容和编译器行为相关。若要复用,应把模型源码摘要、xcodebuild -version 输出和构建脚本版本共同组成缓存键。
给生成型模型增加输入检查
部分项目在构建前由 Python 或转换工具生成模型。此时要先验证输入文件,再调用 coremlcompiler。生成任务必须写入新目录,全部完成后再原子移动到约定路径,避免 Xcode 读到半成品。
建议保存以下字段作为清单:
| 字段 | 用途 |
|---|---|
| 模型相对路径 | 区分主应用与扩展使用的模型 |
| 源文件 SHA-256 | 判断输入是否真正变化 |
| Xcode 版本 | 解释编译产物差异 |
| 编译后大小 | 发现意外附带资源 |
| 目标名称 | 检查 Target Membership |
从归档里确认最终产物
独立编译通过后,再进行干净归档。为避免读取上一次任务的残留,给 DerivedData 和归档路径分配本次任务专属目录:
RUN_ROOT="$PWD/build/model-validation"
DERIVED_DATA="$RUN_ROOT/DerivedData"
ARCHIVE_PATH="$RUN_ROOT/App.xcarchive"
rm -rf "$RUN_ROOT"
mkdir -p "$RUN_ROOT"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-destination "generic/platform=iOS" \
-derivedDataPath "$DERIVED_DATA" \
-archivePath "$ARCHIVE_PATH" \
archive
随后定位应用包并导出模型清单:
APP_PATH="$(find "$ARCHIVE_PATH/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
find "$APP_PATH" -type d -name '*.mlmodelc' -print |
LC_ALL=C sort > "$RUN_ROOT/bundled-models.txt"
test -s "$RUN_ROOT/bundled-models.txt"
这里不要只匹配主应用根目录。模型可能属于扩展,也可能由资源包携带。检查脚本应记录完整相对路径,再按项目定义判断哪些路径允许出现。预期一个模型时发现两个同名目录,应立即查看主应用、扩展和依赖包是否各自复制了一份。
建立不依赖猜测的基线
模型体积受结构和编译工具影响,不能简单规定“增长超过某个统一百分比就失败”。更稳妥的做法是为每个模型保存审核后的基线,同时设置绝对增量与比例增量。小模型适合关注绝对值,大模型则更需要关注比例。
基线文件可以使用简单的制表符格式:
Classifier.mlmodelc 8421376
Embedding.mlmodelc 27156480
比较时还要处理新增、删除和重命名。新增模型不能自动视为错误,但必须要求变更记录说明用途;模型被删除则应阻断,除非清单也经过审核更新。重命名应按“删除旧项并新增新项”处理,避免历史数据被错误继承。
清单应作为构建产物保存,但不要上传模型源码之外的敏感配置。若模型由受控下载步骤取得,只记录摘要、逻辑名称和最终包内路径,不在日志中输出带令牌的下载地址。
常见失败与交付检查项
模型能独立编译却没有进入应用包,优先检查 Target Membership、Copy Bundle Resources、资源包依赖和条件化构建设置。Debug 正常而 Release 缺失时,比较两个配置的资源阶段与自定义脚本输入输出列表。
如果同名模型重复出现,先执行完整相对路径盘点,不要直接删除其中一个。主应用和扩展可能确实分别需要副本;真正的问题是未经设计的重复复制。若归档体积突然增加,还应检查生成目录是否被整体加入资源阶段,把原始模型、临时文件和编译产物一起装进应用。
提交前可按以下顺序验收:
- 固定
DEVELOPER_DIR并记录 Xcode 版本。 - 在隔离目录独立运行
coremlcompiler。 - 为模型源码和编译结果生成摘要清单。
- 使用全新的 DerivedData 完成 Release 归档。
- 从
.xcarchive内枚举全部.mlmodelc。 - 对照允许路径、预期数量和已审核大小基线。
- 保存清单、命令输出与归档标识,便于后续比较。
这套流程不会判断模型精度,也不能替代端到端推理测试。它解决的是更靠前、也更适合自动化的问题:模型是否可编译、是否被正确打包、交付内容是否发生未经解释的变化。把这些结论固定为机器可读清单后,运行时的模型缺失问题就能在归档阶段被发现。
常见问题
为什么不能只看 Xcode 构建是否成功?
构建成功只能说明编译链路没有返回错误,不能证明目标模型已进入最终应用包。仍需解开归档或导出产物,检查 mlmodelc 目录、文件数量和路径。
模型体积变化多少时应该阻断构建?
不要使用适用于所有模型的固定比例。先为每个模型保存已审核基线,再按团队允许的绝对增量与比例增量共同判断,并要求变更提交说明来源。
同名 Core ML 模型出现两份是否一定有问题?
通常应视为需要调查的信号。先确认它们是否属于不同目标或扩展;若最终进入同一应用包,应检查 Copy Bundle Resources、包依赖和生成脚本是否重复复制。
为下一次构建选择独享 Apple Silicon 物理节点
按天、周、月或季租用 ArmVMS M4 与 ArmVMS M4 Pro。配置、节点和附加项目在创建订单前逐项确认。