首页 / Wails 入门教程 / macOS 打包、签名与公证

Wails 入门教程

macOS 打包、签名与公证

本教程共 42 篇 · 第 35 篇 · 更新于 2026-08-03

Wails桌面开发macOS代码签名公证App Store

35. macOS 打包、签名与公证

本节目标

  • 看懂 .app 包的目录结构,会改 Info.plist 里的元数据
  • 明白 Gatekeeper 为什么会拦下你的应用,签名和公证各自解决什么
  • 会用 codesign 在本地签名,用 notarytool 提交公证
  • 会把 .app 打成 .dmg 分发
  • 了解提交 Mac App Store 多出来的那几步

35-1 .app 包里有什么

在 macOS 上执行构建:

wails build -platform darwin/universal -clean

build/bin 里会出现一个 MyApp.app。Finder 里它显示成一个应用图标,实际上是个目录。用终端 ls 进去看看:

MyApp.app/
└── Contents/
    ├── Info.plist          # 应用元数据
    ├── MacOS/
    │   └── MyApp           # 真正的可执行文件
    └── Resources/
        └── iconfile.icns   # 应用图标

Info.plist 是核心。它告诉系统这个应用叫什么、Bundle ID 是多少、支持的最低系统版本、图标文件名等。Wails 用项目里的 build/darwin/Info.plist 作为模板生成它。

一份典型的配置:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>CFBundleName</key><string>MyApp</string>
    <key>CFBundleExecutable</key><string>MyApp</string>
    <key>CFBundleIdentifier</key><string>com.example.myapp</string>
    <key>CFBundleVersion</key><string>1.2.0</string>
    <key>CFBundleShortVersionString</key><string>1.2.0</string>
    <key>CFBundleIconFile</key><string>iconfile</string>
    <key>LSMinimumSystemVersion</key><string>10.13.0</string>
    <key>NSHighResolutionCapable</key><string>true</string>
    <key>LSApplicationCategoryType</key><string>public.app-category.utilities</string>
    <key>NSHumanReadableCopyright</key><string>© 2026 码上学</string>
</dict></plist>

CFBundleIdentifier 要跟后面签名、公证用的 Bundle ID 完全一致,写错会在公证环节被打回。

Note

同目录下还有一个 Info.dev.plist,那是 wails dev 用的。wails build 只读 Info.plist。改元数据别改错文件。

想调整支持的最低 macOS 版本,通过环境变量传给编译器:

CGO_CFLAGS=-mmacosx-version-min=10.15.0 \
CGO_LDFLAGS=-mmacosx-version-min=10.15.0 \
wails build

35-2 Gatekeeper 与两道关卡

把没签名的 .app 发给别人,对方双击会看到「无法打开,因为无法验证开发者」。这是 Gatekeeper 在拦截。

要让应用干净地打开,得过两道关:

代码签名(codesign) —— 用 Apple 颁发的开发者证书给应用打上数字签名,证明「这是某个已知开发者发布的,且发布后没被篡改」。

公证(notarization) —— 把签名后的应用上传给 Apple,让苹果的服务器扫一遍恶意代码,通过后返回一张「票据」。macOS 10.15 之后,从网上下载的应用没有公证票据一样会被拦。

两步都做完,用户双击就是正常打开,什么提示都没有。

Warning

签名和公证都需要 Apple Developer Program 账号,年费 99 美元。这是硬门槛,绕不过去。做个人小工具、不介意用户右键「打开」一次的话,直接发 zip 也可以,但要在说明里写清楚操作步骤。

35-3 拿到证书并确认身份

在开发者后台生成 Developer ID Application 证书(用于 App Store 之外的分发),下载后双击导入钥匙串。

导入完,确认一下本机能看到它:

security find-identity -v -p codesigning

输出类似:

1) 00000000000000000000000000000000000000000 "Developer ID Application: Human User (ABCDE12345)"

引号里那一整串就是签名时要用的「身份标识」,括号里的 ABCDE12345 是你的 Team ID,公证时要用。

