macOS 应用打包、签名与公证实战记录
这是一篇通用的 macOS 桌面应用打包记录,适用于使用 Tauri、Electron 或其他工具生成 .app 后,需要在本地完成签名、公证并分发 DMG 的场景。
文中所有应用名称、证书名称、Team ID、Apple ID 和路径均使用占位符,避免泄露个人信息。
为什么要拆分打包流程
很多桌面应用框架都支持一步生成 DMG,例如:
<build-command> --bundles dmg
这种方式简单,但失败时很容易变成黑盒。一次完整的 macOS 分发包其实包含多个阶段:
构建 .app
↓
签名 .app
↓
公证 .app
↓
staple .app
↓
生成 DMG
↓
签名 DMG
↓
公证 DMG
↓
staple DMG
↓
最终验证
如果把所有步骤塞进一个命令,失败时很难判断问题到底发生在签名、公证、DMG 生成,还是 Gatekeeper 校验。
本文采用的策略是:
只让构建工具生成并签好 .app
↓
手工验证 .app
↓
使用 create-dmg 生成 DMG
↓
单独 notarize DMG
↓
staple + verify DMG
这种方式命令多一点,但每一步都可验证,也更适合排查问题。
前置条件
本地机器需要准备:
- Apple Developer Program 账号
Developer ID Application证书- 证书必须带 private key
- Xcode Command Line Tools
- 应用构建工具,例如 Tauri CLI、Electron Builder 等
create-dmg
安装 create-dmg:
brew install create-dmg
如果应用需要交叉构建不同的 macOS 架构,也要提前安装对应 target。以 Rust/Tauri 为例:
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
确认证书类型
macOS 应用直接分发给用户安装时,不能使用 Apple Distribution 证书,而应该使用:
Developer ID Application
检查本机可用的签名身份:
security find-identity -v -p codesigning | grep "Developer ID Application"
期望看到类似:
"Developer ID Application: <Developer Name> (<TEAM_ID>)"
双引号里的完整字符串后面会作为 APPLE_SIGNING_IDENTITY 使用。
如果只有 .cer 文件,或者 Keychain Access 中证书下面没有 private key,签名会失败。正确做法是在创建 CSR 的那台 Mac 上安装证书,然后从 Keychain Access 的 My Certificates 中导出带 private key 的 .p12。
设置环境变量
本地打包前设置:
export APPLE_SIGNING_IDENTITY="Developer ID Application: <Developer Name> (<TEAM_ID>)"
export APPLE_ID="<[email protected]>"
export APPLE_PASSWORD="<app-specific-password>"
export APPLE_TEAM_ID="<TEAM_ID>"
注意:
APPLE_PASSWORD不是 Apple ID 登录密码,必须是在 Apple 账号管理页面生成的 app-specific password。APPLE_SIGNING_IDENTITY必须与security find-identity显示的证书名称完全一致。- 不要把这些环境变量的真实值提交到 Git 仓库或输出到公开的构建日志中。
第一步:只构建 .app
先不要生成 DMG,只生成 .app:
<build-command> --target <TARGET_TRIPLE> --bundles app
以 Tauri 为例:
npm run tauri -- build --target <TARGET_TRIPLE> --bundles app
构建成功后,通常会得到一个 .app 目录,例如:
<APP_BUNDLE_PATH>/<APP_NAME>.app
为了方便执行后续命令,先设置变量:
APP="<APP_BUNDLE_PATH>/<APP_NAME>.app"
第二步:验证 .app
验证签名:
codesign --verify --deep --strict --verbose=2 "$APP"
期望看到:
valid on disk
satisfies its Designated Requirement
验证公证票据:
xcrun stapler validate "$APP"
期望看到:
The validate action worked!
验证 Gatekeeper:
spctl --assess --type exec --verbose "$APP"
期望看到:
accepted
source=Notarized Developer ID
这三步都通过,说明 .app 本身已经是健康的。
第三步:准备 DMG 目录
设置输出路径:
DMG_STAGE="/tmp/<app-name>-dmg-stage"
DMG_OUT="dist/<APP_NAME>-<ARCH>.dmg"
清理旧文件并创建目录:
rm -rf "$DMG_STAGE"
rm -f "$DMG_OUT"
rm -f dist/rw.*.dmg
mkdir -p "$DMG_STAGE"
mkdir -p dist
把 .app 复制到 DMG staging 目录:
ditto "$APP" "$DMG_STAGE/<APP_NAME>.app"
第四步:使用 create-dmg 生成 DMG
create-dmg \
--volname "<APP_NAME>" \
--window-size 660 400 \
--window-pos 200 120 \
--icon-size 80 \
--icon "<APP_NAME>.app" 180 220 \
--hide-extension "<APP_NAME>.app" \
--app-drop-link 480 220 \
--codesign "$APPLE_SIGNING_IDENTITY" \
--no-internet-enable \
--skip-jenkins \
"$DMG_OUT" \
"$DMG_STAGE"
这里使用 --skip-jenkins 是为了跳过 Finder AppleScript 美化步骤。create-dmg 默认会控制 Finder 摆放 DMG 里的图标,如果终端没有 Automation 权限,可能会失败:
Not authorized to send Apple events to Finder. (-1743)
加上 --skip-jenkins 后,DMG 仍然可以正常签名、公证和分发,只是打开时的图标布局没有那么精致。
如果确实需要 Finder 布局效果,可以在系统设置中授权终端控制 Finder:
System Settings
→ Privacy & Security
→ Automation
→ Allow Terminal / iTerm / VS Code to control Finder
第五步:验证 DMG 签名
codesign --verify --verbose=2 "$DMG_OUT"
期望看到:
valid on disk
satisfies its Designated Requirement
第六步:公证 DMG
提交 DMG 到 Apple 公证服务:
xcrun notarytool submit "$DMG_OUT" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
成功时会看到:
status: Accepted
如果长时间没有返回,可以另开终端查看历史:
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
如果某个任务显示:
status: In Progress
说明 Apple 已经收到提交,还在处理,不一定是本地卡死。
查询具体任务:
xcrun notarytool info <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
如果任务变成:
status: Invalid
下载日志:
xcrun notarytool log <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
第七步:staple DMG 并最终验证
公证通过后,将票据 staple 到 DMG:
xcrun stapler staple "$DMG_OUT"
xcrun stapler validate "$DMG_OUT"
最后使用 Gatekeeper 验证:
spctl --assess --type open --context context:primary-signature --verbose "$DMG_OUT"
期望看到:
accepted
source=Notarized Developer ID
到这里,$DMG_OUT 就是可以分发给用户的 macOS 安装包。
常见问题
1. failed codesign application
常见原因:
APPLE_SIGNING_IDENTITY写错- 证书不是
Developer ID Application - 证书没有 private key
- 钥匙串锁住了
codesign没有权限访问 private key
排查:
echo "$APPLE_SIGNING_IDENTITY"
security find-identity -v -p codesigning | grep "Developer ID Application"
解锁钥匙串:
security unlock-keychain ~/Library/Keychains/login.keychain-db
也可以用一个临时二进制测试签名:
cp /bin/echo /tmp/codesign-test
codesign --force \
--sign "$APPLE_SIGNING_IDENTITY" \
--options runtime \
--timestamp \
/tmp/codesign-test
如果这一步都失败,问题就在证书或钥匙串,不在应用构建。
2. Keychain 弹窗要求访问私钥
签名过程中可能出现:
Keychain Access wants to export key "<key name>" from your keychain.
这是正常的,codesign 需要读取 private key。
建议选择:
Always Allow
输入的是本机登录密码,不是 Apple ID 密码。
3. 公证长时间停在 In Progress
如果看到:
status: In Progress
说明 Apple 已经收到提交,还在处理。正常可能需要几分钟到二十分钟,偶尔会更久。
查看历史:
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
查询具体任务:
xcrun notarytool info <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
4. DMG 生成阶段失败
如果框架内置的 DMG 生成脚本失败,例如:
failed to run bundle_dmg.sh
可以改成:
先构建 .app
↓
验证 .app
↓
手工 create-dmg
↓
单独 notarize DMG
如果失败后 .app 变成:
invalid signature (code or signature have been modified)
建议清理 bundle 目录并重新构建 .app,不要继续使用损坏的 .app 生成 DMG。
5. Not authorized to send Apple events to Finder. (-1743)
这是 create-dmg 的 AppleScript 控制 Finder 被 macOS 隐私权限拦截。
最快的解决方法:
create-dmg ... --skip-jenkins ...
如果需要保留 Finder 图标布局,则给终端授权:
System Settings
→ Privacy & Security
→ Automation
→ Allow Terminal to control Finder
完整命令模板
下面是一组可复用的通用命令模板:
cd /path/to/project
export APPLE_SIGNING_IDENTITY="Developer ID Application: <Developer Name> (<TEAM_ID>)"
export APPLE_ID="<[email protected]>"
export APPLE_PASSWORD="<app-specific-password>"
export APPLE_TEAM_ID="<TEAM_ID>"
<build-command> --target <TARGET_TRIPLE> --bundles app
APP="<APP_BUNDLE_PATH>/<APP_NAME>.app"
DMG_STAGE="/tmp/<app-name>-dmg-stage"
DMG_OUT="dist/<APP_NAME>-<ARCH>.dmg"
codesign --verify --deep --strict --verbose=2 "$APP"
xcrun stapler validate "$APP"
spctl --assess --type exec --verbose "$APP"
rm -rf "$DMG_STAGE"
rm -f "$DMG_OUT"
rm -f dist/rw.*.dmg
mkdir -p "$DMG_STAGE"
mkdir -p dist
ditto "$APP" "$DMG_STAGE/<APP_NAME>.app"
create-dmg \
--volname "<APP_NAME>" \
--window-size 660 400 \
--window-pos 200 120 \
--icon-size 80 \
--icon "<APP_NAME>.app" 180 220 \
--hide-extension "<APP_NAME>.app" \
--app-drop-link 480 220 \
--codesign "$APPLE_SIGNING_IDENTITY" \
--no-internet-enable \
--skip-jenkins \
"$DMG_OUT" \
"$DMG_STAGE"
codesign --verify --verbose=2 "$DMG_OUT"
xcrun notarytool submit "$DMG_OUT" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG_OUT"
xcrun stapler validate "$DMG_OUT"
spctl --assess --type open --context context:primary-signature --verbose "$DMG_OUT"
总结
macOS 打包最重要的经验是:不要把“构建、签名、公证、生成 DMG、公证 DMG”全部当成一个不可拆分的黑盒。
把流程拆开后,可以明确知道:
.app是否签名成功.app是否公证成功- DMG 是否生成成功
- DMG 是否签名成功
- DMG 是否公证成功
- Gatekeeper 是否接受最终产物
当内置 DMG 打包流程失败时,先确认 .app 是否健康。如果 .app 已经签名、公证并通过 Gatekeeper,就可以绕过内置 DMG 步骤,使用 create-dmg 手工生成 DMG,再对 DMG 单独公证。