macos-auto-update
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSparkle Auto-Update for macOS Apps
为macOS应用添加Sparkle自动更新
This skill adds Sparkle auto-update support to a native macOS app. Sparkle is the standard open-source framework for macOS app updates outside the Mac App Store.
本技能可为原生macOS应用添加Sparkle自动更新支持。Sparkle是Mac App Store之外,macOS应用更新的标准开源框架。
Overview
概述
The implementation has 4 parts:
- SPM dependency -- add the Sparkle package to the Xcode project
- UpdaterManager.swift -- a singleton that wraps
SPUStandardUpdaterController - Info.plist keys -- and
SUFeedURLSUPublicEDKey - UI integration -- "Check for Updates" button in settings/menu bar
该实现包含4个部分:
- SPM依赖——将Sparkle包添加到Xcode项目中
- UpdaterManager.swift——封装的单例类
SPUStandardUpdaterController - Info.plist键——和
SUFeedURLSUPublicEDKey - UI集成——设置界面/菜单栏中的「检查更新」按钮
Step 1: Add Sparkle via SPM
步骤1:通过SPM添加Sparkle
In Xcode: File > Add Package Dependencies > enter:
https://github.com/sparkle-project/SparkleUse the "Up to Next Major Version" rule with . Add the framework to your app target.
2.0.0SparkleOr add it to if your project uses one:
Package.swiftswift
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")在Xcode中:文件 > 添加包依赖 > 输入:
https://github.com/sparkle-project/Sparkle使用「兼容至下一个大版本」规则,版本选择。将框架添加到你的应用目标中。
2.0.0Sparkle如果你的项目使用,也可以这样添加:
Package.swiftswift
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")Step 2: Create UpdaterManager.swift
步骤2:创建UpdaterManager.swift
Copy into your project. This is a singleton that:
references/UpdaterManager.swift- Creates early (before
SPUStandardUpdaterControllerreturns)applicationDidFinishLaunching - Publishes for UI binding
canCheckForUpdates - Exposes toggle
automaticallyChecksForUpdates - Skips all update logic in DEBUG builds (so you don't get update prompts during development)
- For menu-bar-only apps: temporarily switches to activation policy before showing the update window
.regular
The key design decisions in this file:
- in the initializer, then calling
startingUpdater: falseexplicitly instart(). This gives you control over timing.applicationDidFinishLaunching - DEBUG guards on and
start(). Sparkle should never run in debug builds -- it would try to update your debug app with a release build.checkForUpdates() - with
ObservableObject(not@Published) because we need the Combine@Observablebridge from Sparkle's KVO.publisher(for:)
将复制到你的项目中。这个单例类具备以下功能:
references/UpdaterManager.swift- 在返回前提前创建
applicationDidFinishLaunchingSPUStandardUpdaterController - 发布用于UI绑定
canCheckForUpdates - 暴露开关
automaticallyChecksForUpdates - 在DEBUG构建中跳过所有更新逻辑(避免开发期间收到更新提示)
- 对于仅菜单栏应用:显示更新窗口前临时切换至激活策略
.regular
该文件中的关键设计决策:
- 初始化器中设置****,然后在
startingUpdater: false中显式调用applicationDidFinishLaunching。这让你可以控制启动时机。start() - 在和
start()上添加DEBUG保护。Sparkle绝对不应在调试构建中运行——否则它会尝试用发布版本更新你的调试应用。checkForUpdates() - 使用带的
@Published(而非ObservableObject),因为我们需要从Sparkle的KVO桥接到Combine的@Observable。publisher(for:)
Step 3: Configure Info.plist
步骤3:配置Info.plist
Add these keys to your app's :
Info.plistxml
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/OWNER/REPO/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_PUBLIC_EDDSA_KEY_HERE</string>
<key>SUEnableInstallerLauncherService</key>
<true/>在应用的中添加以下键:
Info.plistxml
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/OWNER/REPO/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_PUBLIC_EDDSA_KEY_HERE</string>
<key>SUEnableInstallerLauncherService</key>
<true/>Generating EdDSA Keys
生成EdDSA密钥
Sparkle uses EdDSA (Ed25519) signing. Generate a keypair:
bash
undefinedSparkle使用EdDSA(Ed25519)签名。生成密钥对:
bash
undefinedFind generate_keys in your DerivedData after building the project with Sparkle
在构建包含Sparkle的项目后,在DerivedData中找到generate_keys
find ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1
Run it:
```bash
/path/to/generate_keysThis prints the public key and stores the private key in your Keychain. Put the public key in in Info.plist. The private key stays in Keychain and is used by during release.
SUPublicEDKeysign_updatefind ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1
运行该工具:
```bash
/path/to/generate_keys这会打印公钥,并将私钥存储在你的钥匙串中。将公钥放入Info.plist的中。私钥保留在钥匙串中,发布时由工具使用。
SUPublicEDKeysign_updateStep 4: Wire Into App
步骤4:接入应用
App Delegate
应用委托
swift
final class AppDelegate: NSObject, NSApplicationDelegate {
private let updaterManager = UpdaterManager.shared
func applicationDidFinishLaunching(_ notification: Notification) {
updaterManager.start()
}
}The property must be accessed early so the is created before the app finishes launching. Referencing it in the property ensures this.
UpdaterManager.sharedSPUStandardUpdaterControllerAppDelegateswift
final class AppDelegate: NSObject, NSApplicationDelegate {
private let updaterManager = UpdaterManager.shared
func applicationDidFinishLaunching(_ notification: Notification) {
updaterManager.start()
}
}必须尽早访问属性,确保在应用完成启动前创建。在属性中引用它可保证这一点。
UpdaterManager.sharedSPUStandardUpdaterControllerAppDelegateSettings UI (About Pane)
设置UI(关于面板)
swift
struct AboutSettingsPane: View {
@ObservedObject private var updaterManager = UpdaterManager.shared
var body: some View {
Form {
Section("Updates") {
Toggle(isOn: Binding(
get: { updaterManager.automaticallyChecksForUpdates },
set: { updaterManager.automaticallyChecksForUpdates = $0 }
)) {
Text("Automatically check for updates")
}
Button("Check for Updates...") {
updaterManager.checkForUpdates()
}
.disabled(!updaterManager.canCheckForUpdates)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
}
}swift
struct AboutSettingsPane: View {
@ObservedObject private var updaterManager = UpdaterManager.shared
var body: some View {
Form {
Section("更新") {
Toggle(isOn: Binding(
get: { updaterManager.automaticallyChecksForUpdates },
set: { updaterManager.automaticallyChecksForUpdates = $0 }
)) {
Text("自动检查更新")
}
Button("检查更新...") {
updaterManager.checkForUpdates()
}
.disabled(!updaterManager.canCheckForUpdates)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
}
}Menu Bar (optional)
菜单栏(可选)
swift
Button {
updaterManager.checkForUpdates()
} label: {
Label("Check for Updates...", systemImage: "arrow.down.circle")
}
.disabled(!updaterManager.canCheckForUpdates)swift
Button {
updaterManager.checkForUpdates()
} label: {
Label("检查更新...", systemImage: "arrow.down.circle")
}
.disabled(!updaterManager.canCheckForUpdates)Step 5: Create Initial Appcast
步骤5:创建初始Appcast
Create an at the root of your repo. It starts empty and gets populated by your release process:
appcast.xmlxml
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>YourApp</title>
<description>Most recent changes for YourApp.</description>
<language>en</language>
</channel>
</rss>Host this file on GitHub (raw URL) or any static file host. The URL must match in Info.plist.
SUFeedURL在仓库根目录创建。初始文件为空,后续会由发布流程填充内容:
appcast.xmlxml
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>YourApp</title>
<description>Most recent changes for YourApp.</description>
<language>en</language>
</channel>
</rss>将该文件托管在GitHub(原始URL)或任何静态文件托管服务上。URL必须与Info.plist中的匹配。
SUFeedURLMenu-Bar-Only Apps
仅菜单栏应用
If your app runs as (no Dock icon), Sparkle's update window won't appear unless you temporarily switch to . The reference handles this in :
.accessory.regularUpdaterManagercheckForUpdates()swift
func checkForUpdates() {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
controller.checkForUpdates(nil)
}The app reverts to when the update window closes (handled by your existing activation policy manager).
.accessory如果你的应用以模式运行(无Dock图标),除非临时切换至模式,否则Sparkle的更新窗口不会显示。参考实现的在中处理了这一点:
.accessory.regularUpdaterManagercheckForUpdates()swift
func checkForUpdates() {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
controller.checkForUpdates(nil)
}当更新窗口关闭时,应用会恢复为模式(由你现有的激活策略管理器处理)。
.accessoryHardened Runtime Entitlements
强化运行时权限
If your app uses Hardened Runtime (required for notarization), no special Sparkle entitlements are needed. Sparkle 2.x works with the standard hardened runtime configuration.
如果你的应用使用强化运行时(公证所需),无需特殊的Sparkle权限。Sparkle 2.x可与标准强化运行时配置兼容。
Appcast Item Format
Appcast条目格式
Each release in the appcast looks like this (for reference when building release tooling):
xml
<item>
<title>Version 1.2 (Build 5)</title>
<pubDate>Mon, 26 May 2026 12:00:00 +0000</pubDate>
<sparkle:version>5</sparkle:version>
<sparkle:shortVersionString>1.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<description><![CDATA[<ul><li>New feature</li><li>Bug fix</li></ul>]]></description>
<enclosure url="https://github.com/OWNER/REPO/releases/download/v1.2/YourApp.dmg"
type="application/octet-stream"
sparkle:edSignature="BASE64_EDDSA_SIGNATURE"
length="FILE_SIZE_BYTES" />
</item>- =
sparkle:version(build number)CFBundleVersion - =
sparkle:shortVersionString(marketing version)CFBundleShortVersionString - = output of
sparkle:edSignaturesign_update YourApp.dmg - = file size in bytes
length
Appcast中的每个发布条目格式如下(构建发布工具时参考):
xml
<item>
<title>Version 1.2 (Build 5)</title>
<pubDate>Mon, 26 May 2026 12:00:00 +0000</pubDate>
<sparkle:version>5</sparkle:version>
<sparkle:shortVersionString>1.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<description><![CDATA[<ul><li>New feature</li><li>Bug fix</li></ul>]]></description>
<enclosure url="https://github.com/OWNER/REPO/releases/download/v1.2/YourApp.dmg"
type="application/octet-stream"
sparkle:edSignature="BASE64_EDDSA_SIGNATURE"
length="FILE_SIZE_BYTES" />
</item>- =
sparkle:version(构建编号)CFBundleVersion - =
sparkle:shortVersionString(市场版本号)CFBundleShortVersionString - =
sparkle:edSignature的输出sign_update YourApp.dmg - = 文件大小(字节)
length