# 2026-06-05 Mac App Store 上传教训与预检清单 ## 这次的问题 今天上传成功前反复踩了三个坑: 1. `CFBundleIdentifier` 与 Apple Developer profile 的 Bundle ID 不一致。 2. `Info.plist` 缺少 Mac App Store 必填的 `LSApplicationCategoryType`。 3. `.app` 嵌入的 provisioning profile 有 `com.apple.application-identifier`,但签名 entitlements 没有同一个值,触发 Transporter/TestFlight `90886`。 4. 下载得到的 provisioning profile 带 `com.apple.quarantine` 扩展属性,复制进 `.app` 后被打进 `.pkg`,触发 Transporter/TestFlight `91109`。 这些都不是业务功能问题,而是 Mac App Store 上传包的元数据和签名验收不完整。 ## 必须记住的规则 - Transporter 不是完整的本地 QA 工具;不能把用户上传失败当成检查方式。 - `pkgutil --check-signature` 只能证明 `.pkg` 被 Installer 证书签了,不能证明 App Store 会接受。 - `codesign -vvv --deep --strict` 只能证明签名结构有效,不能证明 entitlements 与 embedded profile 一致。 - 每次重传必须换新的 `CFBundleVersion` build 号。 - 同一个 App Store 版本可以保持 `CFBundleShortVersionString=2.1.28`,但每个新上传包必须有新的 build。 - 最终给用户上传的只能是 `*-universal-macappstore.pkg`,不能是 sandbox POC DMG,也不能是旧 build。 ## 上传前硬性预检 在把 `.pkg` 交给用户上传前,必须对最终 `.pkg` 展开检查,而不是只检查 build 目录里的 `.app`。 必须检查: - `CFBundleIdentifier=com.shanghai3168.privatevoicedictation` - `CFBundleShortVersionString` 与本次发布版本一致 - `CFBundleVersion` 是本次新 build - `LSApplicationCategoryType=public.app-category.productivity` - embedded provisioning profile 的 App ID 是 `CR3J54M8BQ.com.shanghai3168.privatevoicedictation` - signed entitlements 的 `com.apple.application-identifier` 是 `CR3J54M8BQ.com.shanghai3168.privatevoicedictation` - signed entitlements 的 `com.apple.developer.team-identifier` 是 `CR3J54M8BQ` - signed entitlements 包含 `com.apple.security.app-sandbox=true` - app 主程序是 universal `arm64 + x86_64` - `libsherpa-onnx-c-api.dylib` 是 universal `arm64 + x86_64` - `libonnxruntime.1.24.4.dylib` 是 universal `arm64 + x86_64` - `codesign -vvv --deep --strict` 对最终 app PASS - `pkgutil --check-signature` 对最终 pkg PASS - 最终 `.pkg` 展开后的 `.app` 内没有 `com.apple.quarantine` 扩展属性 - release、桌面上传目录、运营资料目录里的 pkg SHA256 完全一致 ## 参考检查命令 ```bash PKG="/path/to/PrivateVoice-Dictation-2.1.28-buildYYYYMMDD.HHMM-universal-macappstore.pkg" TMP="/tmp/privatevoice-pkg-check" rm -rf "$TMP" pkgutil --expand-full "$PKG" "$TMP" APP="$(find "$TMP" -name 'PrivateVoice Dictation.app' -type d -print -quit)" /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c 'Print :LSApplicationCategoryType' "$APP/Contents/Info.plist" PROFILE_PLIST="$TMP/profile.plist" security cms -D -i "$APP/Contents/embedded.provisionprofile" > "$PROFILE_PLIST" /usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.application-identifier' "$PROFILE_PLIST" /usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.developer.team-identifier' "$PROFILE_PLIST" SIGNED_ENTITLEMENTS="$TMP/signed-entitlements.plist" codesign -d --entitlements :- "$APP" > "$SIGNED_ENTITLEMENTS" 2>/dev/null /usr/libexec/PlistBuddy -c 'Print :com.apple.application-identifier' "$SIGNED_ENTITLEMENTS" /usr/libexec/PlistBuddy -c 'Print :com.apple.developer.team-identifier' "$SIGNED_ENTITLEMENTS" /usr/libexec/PlistBuddy -c 'Print :com.apple.security.app-sandbox' "$SIGNED_ENTITLEMENTS" file "$APP/Contents/MacOS/PrivateVoice Dictation" file "$APP/Contents/Frameworks/libsherpa-onnx-c-api.dylib" file "$APP/Contents/Frameworks/libonnxruntime.1.24.4.dylib" codesign -vvv --deep --strict "$APP" pkgutil --check-signature "$PKG" xattr -lr "$APP" | grep -F 'com.apple.quarantine' && exit 1 || true shasum -a 256 "$PKG" ``` ## 今天形成的修复 - 正式 Bundle ID 固定为 `com.shanghai3168.privatevoicedictation`。 - Mac App Store 分类固定为 `public.app-category.productivity`。 - 打包脚本必须从 provisioning profile 合并以下 entitlements 后再签名: - `com.apple.application-identifier` - `com.apple.developer.team-identifier` - `keychain-access-groups` - 当前可上传包是 `20260605.0216`,不要再上传 `20260604.0049` 或 `20260604.2353`。 - 如果遇到 `91109`,必须在组装 app 后、签名后、productbuild 前对 `.app` 执行 `xattr -cr`,并展开最终 `.pkg` 检查没有 `com.apple.quarantine`。 ## 上传后还要检查 - App Store Connect 里选择最新 build。 - 付费应用必须确认 Paid Apps Agreement、税务和银行信息已生效。 - 价格不在 Transporter 设置;在 App Store Connect 的 `Monetization / Pricing and Availability` 设置。 - 提交审核前确认截图、隐私政策、支持 URL、审核备注、隐私问卷和价格都已填完。