Flutter 基础体系 · 第 77/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。
Flutter 桌面发布:Windows、macOS、Linux 打包、签名和更新
Flutter 桌面发布不是一个单独的 flutter build 命令,而是一条由多个阶段组成的交付链:
Dart/Flutter 源码
│
├─ 编译:生成目标平台的原生可执行文件和资源
│
├─ 打包:组织为应用目录、安装包或应用商店格式
│
├─ 签名:证明发布者身份,并保护安装包或应用未被篡改
│
├─ 分发:网站、应用商店、软件仓库或企业内网
│
└─ 更新:发现新版本、验证、安装、迁移和失败恢复
这几个阶段解决的问题不同:
- 编译解决“程序能否在目标平台运行”。
- 打包解决“用户如何安装和启动”。
- 签名解决“用户是否能确认来源,以及操作系统是否允许运行”。
- 分发解决“用户从哪里获得程序”。
- 更新解决“已安装用户如何安全地迁移到新版本”。
Flutter 官方文档和 API 文档分别提供了桌面支持、构建命令和 Dart/Flutter API 的规范说明;但安装包格式、证书申请、应用商店审核和自动更新机制还依赖 Windows、macOS、Linux 各自的生态。
一、先区分 Flutter 桌面发布中的几个对象
1. Flutter 桌面应用不是 Dart 脚本
在 Windows、macOS 和 Linux 上,Flutter 使用 Dart AOT 编译器将 Dart 代码编译成目标平台可执行代码,并通过 Flutter Engine、平台 Runner 和原生插件访问系统能力。
一个桌面构建产物通常包含:
应用目录/
├── 主可执行文件
├── Flutter 引擎动态库或框架
├── flutter_assets/
│ ├── AssetManifest.bin
│ ├── FontManifest.json
│ ├── fonts/
│ └── assets/
└── 插件所需的原生动态库
因此,发布时不能只复制主可执行文件。缺少 Flutter 引擎、资源目录或插件动态库时,常见结果包括:
- 程序双击后立即退出;
- 启动时报找不到 DLL、动态库或共享对象;
- 图片、字体、国际化资源加载失败;
- 某个插件初始化失败。
flutter build windows、flutter build macos 和 flutter build linux 的默认产物本质上是可运行的应用目录,而不是天然适合终端用户安装的安装包。
2. 构建、打包和安装包不是同一件事
下面三种产物必须区分:
| 类型 | 作用 | 示例 |
|---|---|---|
| 应用目录 | 直接运行或作为打包输入 | Windows Release 目录、.app、Linux bundle |
| 分发归档 | 便于上传和下载 | .zip、.tar.gz、.dmg |
| 安装包 | 负责安装、卸载、快捷方式、权限和升级 | .msix、.msi、.pkg、.deb、.rpm |
例如,macOS 的 .app 是一个目录包,用户可以直接双击;.dmg 只是一个磁盘映像,通常用于承载 .app,它不是 macOS 唯一的安装格式。Linux 的 AppImage、Flatpak、Debian 包和 RPM 包也代表不同的分发模型。
3. 版本号必须先设计
通常在 pubspec.yaml 中声明版本:
name: desk_notes
description: A desktop note application
version: 1.4.2+37
这里:
1.4.2是面向用户的版本名,通常遵循主版本、次版本、修订版本;37是构建号,适合用于构建流水线和商店后台;+后面的构建号不是所有平台最终显示方式都完全相同。
版本号至少要满足两个条件:
- 用户能判断新旧版本;
- 更新系统能判断哪个版本更高。
若发布系统使用版本比较,版本 1.10.0 不能被错误地按字符串排序为低于 1.9.0。更新服务应解析数字段,而不是直接比较字符串。
在 CI 中可以通过命令覆盖版本:
flutter build windows --release --build-name=1.4.2 --build-number=37
不同 Flutter 版本和平台对构建参数的具体映射可能存在差异,因此最终应检查目标平台产物中的真实版本字段,而不能只依赖命令成功。
二、准备桌面构建环境
1. 检查 Flutter 和平台工具链
先确认 Flutter 版本、Dart 版本以及平台工具链:
flutter --version
flutter doctor -v
flutter devices
预期结果应包含相应桌面设备,例如:
Windows (desktop) • windows • windows-x64
macOS (desktop) • macos • darwin-arm64
Linux (desktop) • linux • linux-x64
flutter doctor -v 的结果不是“能否发布”的抽象评分,而是对工具链依赖的诊断。典型依赖包括:
- Windows:Visual Studio 的 C++ 桌面开发组件、Windows SDK;
- macOS:Xcode、命令行工具、有效的开发者工具链;
- Linux:CMake、Ninja、GTK 开发包和编译器;
- 所有平台:Flutter SDK、Dart SDK、项目依赖可解析。
桌面平台必须在 Flutter 中启用:
flutter config --enable-windows-desktop
flutter config --enable-macos-desktop
flutter config --enable-linux-desktop
当前稳定版 Flutter 通常已默认支持这些平台,但命令是幂等的,适合放入环境初始化脚本。启用平台支持不等于当前机器具备该平台的编译器。例如,在 Linux 上启用 macOS 桌面并不能替代 macOS 和 Xcode。
2. 桌面通常不能跨平台本地构建
Flutter 的桌面构建依赖目标平台的原生工具链:
- Windows 构建 Windows 版本;
- macOS 构建 macOS 版本;
- Linux 构建 Linux 版本。
尤其是 macOS 签名和公证必须在 Apple 的工具链环境中完成。工程实践中通常为每个平台配置独立 CI Runner,而不是试图在一个操作系统上构建所有桌面版本。
三、Windows:构建、MSIX、代码签名和更新
3.1 构建 Windows Release 目录
执行:
flutter clean
flutter pub get
flutter test
flutter build windows --release
典型产物位于:
build\windows\x64\runner\Release\
目录名称可能随 Flutter 版本和目标架构变化,例如 x64、arm64。应以构建输出为准,不要在脚本中无条件假设固定路径。
可以直接启动主程序验证:
.\build\windows\x64\runner\Release\desk_notes.exe
验证重点包括:
- 在没有 Flutter SDK 的干净 Windows 环境中能否启动;
- 所有 Flutter 资源是否存在;
- 原生插件 DLL 是否随应用分发;
- x64、ARM64 和目标 Windows 版本是否匹配;
- 杀毒软件或 SmartScreen 是否报告来源问题。
Release 构建会启用面向发布的编译方式,但它仍然只是一个应用目录。把整个 Release 目录压缩后分发可以运行,却缺少安装、卸载、快捷方式和系统注册等能力。
3.2 Windows 安装包格式
Windows 常见分发格式有:
- MSIX:微软现代应用包格式,支持清晰的包身份、签名、安装和卸载;
- MSI:传统 Windows Installer 格式,适合企业部署和已有安装体系;
- EXE 安装器:由 Inno Setup、WiX Burn、NSIS 等工具生成;
- ZIP:便携版,不负责安装和卸载。
Flutter 本身负责生成 Windows 应用,通常不负责为所有安装器格式提供完整的官方打包流程。生成 MSIX、MSI 或 EXE 安装器时,应使用对应打包工具,并明确工具版本和许可证条件。
MSIX 的核心约束
MSIX 包中包含应用清单。清单会声明:
- 包身份和发布者;
- 应用版本;
- 支持的架构;
- 入口程序;
- 显示名称和图标;
- 能力声明。
MSIX 必须签名。测试阶段可以创建自签名证书,但普通用户的系统不默认信任它;生产环境则应使用受信任的代码签名证书,或通过企业设备管理分发根证书。
一个常见的测试签名流程如下,命令仅适用于测试证书场景:
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=Example Test Publisher" `
-CertStoreLocation "Cert:\CurrentUser\My"
$certPath = "Cert:\CurrentUser\My\$($cert.Thumbprint)"
$password = ConvertTo-SecureString "change-this-password" `
-AsPlainText -Force
Export-PfxCertificate `
-Cert $certPath `
-FilePath .\test-publisher.pfx `
-Password $password
之后由 MSIX 打包工具使用该 PFX 签名。测试机器还要把证书导入“受信任的根证书颁发机构”或相应信任位置,否则安装时可能出现:
The certificate chain processed, but terminated in a root certificate which is not trusted.
这不是 Flutter 运行时错误,而是 Windows 不信任发布者证书链。
生产证书的私钥不能放入公开仓库,也不应以明文文件长期存储在 CI 工作目录。常见做法是:
- 证书存放在 CI 的密钥库或硬件密钥服务中;
- 构建时临时注入;
- 使用后删除临时文件;
- 对签名操作和证书访问进行审计。
3.3 Windows Authenticode 签名
Authenticode 是 Windows 用于验证可执行文件、DLL 和安装程序发布者及完整性的签名机制。它解决的是“文件是否由某个证书持有者签署、签署后是否被修改”,不是“程序一定安全”。
可以使用 Windows SDK 中的 signtool:
signtool sign `
/fd SHA256 `
/a `
/tr http://timestamp.digicert.com `
/td SHA256 `
.\DeskNotesSetup.exe
参数含义:
/fd SHA256:文件摘要算法;/a:自动选择合适的证书;/tr:使用 RFC 3161 时间戳服务;/td SHA256:时间戳摘要算法。
签名后验证:
signtool verify /pa /all /v .\DeskNotesSetup.exe
成功的验证应能看到签名证书、证书链和摘要验证结果。生产签名失败时,重点检查:
- 证书是否包含代码签名用途;
- 证书链是否完整;
- 时间戳服务是否可访问;
- 签名后是否又修改了文件;
- 安装器内部嵌入的可执行文件是否也需要单独签名。
如果先签名再修改二进制文件,签名会失效。安装器通常还需要对嵌入的 EXE、DLL 和最终安装器按正确顺序签名。
SmartScreen 与代码签名不是一回事
即使 Authenticode 验证通过,SmartScreen 仍可能警告“Windows protected your PC”。这是因为 SmartScreen 还会考虑文件来源、下载标记、发布者信誉和样本信誉。购买证书不能保证所有新发布文件立即没有警告。
因此,生产验证应分别执行:
签名完整性验证
+
干净机器安装验证
+
SmartScreen/企业策略下的实际行为验证
不要把“签名成功”解释为“所有用户都不会看到安全警告”。
3.4 Windows 更新
Windows 更新方式取决于分发格式:
MSIX 更新
MSIX 可以通过 Microsoft Store、App Installer 或企业分发体系提供更新。更新系统会依据包身份和版本选择新包,因此:
- 包身份不能随意改变;
- 发布者身份必须保持兼容;
- 新版本应满足平台要求的版本递增规则;
- 更新包必须重新签名。
EXE/MSI 更新
自建安装器通常需要:
- 下载新安装器;
- 验证下载文件的哈希或签名;
- 退出正在运行的旧程序;
- 执行安装或替换;
- 重新启动;
- 失败时恢复旧版本。
不要让正在运行的程序直接覆盖自己的主 EXE。Windows 文件锁、杀毒软件扫描和并发启动都可能使替换不完整。更可靠的方式是让独立的更新进程或安装器执行替换。
一个更新清单可以包含:
{
"channel": "stable",
"version": "1.4.2",
"build": 37,
"url": "https://download.example.com/desk-notes-1.4.2.msix",
"sha256": "hex-encoded-sha256",
"signature": "detached-signature-or-signed-manifest"
}
仅校验 SHA-256 只能发现传输或存储损坏;如果攻击者可以同时替换文件和清单,哈希本身不能证明发布者身份。生产更新应使用 HTTPS,并对清单或包使用受信任的签名。
四、macOS:应用包、代码签名、沙盒和公证
4.1 构建 .app
执行:
flutter clean
flutter pub get
flutter test
flutter build macos --release
典型产物:
build/macos/Build/Products/Release/DeskNotes.app
.app 不是普通单文件,而是一个具有约定目录结构的 macOS 应用包,内部通常包含:
DeskNotes.app/
├── Contents/
│ ├── Info.plist
│ ├── MacOS/
│ │ └── DeskNotes
│ ├── Frameworks/
│ │ └── FlutterMacOS.framework
│ ├── Resources/
│ └── _CodeSignature/
macOS 通过 Info.plist 获得应用标识、版本、入口程序和权限声明。应用 Bundle Identifier,例如:
com.example.desknote
是应用身份的重要部分。改变它可能导致钥匙串数据、沙盒容器、更新渠道和商店条目被视为不同应用。
4.2 macOS 架构和 Universal Binary
现代 Mac 可能使用 Intel x86_64 或 Apple Silicon arm64。可以分别构建,也可以发布 Universal 产物。
检查二进制架构:
file build/macos/Build/Products/Release/DeskNotes.app/Contents/MacOS/DeskNotes
可能输出:
Mach-O 64-bit executable arm64
或:
Mach-O universal binary with 2 architectures
如果依赖的原生插件只提供单一架构,Flutter 应用即使主程序支持双架构,也不能真正成为可运行的 Universal 应用。应检查每个嵌套框架和动态库:
find DeskNotes.app -type f \( -name "*.dylib" -o -name "*.framework" \) -print
架构不匹配通常表现为启动失败,或系统提示无法打开应用,而不是 Dart 代码异常。
4.3 macOS 代码签名
macOS 代码签名使用开发者证书和 codesign。签名覆盖应用代码、嵌套框架、动态库、资源和权限声明。
查看可用身份:
security find-identity -v -p codesigning
签名验证:
codesign --verify --deep --strict --verbose=2 DeskNotes.app
spctl --assess --type execute --verbose=4 DeskNotes.app
这里需要区分两个动作:
codesign --verify检查签名结构和完整性;spctl模拟 Gatekeeper 评估,涉及系统认可的签名、公证和策略。
生产签名不应简单依赖 codesign --deep 作为“自动修复一切”的方案。--deep 可以帮助验证嵌套代码,但可能掩盖嵌套组件没有按正确身份、权限和顺序签名的问题。更稳妥的流程是:
- 先确定应用中的所有嵌套代码;
- 按内层到外层签名;
- 为需要 Hardened Runtime 的应用设置正确选项;
- 最后签名
.app; - 再验证整个包。
示意命令如下:
codesign --force --options runtime \
--timestamp \
--sign "Developer ID Application: Example Company (TEAMID)" \
DeskNotes.app/Contents/Frameworks/SomePlugin.dylib
codesign --force --options runtime \
--timestamp \
--sign "Developer ID Application: Example Company (TEAMID)" \
DeskNotes.app
实际项目中嵌套对象的签名顺序和权限文件由依赖结构决定,不能无条件套用上述路径。
Entitlements 会改变运行时权限
macOS 的 entitlements 是签名时写入的权限声明,例如:
- App Sandbox;
- 网络客户端;
- 文件访问;
- Keychain Access Groups;
- Hardened Runtime 的例外能力。
Info.plist 中的使用说明和 entitlements 不是同一件事。比如摄像头、麦克风等隐私权限通常还需要在 Info.plist 中提供用途说明;缺少说明时,系统可能拒绝访问或应用审核失败。
权限越多,审核和攻击面越复杂。权限文件应只包含实际需要的能力,不应为了“先让功能工作”而开放过宽的访问范围。
4.4 公证(Notarization)
公证是 Apple 对提交的应用包进行自动化安全检查并记录结果的服务。它不同于代码签名:
- 签名证明文件由哪个开发者身份签署;
- 公证表示 Apple 的服务接受了该构建用于分发的检查结果;
- Gatekeeper 通常综合签名、公证票据、隔离属性和系统策略决定是否放行。
常见外部分发流程:
构建 .app
→ 代码签名
→ 压缩为 ZIP 或制作 DMG
→ 上传 notarytool
→ 等待公证结果
→ staple 写入票据
→ 在干净 Mac 验证
压缩并提交:
ditto -c -k --keepParent \
build/macos/Build/Products/Release/DeskNotes.app \
DeskNotes.zip
xcrun notarytool submit DeskNotes.zip \
--keychain-profile "notary-profile" \
--wait
公证成功后,将票据固定到应用:
xcrun stapler staple DeskNotes.app
xcrun stapler validate DeskNotes.app
验证:
spctl --assess --type execute --verbose=4 DeskNotes.app
codesign --verify --deep --strict --verbose=2 DeskNotes.app
notarytool 的认证信息可以通过 App Store Connect API Key 或 Keychain Profile 管理。不要把 Apple 私钥、团队凭据和证书密码写入仓库。
如果公证失败,应先查询提交日志:
xcrun notarytool log <submission-id> \
--keychain-profile "notary-profile"
常见原因包括:
- 未签名或签名身份不正确的嵌套动态库;
- Hardened Runtime 或 entitlements 不一致;
- 包含不允许的未签名脚本或二进制;
- 应用包在签名后被修改;
- 使用了不适合分发的临时开发签名。
4.5 DMG、PKG 和 App Store
制作简单 DMG 的示例:
hdiutil create \
-volname "Desk Notes" \
-srcfolder DeskNotes.app \
-ov \
-format UDZO \
DeskNotes-1.4.2.dmg
如果 DMG 作为下载给用户,应在 DMG 生成后对 DMG 本身签名或按当前分发流程公证验证;不能假设“内部 .app 公证过,所以任何后来生成的 DMG 都自动完成分发验证”。
macOS 还可以使用:
.pkg安装器;- Mac App Store;
- 企业 MDM;
- 直接下载
.dmg或.zip。
Mac App Store 需要 App Store Connect、相应的分发证书、Provisioning Profile 和沙盒能力。App Store 分发与 Developer ID 外部分发不是同一套签名身份和审核路径。Flutter 生成 .app 之后,仍要在 Xcode 工程和 Apple 发布体系中完成配置。
4.6 macOS 更新
macOS 更新可以由分发渠道负责,也可以由应用集成第三方更新框架。Flutter SDK 本身不提供一个跨 Windows、macOS、Linux 的官方统一自动更新 API。
不同方案的责任不同:
- Mac App Store:由商店管理版本发现、下载和安装;
- 企业 MDM:由组织设备管理系统管理;
- 网站分发:应用或独立更新器需要自行完成发现、验证、替换和回滚;
- 第三方更新框架:通常需要原生插件、签名配置和单独的更新服务器。
macOS 应用不能可靠地在运行期间直接覆盖自己的 Bundle。更新器通常需要:
- 下载新的
.app; - 验证签名、公证状态或更新清单签名;
- 退出旧应用;
- 将新应用原子地移动到目标位置;
- 启动新应用;
- 新应用启动失败时保留旧版本或恢复备份。
五、Linux:应用目录、系统依赖和发行格式
5.1 构建 Linux Release
在安装好 GTK、CMake、Ninja 和编译器后执行:
flutter clean
flutter pub get
flutter test
flutter build linux --release
典型产物:
build/linux/x64/release/bundle/
通常包含:
bundle/
├── desk_notes
├── lib/
│ └── libflutter_linux_gtk.so
├── data/
│ ├── flutter_assets/
│ └── icudtl.dat
└── plugins/
直接运行:
./build/linux/x64/release/bundle/desk_notes
如果目标系统缺少 GTK 或其他动态库,可能看到:
error while loading shared libraries: libgtk-3.so.0
这类错误发生在 ELF 动态链接阶段,Flutter 的 Dart 异常处理器无法捕获。诊断可以使用:
ldd ./build/linux/x64/release/bundle/desk_notes
检查哪些依赖显示为 not found。
Linux 不存在一个由所有发行版共同强制采用的桌面安装和签名格式。常见选择如下:
| 格式 | 运行模型 | 典型责任 |
|---|---|---|
| AppImage | 携带较多用户态依赖,下载即运行 | 发布者处理兼容性、签名和更新 |
| Flatpak | 沙盒和运行时模型 | 由 Flathub 或私有仓库分发 |
.deb |
Debian/Ubuntu 系 | 由 APT 仓库管理依赖和更新 |
.rpm |
Fedora/RHEL/openSUSE 等 | 由 RPM 仓库管理依赖和更新 |
| tar.gz | 手动解压 | 发布者自行处理安装、卸载和升级 |
5.2 Linux 的兼容性边界
Linux 发布最容易被误解为“编译一次,到处运行”。实际上,ELF 程序会受到以下因素影响:
- CPU 架构,例如 x86_64、arm64;
- glibc 版本;
- GTK 版本;
- OpenGL、音频、输入法和桌面环境;
- 原生插件的动态库依赖;
- 发行版的打包策略。
如果在较新的发行版上编译,生成的二进制可能依赖较新的 glibc,导致无法在旧发行版运行。解决方法不是把 Dart 代码重新编译成“纯 Linux”,而是:
- 在目标兼容范围内的较旧基线环境构建;
- 使用 Flatpak Runtime;
- 为不同发行版分别构建和测试;
- 明确最低支持发行版与架构。
ldd 能诊断当前机器的动态依赖,但不能证明所有目标发行版都兼容。生产发布必须在干净的目标环境或容器/虚拟机中验证。
5.3 Linux 签名不是一个统一机制
Linux 的“签名”要先明确签什么:
- AppImage 可以提供外部签名或校验文件,但桌面环境通常不会像 Gatekeeper 一样统一强制验证;
- Debian 和 RPM 包可以签名,仓库元数据也可以签名;
- Flatpak 仓库具有自己的提交和签名模型;
- HTTPS、SHA-256 校验和 GPG 签名解决的是不同层次的问题。
仅发布:
DeskNotes.AppImage
DeskNotes.AppImage.sha256
并不能让用户确认 SHA-256 文件本身没有被替换。更强的发布模型是让签名公钥通过独立可信渠道分发,再验证签名清单或仓库元数据。
示意性校验:
sha256sum DeskNotes.AppImage
输出的摘要必须与发布方通过受信任渠道公布的摘要一致。若需要抗“文件和摘要同时被替换”,还需要 GPG、Sigstore 或发行仓库自身的信任链,而不是只使用裸哈希。
5.4 Linux 更新
Linux 上最可靠的更新机制通常来自分发格式:
.deb:发布到 APT 仓库,由 APT 处理版本、依赖和更新;.rpm:发布到 RPM 仓库,由对应包管理器处理;- Flatpak:通过 Flatpak Remote 更新;
- AppImage:需要应用内更新器、外部工具或用户重新下载;
- tar.gz:通常是手动替换或由企业脚本升级。
自建 AppImage 更新器仍需处理:
- 获取更新清单;
- 验证清单和文件来源;
- 下载到临时文件;
- 校验摘要和签名;
- 保留旧文件;
- 原子替换或切换符号链接;
- 启动新版本;
- 新版本无法启动时回滚。
不要直接在原文件上写入下载内容。更安全的过程是:
DeskNotes.AppImage
DeskNotes.AppImage.new
│
├─ 下载完成
├─ 校验大小、摘要、签名
├─ fsync/关闭文件
├─ 原子 rename
└─ 启动新版本
不过 rename 的原子性只表示同一文件系统内的目录项切换,不代表电源中断、文件系统损坏或程序启动失败时自动恢复。因此仍要保留旧版本或使用版本目录加符号链接切换。
六、签名到底证明什么
签名通常提供两个性质。
设发布者持有私钥 sk,公开公钥为 pk,文件内容为 M:
S = Sign(sk, Hash(M))
Verify(pk, M, S) = true
验证成功说明:
M在签名后没有被修改;- 签名与对应公钥匹配。
如果公钥与某个受信任身份绑定,用户还可以推断文件来自该身份。这里有一个重要边界:
签名证明来源和完整性,不证明程序没有漏洞,也不证明程序没有恶意行为。
为什么签名必须靠近最终产物
假设发布流程为:
构建 A → 签名 A → 压缩 → 修改文件 → 发布
最后发布的内容已经不是签名时的内容。正确关系应是:
构建最终内容
→ 生成安装包/归档
→ 对最终需要验证的对象签名
→ 公证或生成仓库元数据
→ 发布
对于包含嵌套代码的格式,还需要先签内层,再签外层。对一个已签名 .app 添加资源,会破坏其代码签名;对已签名安装器重新封装,也可能改变签名对象。
七、更新系统的完整状态机
自动更新不是“检查版本号后下载文件”。一个可恢复的更新流程至少包含这些状态:
stateDiagram-v2
[*] --> Running
Running --> Checking: 定时/手动检查
Checking --> Running: 无更新或检查失败
Checking --> Available: 发现更高版本
Available --> Downloading: 用户确认或自动策略允许
Downloading --> Verifying: 下载完成
Downloading --> Running: 网络失败/取消
Verifying --> Staged: 签名、摘要、版本验证通过
Verifying --> Failed: 验证失败
Staged --> Installing: 退出旧应用或交给安装器
Installing --> Relaunching: 安装成功
Installing --> Rollback: 安装失败
Relaunching --> Healthy: 新版本启动并完成自检
Relaunching --> Rollback: 启动失败
Healthy --> Running
Rollback --> Running: 恢复旧版本
Failed --> Running
每个状态都有不同的故障路径:
检查阶段
检查接口返回的版本必须解析为结构化版本,而不是简单字符串。清单至少应包含:
- 渠道,例如 stable、beta;
- 版本和构建号;
- 平台和架构;
- 下载地址;
- 文件摘要;
- 签名或签名清单;
- 最低系统版本;
- 是否强制更新。
下载阶段
下载应写入临时文件,并检查:
- HTTP 状态码;
- Content-Length 与实际大小;
- 网络中断;
- 磁盘空间;
- 用户取消;
- 代理和证书错误。
下载到一半不能覆盖当前版本,否则失败后可能留下不可启动文件。
验证阶段
推荐顺序是:
解析清单
→ 检查平台/架构/版本
→ 下载文件
→ 校验摘要
→ 验证签名或平台签名
→ 检查包格式
→ 进入待安装状态
摘要验证用于检测文件是否与清单一致;签名验证用于判断清单或文件是否由受信任发布者产生。两者不是互相替代的关系。
安装和回滚阶段
更新器需要区分:
- 旧程序是否已经退出;
- 文件是否仍被占用;
- 新版本是否已完整落盘;
- 新版本是否能启动;
- 数据迁移是否成功。
可以采用版本目录:
app/
├── versions/
│ ├── 1.4.1/
│ └── 1.4.2/
└── current -> versions/1.4.2
切换 current 的思路比直接删除旧版本更容易回滚,但不同操作系统对符号链接、快捷方式、权限和安装位置的处理不同。Windows MSIX、macOS App Store 和 Linux 包管理器通常应优先使用平台自己的更新机制,而不是重新实现一套覆盖逻辑。
八、数据迁移决定“更新是否成功”
应用文件更新成功,不代表应用更新成功。版本迁移通常包括:
启动新版本
→ 读取当前数据版本
→ 判断是否需要迁移
→ 在事务或备份保护下迁移
→ 写入新数据版本
→ 完成自检
例如旧数据库版本为 3,新版本需要 4:
version = 3
→ 创建备份
→ 执行 3 → 4 的 schema migration
→ 校验关键表和索引
→ 写入 version = 4
不能直接把所有旧数据按“当前格式”读取并覆盖。迁移过程中断电、磁盘空间不足或字段转换失败时,必须有备份或事务策略,否则安装器回滚了程序,数据却已经被不可逆地修改。
更新器和应用还应避免版本降级覆盖。例如,用户从 1.4.2 安装 1.3.9,可能导致数据库迁移方向不存在。发布服务器、安装器和应用启动检查都应对降级做出明确处理。
九、CI/CD 中应保存的构建证据
一次可审计的发布不应只保存最终安装包,还应记录:
- Flutter SDK 和 Dart SDK 版本;
- 操作系统与 CPU 架构;
- 依赖锁定文件;
- 构建命令;
- 包版本、构建号和渠道;
- 代码签名证书标识;
- 公证或商店提交 ID;
- 文件 SHA-256;
- 测试结果;
- 发布清单。
一个跨平台流水线可以组织为:
代码提交
→ 依赖解析与测试
→ Windows 构建/打包/签名
→ macOS 构建/签名/公证
→ Linux 构建/生成发行包
→ 干净环境安装测试
→ 生成签名更新清单
→ 发布到对应渠道
Linux 构建可以在容器中提高可重复性,但容器内的 glibc 和 GTK 基线会影响兼容范围。macOS 的签名、公证和 Apple 资源访问则通常需要 macOS Runner。Windows 的 Authenticode 需要可访问 Windows SDK 和签名证书。
十、移动端、Web 与桌面发布的差异
Flutter 的 UI 和大部分 Dart 业务代码可以跨平台复用,但发布模型不能简单复用。
Android
Android 使用 APK 或 AAB,签名由 Android keystore 管理,Google Play 还涉及 Play App Signing、版本号和商店审核。桌面上的 Authenticode、Developer ID 和 Linux 仓库签名不适用于 Android。
iOS
iOS 使用 App Bundle、Provisioning Profile、签名和 App Store Connect 分发。应用通常不能像桌面程序一样从任意网站下载新二进制并自行替换,更新主要由 App Store 或企业分发体系负责。
Web
Web 构建产物是 JavaScript、资源和 HTML:
flutter build web --release
Web 没有本地 .exe、.app 或 .deb 安装过程。更新通常由静态资源部署、缓存失效和 Service Worker 策略决定。即使 Flutter 应用代码相同,桌面端的代码签名、公证和原生安装器问题也不会自动转化为 Web 问题。
桌面
桌面应用需要面对:
- 本地可执行文件和动态库;
- 系统安装器;
- 操作系统信任策略;
- 原生插件;
- 架构和系统版本兼容;
- 自定义更新或平台商店更新。
因此,“Flutter 支持跨平台”主要表示框架和代码模型可以跨平台,并不表示一次构建即可获得所有平台的安装、签名和更新产物。
十一、发布前的可验证流程
Windows
flutter build windows --release
# 使用目标打包工具生成 MSIX/MSI/EXE
signtool verify /pa /all /v .\DeskNotesSetup.exe
应在没有 Flutter SDK 的干净 Windows 虚拟机中验证:
- 安装;
- 启动;
- 卸载;
- 快捷方式;
- 升级旧版本;
- 从升级失败中恢复;
- x64 或 ARM64 架构行为。
macOS
flutter build macos --release
codesign --verify --deep --strict --verbose=2 DeskNotes.app
xcrun notarytool submit DeskNotes.zip \
--keychain-profile "notary-profile" \
--wait
xcrun stapler validate DeskNotes.app
spctl --assess --type execute --verbose=4 DeskNotes.app
应在未安装开发证书、未使用 Xcode 调试环境的干净 Mac 上测试首次打开、权限弹窗、拖拽安装和升级。
Linux
flutter build linux --release
ldd build/linux/x64/release/bundle/desk_notes
./build/linux/x64/release/bundle/desk_notes
应在声明支持的每个发行版和架构上测试:
- 动态库解析;
- 字体和输入法;
- 文件选择和桌面集成;
- 权限不足时的行为;
- 包管理器安装、升级和卸载;
- 用户数据是否在卸载后按预期保留。
十二、常见错误及其根因
“构建成功,但用户电脑打不开”
可能原因:
- 只复制了主可执行文件;
- 缺少 Flutter 资源目录;
- 原生插件 DLL 或动态库未打包;
- 目标系统缺少 GTK 或其他运行库;
- 架构不匹配;
- macOS 签名或公证失败。
诊断应从系统层开始,而不是先修改 Dart 代码:
检查产物完整性
→ 检查动态依赖
→ 检查架构
→ 检查平台签名
→ 在干净机器复现
“哈希一致,但仍然不安全”
如果哈希文件和应用文件都从同一个未认证 HTTP 位置下载,攻击者可以同时替换两者。哈希只解决完整性校验,不解决发布者身份认证。需要 HTTPS、签名清单、GPG/Sigstore 或平台仓库信任链。
“签名成功,但系统仍然警告”
Windows 可能是 SmartScreen 信誉问题,macOS 可能是公证、隔离属性、签名嵌套组件或 Gatekeeper 策略问题。签名验证、公证验证和用户界面中的安全提示是不同层次的结果。
“更新后程序能启动,但数据丢了”
这通常不是安装器覆盖失败,而是数据迁移没有版本化、没有事务或没有备份。更新设计必须同时测试:
程序升级成功 + 数据迁移成功
程序升级成功 + 数据迁移失败
程序升级失败 + 旧程序仍可启动
“Linux 在开发机能运行,客户机器不能运行”
开发机通常安装了完整 SDK 和开发依赖,客户机器没有。应使用发布目录或正式发行包在最小环境中测试,并用 ldd、系统日志和目标发行版的包工具定位依赖问题。
十三、适合生产发布的最小原则
一个可交付的 Flutter 桌面版本至少应满足:
- 已锁定 Flutter、Dart、依赖和目标架构;
- 已在目标平台生成 Release 产物;
- 已使用适合平台的安装或分发格式;
- 已签署最终需要验证的文件;
- macOS 外部分发已完成公证;
- Windows 安装器或 MSIX 的发布者证书受目标用户信任;
- Linux 明确了发行版、架构和系统依赖范围;
- 更新清单能验证版本、平台、摘要和签名;
- 更新失败不会直接破坏当前版本;
- 数据迁移可检测、可恢复并经过旧版本升级测试。
Flutter 解决了桌面应用的编译和界面运行时问题,但操作系统的安装信任、证书体系、商店渠道和更新策略仍由各平台负责。正确的发布流程不是把三个 flutter build 命令放进脚本,而是让“最终产物、身份签名、分发渠道和可恢复更新”形成一条可以验证的闭环。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 上一篇:Flutter iOS 发布:证书、Provisioning、Capability、归档和审核
- 下一篇:Flutter Web 发布:Renderer、缓存、路由、CDN、PWA 和回滚
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论