macos-auto-update

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sparkle 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:
  1. SPM dependency -- add the Sparkle package to the Xcode project
  2. UpdaterManager.swift -- a singleton that wraps
    SPUStandardUpdaterController
  3. Info.plist keys --
    SUFeedURL
    and
    SUPublicEDKey
  4. UI integration -- "Check for Updates" button in settings/menu bar
该实现包含4个部分:
  1. SPM依赖——将Sparkle包添加到Xcode项目中
  2. UpdaterManager.swift——封装
    SPUStandardUpdaterController
    的单例类
  3. Info.plist键——
    SUFeedURL
    SUPublicEDKey
  4. UI集成——设置界面/菜单栏中的「检查更新」按钮

Step 1: Add Sparkle via SPM

步骤1:通过SPM添加Sparkle

In Xcode: File > Add Package Dependencies > enter:
https://github.com/sparkle-project/Sparkle
Use the "Up to Next Major Version" rule with
2.0.0
. Add the
Sparkle
framework to your app target.
Or add it to
Package.swift
if your project uses one:
swift
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")
在Xcode中:文件 > 添加包依赖 > 输入:
https://github.com/sparkle-project/Sparkle
使用「兼容至下一个大版本」规则,版本选择
2.0.0
。将
Sparkle
框架添加到你的应用目标中。
如果你的项目使用
Package.swift
,也可以这样添加:
swift
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")

Step 2: Create UpdaterManager.swift

步骤2:创建UpdaterManager.swift

Copy
references/UpdaterManager.swift
into your project. This is a singleton that:
  • Creates
    SPUStandardUpdaterController
    early (before
    applicationDidFinishLaunching
    returns)
  • Publishes
    canCheckForUpdates
    for UI binding
  • Exposes
    automaticallyChecksForUpdates
    toggle
  • Skips all update logic in DEBUG builds (so you don't get update prompts during development)
  • For menu-bar-only apps: temporarily switches to
    .regular
    activation policy before showing the update window
The key design decisions in this file:
  • startingUpdater: false
    in the initializer, then calling
    start()
    explicitly in
    applicationDidFinishLaunching
    . This gives you control over timing.
  • DEBUG guards on
    start()
    and
    checkForUpdates()
    . Sparkle should never run in debug builds -- it would try to update your debug app with a release build.
  • ObservableObject
    with
    @Published
    (not
    @Observable
    ) because we need the Combine
    publisher(for:)
    bridge from Sparkle's KVO.
references/UpdaterManager.swift
复制到你的项目中。这个单例类具备以下功能:
  • applicationDidFinishLaunching
    返回前提前创建
    SPUStandardUpdaterController
  • 发布
    canCheckForUpdates
    用于UI绑定
  • 暴露
    automaticallyChecksForUpdates
    开关
  • 在DEBUG构建中跳过所有更新逻辑(避免开发期间收到更新提示)
  • 对于仅菜单栏应用:显示更新窗口前临时切换至
    .regular
    激活策略
该文件中的关键设计决策:
  • 初始化器中设置**
    startingUpdater: false
    **,然后在
    applicationDidFinishLaunching
    中显式调用
    start()
    。这让你可以控制启动时机。
  • start()
    checkForUpdates()
    上添加DEBUG保护。Sparkle绝对不应在调试构建中运行——否则它会尝试用发布版本更新你的调试应用。
  • 使用
    @Published
    ObservableObject
    (而非
    @Observable
    ),因为我们需要从Sparkle的KVO桥接到Combine的
    publisher(for:)

Step 3: Configure Info.plist

步骤3:配置Info.plist

Add these keys to your app's
Info.plist
:
xml
<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.plist
中添加以下键:
xml
<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
undefined
Sparkle使用EdDSA(Ed25519)签名。生成密钥对:
bash
undefined

Find 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_keys
This prints the public key and stores the private key in your Keychain. Put the public key in
SUPublicEDKey
in Info.plist. The private key stays in Keychain and is used by
sign_update
during release.
find ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1

运行该工具:

```bash
/path/to/generate_keys
这会打印公钥,并将私钥存储在你的钥匙串中。将公钥放入Info.plist的
SUPublicEDKey
中。私钥保留在钥匙串中,发布时由
sign_update
工具使用。

Step 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
UpdaterManager.shared
property must be accessed early so the
SPUStandardUpdaterController
is created before the app finishes launching. Referencing it in the
AppDelegate
property ensures this.
swift
final class AppDelegate: NSObject, NSApplicationDelegate {
    private let updaterManager = UpdaterManager.shared

    func applicationDidFinishLaunching(_ notification: Notification) {
        updaterManager.start()
    }
}
必须尽早访问
UpdaterManager.shared
属性,确保
SPUStandardUpdaterController
在应用完成启动前创建。在
AppDelegate
属性中引用它可保证这一点。

Settings 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
appcast.xml
at the root of your repo. It starts empty and gets populated by your release process:
xml
<?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
SUFeedURL
in Info.plist.
在仓库根目录创建
appcast.xml
。初始文件为空,后续会由发布流程填充内容:
xml
<?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中的
SUFeedURL
匹配。

Menu-Bar-Only Apps

仅菜单栏应用

If your app runs as
.accessory
(no Dock icon), Sparkle's update window won't appear unless you temporarily switch to
.regular
. The reference
UpdaterManager
handles this in
checkForUpdates()
:
swift
func checkForUpdates() {
    NSApp.setActivationPolicy(.regular)
    NSApp.activate(ignoringOtherApps: true)
    controller.checkForUpdates(nil)
}
The app reverts to
.accessory
when the update window closes (handled by your existing activation policy manager).
如果你的应用以
.accessory
模式运行(无Dock图标),除非临时切换至
.regular
模式,否则Sparkle的更新窗口不会显示。参考实现的
UpdaterManager
checkForUpdates()
中处理了这一点:
swift
func checkForUpdates() {
    NSApp.setActivationPolicy(.regular)
    NSApp.activate(ignoringOtherApps: true)
    controller.checkForUpdates(nil)
}
当更新窗口关闭时,应用会恢复为
.accessory
模式(由你现有的激活策略管理器处理)。

Hardened 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
    =
    CFBundleVersion
    (build number)
  • sparkle:shortVersionString
    =
    CFBundleShortVersionString
    (marketing version)
  • sparkle:edSignature
    = output of
    sign_update YourApp.dmg
  • length
    = file size in bytes
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
    = 文件大小(字节)