macOS 打包、签名与公证
本教程共 42 篇 · 第 35 篇 · 更新于 2026-08-03
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旧文档里能看到
altool和gon这两个工具。Apple 已经把公证迁移到notarytool,altool的@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,放进 .app 的 Contents 目录。
产物是 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 版本问题。