本文是可直接复用的发布 SOP。目标是产出一个可以在其他 Mac 上正常打开的安装包,而不是只能在本机运行的临时构建。

1. 前置条件

必须满足以下条件:

  1. Apple Developer 付费账号(免费账号无法做 Notarization)。
  2. Keychain 中已安装 Developer ID Application 证书,并且证书带私钥。
  3. 已安装 Xcode Command Line Tools(含 xcrunnotarytoolstapler)。
  4. Tauri 项目可正常本地构建。

检查证书:

security find-identity -v -p codesigning | grep "Developer ID Application"

如果没有输出 Developer ID Application: ...,先去 Apple Developer 后台申请并安装证书。

2. 准备 Notarization 凭据

2.1 生成 App-Specific Password

appleid.apple.com 生成应用专用密码(不是 Apple ID 登录密码)。

2.2 设置环境变量(示例)

export APPLE_SIGNING_IDENTITY="Developer ID Application: AHAKNOW LLC (HC559NT2NP)"
export APPLE_ID="ahaknow@gmail.com"
export APPLE_PASSWORD="你的 app-specific-password"
export APPLE_TEAM_ID="HC559NT2NP"

建议不要把示例占位符(如 YOUR NAMETEAMID...)原样复制进命令。

3. 生成多平台图标(避免 bundle 失败)

如果只放单一 icon.png,可能在 macOS 打包时报 No matching IconType

在项目目录执行:

cd /Users/changkunyang/CKProjects/CodexSwitch/apps/desktop
npm run tauri icon -- src-tauri/icons/icon.png

这会生成 icon.icnsicon.ico、多尺寸 PNG 等资源。

4. 构建 universal macOS 包

先安装双架构 target:

rustup target add aarch64-apple-darwin x86_64-apple-darwin

然后构建:

cd /Users/changkunyang/CKProjects/CodexSwitch/apps/desktop
npm run tauri build -- --target universal-apple-darwin --bundles app --ignore-version-mismatches

产物路径(示例):

src-tauri/target/universal-apple-darwin/release/bundle/macos/Auth Switch Desktop.app

5. 验证签名是否为 Developer ID(不是 ad-hoc)

APP="/Users/changkunyang/CKProjects/CodexSwitch/apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/Auth Switch Desktop.app"
codesign -dv --verbose=4 "$APP" 2>&1 | egrep "Authority=|TeamIdentifier=|Signature="

正确特征:

  • Authority=Developer ID Application: ...
  • TeamIdentifier=你的 Team ID
  • 不能是 Signature=adhoc

6. 打 zip 并提交公证

建议使用绝对路径,避免 ditto: Cannot get the real path for source

APP="/Users/changkunyang/CKProjects/CodexSwitch/apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/Auth Switch Desktop.app"
ZIP="/Users/changkunyang/CKProjects/CodexSwitch/apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/Auth-Switch-Desktop_0.1.0_macOS_universal.zip"

rm -f "$ZIP"
ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"

提交 Notarization:

xcrun notarytool submit "$ZIP" \
  --apple-id "$APPLE_ID" \
  --password "$APPLE_PASSWORD" \
  --team-id "$APPLE_TEAM_ID" \
  --wait

返回 status: Accepted 才表示公证通过。

7. Staple 与最终 Gatekeeper 验证

xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
spctl --assess --type execute -vv "$APP"

发布前应看到类似:

  • source=Notarized Developer ID
  • accepted

8. 上传 GitHub Release

上传第 6 步生成的 zip:

Auth-Switch-Desktop_0.1.0_macOS_universal.zip

建议同时附上 SHA256:

shasum -a 256 "$ZIP"

9. 常见错误与修复

9.1 source=no usable signature

原因:包是 ad-hoc 签名(未使用 Developer ID 证书)。

处理:回到第 5 步检查 codesign 输出,确认 Authority=Developer ID Application

9.2 Team ID must be at least 3 characters

原因:--team-id 为空或变量未生效。

处理:命令里直接写死 --team-id "HC559NT2NP" 进行验证。

9.3 HTTP status code: 401. Invalid credentials

原因:--password 用了占位符或错误密码;必须是 app-specific password。

处理:重新生成 app-specific password 再提交。

9.4 Failed to create app icon: No matching IconType

原因:缺少 macOS 所需图标类型(.icns)。

处理:执行第 3 步 npm run tauri icon

9.5 invalid icon / 像素数量不匹配

原因:PNG 位深或像素格式异常(例如 16-bit RGBA)导致解码不兼容。

处理:转成 8-bit RGBA 再生成图标。

9.6 ditto: Cannot get the real path for source

原因:当前目录不在 .app 所在目录。

处理:改用绝对路径。

10. 安全建议

  1. 不要把 APPLE_PASSWORD 明文提交到仓库。
  2. 如果口令在聊天记录或日志泄露,立即在 Apple ID 页面撤销并重建。
  3. 可使用 notarytool store-credentials 存到钥匙串,减少明文环境变量暴露。
xcrun notarytool store-credentials "ac-notary" \
  --apple-id "$APPLE_ID" \
  --team-id "$APPLE_TEAM_ID" \
  --password "$APPLE_PASSWORD"

xcrun notarytool submit "$ZIP" --keychain-profile "ac-notary" --wait

按本文流程执行后,最终验证标准只有两条:

  1. spctl --assess 返回 accepted
  2. 输出含 source=Notarized Developer ID

达到这两条就可以安全发布到 GitHub Releases。