This tutorial builds macOS apps for distribution outside the Mac App Store: a zipped
.app (macos), a disk image (dmg) and an installer package (pkg), signed with a Developer ID certificate and notarized by Apple so Gatekeeper opens them without warnings. All three build on macOS only and share a single
flutter build macos.
What you need#
- A Mac with Xcode (and CocoaPods when the project has a Podfile).
-
For signing and notarization: a paid Apple Developer Program membership, a Developer ID Application
certificate (and Developer ID Installer for
pkg) in your keychain, and one notarization credential. Certificates and credentials shows how to get each.
You can follow the first steps without any of that.
1. Check the machine#
fpack doctor macos dmg pkg
Doctor checks Xcode, CocoaPods, hdiutil / create-dmg, pkgbuild
and productbuild, lists the signing identities in your keychain and validates provisioning profiles and entitlements.
2. A first, unsigned build#
fpack build macos dmg
With nothing configured, the .app keeps the signature Xcode gave it, the DMG is unsigned, and notes in the summary explain how to enable signing. You get:
dist/1.0.0+1/
my_app-1.0.0+1-macos-universal.zip
my_app-1.0.0+1-macos-universal.dmg
This is fine for testing on your own Mac. On other Macs, Gatekeeper blocks unsigned, un-notarized downloads: continue with step 3.
Tip
For quick local packages after signing is set up, add --no-notarize (skips the Apple upload) or
--no-sign (skips signing and notarization).
3. Configure signing and notarization#
Signing is enabled only through fpack's own settings: macos.sign in fpack.yaml,
FPACK_MACOS_* variables, or flags. Highest precedence last: file → variables → flags.
macos:
sign:
identity: "Developer ID Application: Your Name (ABCDE12345)" # turns signing on
notary_profile: fpack-notary # turns notarization on
The rules:
-
an
identityturns signing on; any notary credential turns notarization on; turning signing off also turns notarization off; -
--signwithout an identity picks the first "Developer ID Application" certificate in your keychain; a missing identity lists the available ones; -
--notarize(ornotarize: true) without any credential uses the keychain profile namedNotaryProfile; -
signing uses the Hardened Runtime (
macos.sign.hardened_runtime, on by default, required for notarization) and the entitlements inmacos/Runner/Release.entitlements(ormacos.sign.entitlements); -
fpack does not read other tools' settings in
pubspec.yaml, such as thedmg:section of thedmgpackage.
There are three kinds of notarization credential, in priority order: a keychain profile (notary_profile, best on your own Mac), an App Store Connect API key (notary_api_key,
notary_api_key_id, notary_api_issuer, best for CI) and an Apple ID with an app-specific password (notary_apple_id,
notary_team_id, notary_password). How to create each is on Certificates and credentials.
| Setting | fpack.yaml | Variable | Flag |
|---|---|---|---|
| signing on / off | macos.sign.enabled |
FPACK_MACOS_SIGN |
--sign, --no-sign |
| app identity | macos.sign.identity |
FPACK_MACOS_SIGN_IDENTITY |
--sign-identity |
| pkg identity | macos.sign.installer_identity |
FPACK_MACOS_INSTALLER_IDENTITY |
--installer-identity |
| notarization on / off | macos.sign.notarize |
FPACK_MACOS_NOTARIZE |
--notarize, --no-notarize |
| keychain profile | macos.sign.notary_profile |
FPACK_MACOS_NOTARY_PROFILE |
--notary-profile |
| provisioning profile | macos.sign.provisioning_profile |
FPACK_MACOS_PROVISIONING_PROFILE |
--provisioning-profile |
| smoke test | macos.smoke_test |
FPACK_MACOS_SMOKE_TEST |
--smoke-test |
4. Build, sign and notarize#
fpack build macos dmg
For a DMG, fpack copies the .app, signs the nested code with codesign --deep, re-signs the app with the Hardened Runtime and entitlements, verifies it, builds the disk image, signs the DMG, uploads it with
xcrun notarytool submit, waits for Apple, staples the ticket with stapler staple
and checks the result with spctl. For the zip (macos), the zip is submitted, the
.app is stapled, then it is zipped again.
5. Waiting for notarization#
Apple usually needs a few minutes. fpack shows the elapsed time and writes two files to the output folder as soon as the upload has a submission id:
NOTARIZATION.md (artifact, SHA-256, submission id, the credential used but never a password, and copy-paste commands) and
notarization.json.
Note
Ctrl-C stops waiting; Apple keeps processing. You can continue later, from any shell on this Mac.
You can also not wait at all:
fpack build dmg --notarize-no-wait # submit, write the record, finish
fpack notarize status # ask Apple about every pending submission
fpack notarize finish # wait, staple accepted files, update SHA256SUMS
-
fpack notarize status [dir|ID]reports the state of each submission recorded innotarization.json(or of one submission id). -
fpack notarize finish [dir]waits for the results, staples accepted DMGs and pkgs (an app zip is unpacked, the app stapled and zipped again), rewritesSHA256SUMS, and for rejected submissions downloads Apple's log (notary-log-<file>.json) and summarizes the issues. -
The same behavior as
--notarize-no-waitcan be set withmacos.notarize.wait: falseorFPACK_NOTARIZE_WAIT=false. The summary and--jsonthen reportnotarization.state: submitted. - Apple ID submissions need
FPACK_NOTARY_PASSWORDset when you runnotarize.
A rejected submission (Invalid) usually means unsigned nested code or a missing Hardened Runtime; the summarized log says which file.
6. Verify the result#
spctl --assess --type open --context context:primary-signature -vv dist/1.0.0+1/my_app-1.0.0+1-macos-universal.dmg
xcrun stapler validate dist/1.0.0+1/my_app-1.0.0+1-macos-universal.dmg
codesign --verify --deep --strict --verbose=2 /Applications/MyApp.app
7. Make the DMG yours#
fpack builds DMGs with hdiutil by default and with create-dmg when it is installed or when you set layout keys (macos.dmg.tool: auto). Force one with
--dmg-tool hdiutil|create-dmg.
macos:
dmg:
volume_name: My App
background: macos/dmg/background.png
window_size: [660, 400]
window_position: [200, 120]
icon_size: 128
app_position: [180, 190]
applications_position: [480, 190]
format: ULFO
volume_icon and license (a license agreement shown on mount) are also available;
format and filesystem work with both tools. Install create-dmg
with brew install create-dmg.
8. A pkg installer#
fpack build pkg
fpack runs pkgbuild to create a component package that installs the app into /Applications
(not relocatable, so upgrades replace the copy there), then productbuild to create a distribution package that supports Apple silicon and Intel without a Rosetta prompt.
macos:
pkg:
identifier: com.example.app.pkg
title: My App
welcome: macos/installer/welcome.html
license: macos/installer/license.rtf
min_os: "11.0"
postinstall: macos/installer/postinstall.sh
-
Signing: a pkg needs a separate Developer ID Installer certificate (
macos.sign.installer_identity,--installer-identity); the result is checked withpkgutil --check-signature. Without it you get an unsigned pkg and a note;--no-signturns pkg signing off. -
Notarization: shares the DMG settings; only signed pkgs are submitted and stapled, then assessed with
spctl --assess --type install. -
More options:
readme,conclusion,background,install_location,version,preinstall,require_restart,relocatable(scripts are made executable automatically).min_osdefaults to the project'sMACOSX_DEPLOYMENT_TARGET.
9. Apps with push, iCloud or other restricted entitlements#
If your app uses restricted entitlements (push notifications, iCloud, associated domains, keychain access groups), a Developer ID provisioning profile is required, otherwise the signed app will not launch. Read
Provisioning profiles; add --smoke-test to catch it before notarization.
Flavors and CI#
Flavors are Xcode schemes (Flavors). For signed builds in CI you import certificates into a temporary keychain: export the certificates for CI has the full workflow.
If something goes wrong#
| Message | Fix |
|---|---|
signing identity … not found in the keychain |
use one of the listed identities exactly as printed, or --no-sign |
notarization Invalid |
read the summarized Apple log; usually unsigned nested code or missing Hardened Runtime |
| upload timeouts while notarizing | fpack retries the upload twice; check the network and build again (nothing was submitted) |
the signed app does not launch (RBSRequestErrorDomain Code=5) |
restricted entitlements without a Developer ID profile: Provisioning profiles |
| pkg is unsigned | set macos.sign.installer_identity (a different certificate from the app's) |
More on the troubleshooting page.
本教程为在 Mac App Store 之外分发的 macOS 应用构建:压缩的 .app(macos)、磁盘映像(dmg)和安装包(pkg),用 Developer ID 证书签名并由 Apple 公证,这样 Gatekeeper 打开时不会有警告。三者都只能在 macOS 上构建,并共用一次
flutter build macos。
你需要什么#
- 装有 Xcode 的 Mac(工程有 Podfile 时还需要 CocoaPods)。
-
签名与公证:付费的 Apple Developer Program 会员资格、钥匙串里的 Developer ID Application 证书(
pkg还需要 Developer ID Installer)以及一种公证凭证。每一项怎么获取见证书与凭证。
前面几步不需要这些就能做。
1. 检查机器#
fpack doctor macos dmg pkg
doctor 检查 Xcode、CocoaPods、hdiutil / create-dmg、pkgbuild 和 productbuild,列出钥匙串里的签名证书,并校验描述文件和 entitlements。
2. 先构建一次(未签名)#
fpack build macos dmg
什么都没配置时,.app 保持 Xcode 给它的签名,DMG 不签名,汇总里的提示会说明如何开启签名。你会得到:
dist/1.0.0+1/
my_app-1.0.0+1-macos-universal.zip
my_app-1.0.0+1-macos-universal.dmg
在你自己的 Mac 上测试没问题。在别的 Mac 上,Gatekeeper 会拦截未签名、未公证的下载文件:继续第 3 步。
提示
配置好签名之后,想快速打本地包,可以加 --no-notarize(跳过上传 Apple)或 --no-sign(跳过签名和公证)。
3. 配置签名与公证#
只有通过 fpack 自己的设置才会启用签名:fpack.yaml 的 macos.sign、FPACK_MACOS_* 变量或命令行参数。优先级从低到高:文件 → 变量 → 参数。
macos:
sign:
identity: "Developer ID Application: Your Name (ABCDE12345)" # 设置后即启用签名
notary_profile: fpack-notary # 设置后即启用公证
规则:
- 设置了
identity即启用签名;设置了任一公证凭证即启用公证;关闭签名同时关闭公证; --sign不带证书时,自动选用钥匙串中第一个 “Developer ID Application” 证书;找不到证书时会列出可用的;-
--notarize(或notarize: true)没有任何凭证时,使用名为NotaryProfile的钥匙串配置; -
签名使用 Hardened Runtime(
macos.sign.hardened_runtime,默认开启,公证必需)和macos/Runner/Release.entitlements(或macos.sign.entitlements)里的 entitlements; - fpack 不读取其他工具在
pubspec.yaml里的设置,例如dmg包的dmg:段。
公证凭证有三种,按优先级:钥匙串配置(notary_profile,本机最推荐)、App Store Connect API 密钥(notary_api_key、notary_api_key_id、notary_api_issuer,CI 最推荐)、Apple ID 加 App 专用密码(notary_apple_id、notary_team_id、notary_password)。每一种怎么创建见证书与凭证。
| 设置 | fpack.yaml | 变量 | 参数 |
|---|---|---|---|
| 开启 / 关闭签名 | macos.sign.enabled |
FPACK_MACOS_SIGN |
--sign、--no-sign |
| App 证书 | macos.sign.identity |
FPACK_MACOS_SIGN_IDENTITY |
--sign-identity |
| pkg 证书 | macos.sign.installer_identity |
FPACK_MACOS_INSTALLER_IDENTITY |
--installer-identity |
| 开启 / 关闭公证 | macos.sign.notarize |
FPACK_MACOS_NOTARIZE |
--notarize、--no-notarize |
| 钥匙串配置 | macos.sign.notary_profile |
FPACK_MACOS_NOTARY_PROFILE |
--notary-profile |
| 描述文件 | macos.sign.provisioning_profile |
FPACK_MACOS_PROVISIONING_PROFILE |
--provisioning-profile |
| 冒烟测试 | macos.smoke_test |
FPACK_MACOS_SMOKE_TEST |
--smoke-test |
4. 构建、签名、公证#
fpack build macos dmg
对 DMG,fpack 会复制 .app、用 codesign --deep 给内嵌代码签名、用 Hardened Runtime 和 entitlements 给 App 重新签名并验证,然后制作磁盘映像、给 DMG 签名、用
xcrun notarytool submit 上传、等待 Apple 的结果、用 stapler staple 装订票据,最后用 spctl
检查。对 zip(macos),提交的是 zip,装订 .app 后再重新压缩。
5. 等待公证#
Apple 通常需要几分钟。fpack 会显示已等待的时间,并在上传拿到提交 ID 之后立刻向输出目录写入两个文件:NOTARIZATION.md(产物、SHA-256、提交 ID、使用的凭证(但绝不含密码)以及可直接复制的命令)和
notarization.json。
说明
Ctrl-C 只是停止等待,Apple 仍在处理。你可以稍后在这台 Mac 的任何 shell 里继续。
也可以完全不等待:
fpack build dmg --notarize-no-wait # 提交、写入记录、结束
fpack notarize status # 向 Apple 查询所有未完成的提交
fpack notarize finish # 等待、给通过的文件装订、更新 SHA256SUMS
fpack notarize status [目录|ID]报告notarization.json中每个提交的状态(或某一个提交 ID)。-
fpack notarize finish [目录]等待结果,给通过的 DMG 和 pkg 装订票据(app zip 会先解压,装订 app 后重新压缩),重写SHA256SUMS;对被拒绝的提交下载 Apple 的日志(notary-log-<文件>.json)并总结问题。 -
用
macos.notarize.wait: false或FPACK_NOTARIZE_WAIT=false也能得到与--notarize-no-wait相同的行为。此时汇总和--json报告notarization.state: submitted。 - 用 Apple ID 提交时,运行
notarize需要设置FPACK_NOTARY_PASSWORD。
提交被拒绝(Invalid)通常是内嵌代码没有签名或缺少 Hardened Runtime;总结后的日志会指出是哪个文件。
6. 验证结果#
spctl --assess --type open --context context:primary-signature -vv dist/1.0.0+1/my_app-1.0.0+1-macos-universal.dmg
xcrun stapler validate dist/1.0.0+1/my_app-1.0.0+1-macos-universal.dmg
codesign --verify --deep --strict --verbose=2 /Applications/MyApp.app
7. 定制 DMG#
fpack 默认用 hdiutil 制作 DMG;create-dmg 已安装或设置了布局键时改用它(macos.dmg.tool: auto)。用
--dmg-tool hdiutil|create-dmg 强制指定其一。
macos:
dmg:
volume_name: My App
background: macos/dmg/background.png
window_size: [660, 400]
window_position: [200, 120]
icon_size: 128
app_position: [180, 190]
applications_position: [480, 190]
format: ULFO
还可以用 volume_icon 和 license(挂载时显示的许可协议);format 和 filesystem
对两种工具都有效。用 brew install create-dmg 安装 create-dmg。
8. pkg 安装包#
fpack build pkg
fpack 用 pkgbuild 创建把应用安装到 /Applications 的组件包(不可重定位,所以升级总是替换 /Applications 里的那份),再用
productbuild 创建分发包,同时支持 Apple 芯片和 Intel,不会弹出 Rosetta 提示。
macos:
pkg:
identifier: com.example.app.pkg
title: My App
welcome: macos/installer/welcome.html
license: macos/installer/license.rtf
min_os: "11.0"
postinstall: macos/installer/postinstall.sh
-
签名:pkg 需要单独的 Developer ID Installer 证书(
macos.sign.installer_identity、--installer-identity),结果会用pkgutil --check-signature检查。没有它会生成未签名的 pkg 并给出提示;--no-sign会关闭 pkg 签名。 - 公证:与 DMG 共用设置;只有已签名的 pkg 才会提交和装订,然后用
spctl --assess --type install评估。 -
其他选项:
readme、conclusion、background、install_location、version、preinstall、require_restart、relocatable(脚本会自动设为可执行)。min_os默认是工程的MACOSX_DEPLOYMENT_TARGET。
9. 使用推送、iCloud 等受限 entitlements 的应用#
如果应用使用了受限 entitlements(推送通知、iCloud、关联域名、钥匙串访问组),就必须有 Developer ID 描述文件,否则签名后的应用无法启动。阅读描述文件;加上
--smoke-test 可以在公证之前发现问题。
Flavor 与 CI#
Flavor 是 Xcode scheme(Flavor)。在 CI 里做签名构建需要把证书导入临时钥匙串:为 CI 导出证书有完整的工作流。
出问题时#
| 提示 | 处理 |
|---|---|
signing identity … not found in the keychain |
使用列出的证书,名称与打印的完全一致,或用 --no-sign |
公证 Invalid | 阅读总结后的 Apple 日志;通常是内嵌代码未签名或缺少 Hardened Runtime |
| 公证上传超时 | fpack 会重试上传两次;检查网络后重新构建(没有提交任何内容) |
签名后的应用无法启动(RBSRequestErrorDomain Code=5) |
使用了受限 entitlements 却没有 Developer ID 描述文件:描述文件 |
| pkg 未签名 | 设置 macos.sign.installer_identity(与 App 证书不同的证书) |
更多见故障排查页。