35-4 本地签名

签名前先准备一份授权文件(entitlements),声明应用需要的系统能力。放在 build/darwin/entitlements.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.files.downloads.read-write</key>
    <true/>
</dict>
</plist>

按需增删。应用要用摄像头就加摄像头的键,不用网络就把网络那两条删掉。声明多余的能力会让审核和用户都起疑。

然后签名:

codesign --timestamp --options=runtime \
  -s "Developer ID Application: Human User (ABCDE12345)" \
  -v --entitlements ./build/darwin/entitlements.plist \
  ./build/bin/MyApp.app

参数解释:

  • --timestamp 加可信时间戳,证书过期后已签名的应用仍然有效
  • --options=runtime 启用「加固运行时」,这是公证的必要条件,漏了会被打回
  • -s 指定签名身份
  • --entitlements 指定授权文件

签完验证一下:

codesign --verify --deep --strict --verbose=2 ./build/bin/MyApp.app

35-5 提交公证

公证要求上传的是压缩包。用 ditto 打包,它能正确保留 .app 的符号链接和扩展属性:

ditto -c -k --keepParent ./build/bin/MyApp.app ./MyApp.zip
Warning

别用 Finder 右键「压缩」,也别用普通的 zip 命令。它们可能破坏 .app 内部结构,导致公证失败或者解压后签名失效。

提交给 Apple:

xcrun notarytool submit ./MyApp.zip \
  --apple-id "你的AppleID@example.com" \
  --team-id "ABCDE12345" \
  --password "abcd-efgh-ijkl-mnop" \
  --wait

--password 填的不是 Apple ID 登录密码,而是应用专用密码(App-Specific Password),在 Apple ID 账户管理页面生成。--wait 让命令等到结果出来再返回,方便写进脚本。

公证时间从几分钟到几小时不等,看 Apple 服务器的排队情况。

通过之后,把票据「钉」到应用上:

xcrun stapler staple ./build/bin/MyApp.app

钉完之后即使用户断网,Gatekeeper 也能就地验证票据。不钉的话,离线环境下仍会被拦。

Note

旧文档里能看到 altoolgon 这两个工具。Apple 已经把公证迁移到 notarytoolaltool@env: 语法不再可用。新项目直接上 notarytool

35-6 打成 dmg 分发

zip 能用,但 macOS 用户更习惯 dmg——挂载后把图标拖进 Applications 文件夹,这个交互已经是肌肉记忆了。

最简单的做法是用 create-dmg

brew install create-dmg

create-dmg \
  --volname "MyApp" \
  --window-size 600 400 \
  --icon "MyApp.app" 150 200 \
  --app-drop-link 450 200 \
  "MyApp-1.2.0.dmg" \
  "./build/bin/MyApp.app"
Tip

顺序很重要:先签名和公证 .app,再打 dmg。反过来的话,dmg 里装的是没票据的应用,等于白做。讲究一点的话,dmg 本身也可以再签一次名。

35-7 CI 上自动完成

本地跑通之后搬到 GitHub Actions。证书要先导出成 .p12 并转成 base64:

base64 Certificates.p12 | pbcopy

在仓库的 Settings → Secrets 里建三个密钥:

  • APPLE_DEVELOPER_CERTIFICATE_P12_BASE64:base64 编码后的证书
  • APPLE_DEVELOPER_CERTIFICATE_PASSWORD:证书导出时设的密码
  • APPLE_PASSWORD:应用专用密码

工作流的关键步骤:

      - name: Import Code-Signing Certificates
        uses: Apple-Actions/import-codesign-certs@v1
        with:
          p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
          p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}

      - name: Build
        run: wails build -platform darwin/universal -clean

      - name: Sign
        run: |
          codesign --timestamp --options=runtime \
            -s "Developer ID Application: Human User (ABCDE12345)" \
            -v --entitlements ./build/darwin/entitlements.plist \
            ./build/bin/MyApp.app

      - name: Notarize
        run: |
          ditto -c -k --keepParent ./build/bin/MyApp.app ./MyApp.zip
          xcrun notarytool submit ./MyApp.zip \
            --apple-id "${{ secrets.APPLE_ID }}" \
            --team-id "ABCDE12345" \
            --password "${{ secrets.APPLE_PASSWORD }}" \
            --wait
          xcrun stapler staple ./build/bin/MyApp.app

