macOS 应用打包、签名与公证实战记录

一篇适用于 Tauri、Electron 等桌面应用的 macOS 分发实战指南,完整介绍 .app 构建与验证、Developer ID 签名、Apple 公证、DMG 制作、票据装订、Gatekeeper 检查及常见问题排查。
Mttao
Mttao
@mttao
4 分钟
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 单独公证。

/ 评论