软件 · 文章
使用Sparkle快速完成Mac软件更新分发功能
VibeCoding时代来临,越来越多人也开始做独立开发了,一款优秀完善的产品,软件的更新分发是必不可少的。
这篇文章主要就是基于这个功能,对Mac app的更新分发的实现做一期简单的讲解。
当开发的Mac软件并不希望直接是啊苹果的应用商店的情况下,大部分人都会采用官网直的形式,也就是让用户从官网下载安装包。
在Mac OS直售分发的场景中,一个比较成熟的方案是,我们可以选择使用Sparkle 。它是一个专门为Mac OS应用设计的自动更新框架,负责:
检查新版本
展示更新提示
下载更新包
签名校验并替换应用
开源地址: https://github.com/sparkle-project/Sparkle

Sparkle自动更新架构
Sparkle自动更新系统通常由三部分组成。
集成 Sparkle framework,在启动时初始化 updater,并提供“检查更新”入口。
一个 XML 文件,通常叫
appcast.xml,里面描述最新版本、构建号、下载地址、文件大小、签名和发布说明。一般是 zip 包,里面包含新的
.app。Sparkle 下载 zip 后,会校验签名,然后完成替换安装。

2.1 . 引入Sparkle
如果你的项目用的是Swift Package Manager,那么你可以在Package.swift 中加入Sparkle :
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
],
targets: [
.executableTarget(
name: "YourApp",
dependencies: [
.product(name: "Sparkle", package: "Sparkle"),
]
)
]这样应用代码里就可以使用Sparkle提供的SPUStandardUpdaterController。这是使用Sparkle第一步。
2.2 封装更新器
操作Sparkle的方式很多,但是比较建议的是不要在各个View中直接操作Sparkle,而是封装一个独立的Updater服务。
import AppKit
import Sparkle
/// AppUpdater 是应用自动更新管理器(基于 Sparkle 框架)
/// 使用单例模式,确保整个应用中只有一个更新控制器实例
@MainActor
final class AppUpdater {
/// 单例实例
static let shared = AppUpdater()
/// Sparkle 的标准更新控制器
private var updaterController: SPUStandardUpdaterController?
/// 私有初始化,防止外部直接创建实例
private init() {}
/// 判断更新功能是否已正确配置
/// 需要在 Info.plist 中同时设置 SUFeedURL 和 SUPublicEDKey
var isConfigured: Bool {
feedURL != nil && publicKey?.isEmpty == false
}
/// 如果已配置,则启动更新控制器
/// 此方法会在应用启动时或需要更新时被调用
func startIfConfigured() {
guard isConfigured else {
NSLog("Updater is not configured.")
return
}
// 避免重复创建控制器
if updaterController == nil {
updaterController = SPUStandardUpdaterController(
startingUpdater: true, // 立即启动检查器
updaterDelegate: nil, // 使用默认代理
userDriverDelegate: nil // 使用默认用户界面驱动
)
}
}
/// 手动检查更新(通常由用户在菜单中触发)
func checkForUpdates() {
guard isConfigured else {
presentConfigurationMissingAlert()
return
}
startIfConfigured()
updaterController?.checkForUpdates(nil)
}
/// 从 Info.plist 中读取更新 feed URL
private var feedURL: URL? {
guard let value = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String,
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else {
return nil
}
return URL(string: value)
}
/// 从 Info.plist 中读取 Sparkle 公钥(用于验证更新包签名)
private var publicKey: String? {
Bundle.main.object(forInfoDictionaryKey: "SUPublicEDKey") as? String
}
/// 显示配置缺失提示弹窗
/// 通常在 Debug 构建或未正确配置 Info.plist 时出现
private func presentConfigurationMissingAlert() {
let alert = NSAlert()
alert.messageText = "Updates Not Configured"
alert.informativeText = "Set SUFeedURL and SUPublicEDKey in Info.plist for release builds."
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
}这里需要注意两个关键点:
startingUpdater:true表示启动Sparkle更新检查机制。checkForUpdates(nil)用于衣服主动点击 检查更新 UI 。
2.3 启动时初始化
在AppDelegate 或者应用启动入口调用:
AppUpdater.shared.startIfConfigured()这样正式发布包启动之后,Sparkle就会根据自己的策略自动检查更新。同时可以在设置页、菜单栏或者About等页面提供手动检查更新的入口。
Button("Check for Updates...") {
AppUpdater.shared.checkForUpdates()
}2.4 配置Info.plist
Sparkle依赖两个重要的配置,SUFeedURL和SUPublicEDKey 。
SUFeedURL 是 appcast 地址。
SUPublicEDKey 是 Sparkle 的 EdDSA 公钥,用来验证更新包签名。对应的私钥只应该保存在发布机器或 CI 的安全环境里,如果你的项目使用远程仓库托管,绝不能提交到仓库。
实际项目里,建议不要把这两个值硬编码在开发用
Info.plist中,而是在 release 打包脚本里动态写入。这样 Debug 构建可以不启用自动更新,Release 构建才启用。
2.5 生成Sparkle密钥
Sparkle 提供 generate_keys 工具生成公私钥。
大致流程是:
generate_key生成后会得到:
public key:写进应用的
SUPublicEDKeyprivate key:发布时用于签名 appcast/zip
私钥要妥善保存,例如放在 Keychain、安全目录或 CI secret 中。
2.6 打包发布zip
ditto -c -k --norsrc --keepParent YourApp.app YourApp-1.0.0.zip正式发布时,还要确保 app 已完成:
Developer ID 签名
hardened runtime
Apple notarization
staple 公证票据
否则用户下载后可能遇到 Gatekeeper 拦截。
2.7 生成 appcast.xml
appcast 是 Sparkle 的更新索引。一个简化版本如下:
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>YourApp</title>
<item>
<title>1.0.1</title>
<sparkle:version>2</sparkle:version>
<sparkle:shortVersionString>1.0.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<enclosure
url="https://releases.example.com/YourApp-1.0.1.zip"
length="12345678"
type="application/octet-stream"
sparkle:edSignature="..." />
</item>
</channel>
</rss>sparkle:shortVersionString:用户看到的版本号,例如1.0.1sparkle:version:构建号,对应CFBundleVersion,必须递增enclosure url:更新 zip 下载地址sparkle:edSignature:zip 的签名sparkle:minimumSystemVersion:最低系统版本
Sparkle 提供 generate_appcast 工具自动生成 appcast,并可以自动写入签名:
generate_appcast \
--download-url-prefix https://releases.example.com/ \
--ed-key-file /secure/path/private_key \
dist/appcast
dist/appcast目录里放好 zip 后,工具会生成或更新appcast.xml。
2.8 上传发布文件
最终需要上传这些文件到你的发布服务器或者对象存储中,个人用的是Cloudflare。
https://releases.example.com/appcast.xml
https://releases.example.com/YourApp-1.0.1.zip
https://releases.example.com/release-notes/1.0.1/en.html推荐缓存策略:
appcast.xml:no-cachezip/delta 包:长期缓存,文件名带版本号和构建号
release notes:可以
no-cache
原因是 appcast.xml 是更新入口,用户每次检查更新都依赖它,不能被 CDN 长时间缓存旧版本。
2.9 注意安全
Sparkle 的安全关键在于签名校验。
发布端使用私钥给更新包签名:
private key -> sign zip -> appcast.xml 写入 edSignature
客户端应用内置公钥:
SUPublicEDKey -> verify downloaded zip
如果攻击者篡改 zip,签名校验会失败,Sparkle 不会安装。
所以:
私钥不能进仓库
私钥不能出现在日志里
更换公钥会影响老版本升级能力
appcast 和 zip 最好使用 HTTPS 托管
版权声明
本文内容版权归作者或相关权利人所有。转载、引用或其他使用请遵循相应授权条款,并保留本文链接。