Windows 那条线的签名逻辑类似,用 signtool.exe 配合 base64 存进 Secrets 的 .pfx 证书:

certutil -decode certificate\certificate.txt certificate\certificate.pfx
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' `
  sign /fd sha256 /tr http://时间戳服务器地址 `
  /f certificate\certificate.pfx /p '证书密码' .\build\bin\MyApp.exe

不同证书商对时间戳参数的要求不一样,有的要 /t,有的要 /tr。买证书时问清楚,本地先手动签一次跑通,再搬进 CI。

35-8 提交 Mac App Store

走 App Store 这条路,跟 Developer ID 分发有几处不同。

证书不一样。需要 3rd Party Mac Developer Application(签应用)和 3rd Party Mac Developer Installer(签安装包)两张,都要导入钥匙串。

必须开沙盒entitlements.plist 里的 com.apple.security.app-sandbox 必须为 true,并且要补上这两个键:

    <key>com.apple.application-identifier</key>
    <string>TEAM_ID.APP_NAME</string>
    <key>com.apple.developer.team-identifier</key>
    <string>TEAM_ID</string>

要有描述文件。在开发者后台创建 Mac App Store Distribution 的 Provisioning Profile,下载后重命名为 embedded.provisionprofile,放进 .appContents 目录。

产物是 pkg 不是 dmg。完整脚本:

#!/bin/bash

APP_CERTIFICATE="3rd Party Mac Developer Application: YOUR NAME (CODE)"
PKG_CERTIFICATE="3rd Party Mac Developer Installer: YOUR NAME (CODE)"
APP_NAME="MyApp"

wails build -platform darwin/universal -clean

cp ./embedded.provisionprofile "./build/bin/$APP_NAME.app/Contents"

codesign --timestamp --options=runtime -s "$APP_CERTIFICATE" -v \
  --entitlements ./build/darwin/entitlements.plist \
  "./build/bin/$APP_NAME.app"

productbuild --sign "$PKG_CERTIFICATE" \
  --component "./build/bin/$APP_NAME.app" /Applications \
  "./$APP_NAME.pkg"

生成的 .pkg 用 Transporter(App Store 里免费下载)上传,然后回 App Store Connect 关联版本、提交审核。

Warning

构建 App Store 版本时不能带 -devtools。官方明确说了,带开发者工具的包过不了审核。

常见误区

签名时漏了 --options=runtime 公证会直接失败,错误信息还挺隐晦。加固运行时是公证的硬性前提。

用 Finder 压缩 .app 去公证。 结构被破坏,要么公证失败,要么用户解压后签名验不过。老老实实用 ditto

公证完不 staple。 联网时看着正常,用户断网或网络受限就被拦。养成公证后立刻 staple 的习惯。

先打 dmg 再签名。 顺序反了,dmg 里的应用没有票据。

Bundle ID 前后不一致。 Info.plist、开发者后台注册的 App ID、entitlements 里的 identifier,三处必须对齐。

小结

macOS 分发的门槛主要在签名和公证。流程是固定的四步:codesign 签名 → ditto 打包 → notarytool 提交 → stapler 钉票据。每一步的参数记熟了,写成脚本就一劳永逸。

Info.plist 管元数据,entitlements.plist 管权限声明。前者写错影响展示,后者写错影响审核。

App Store 是另一条路:换证书、开沙盒、加描述文件、产出 pkg 而不是 dmg。

下一章讲 Linux。那边没有签名的烦恼,但换来了另一个麻烦——发行版碎片化和 WebKit 的 ABI 版本问题。