参与贡献

编辑此文档

翻译文档,或在您的计算机上打开完整项目。

在您选择提交工作成果之前,不会发布任何内容。

更新器

Wails v3 的应用内自更新功能——支持可插拔提供程序、加密验证、原子替换,以及可自定义主题或替换的默认界面。

更新器可在应用内发布软件更新,无需自行构建下载、验证和替换流水线。它基于app.Updater运行,接受一个或多个可插拔的Provider(GitHub Releases、keygen.sh、Sparkle AppCast、开放的 Wails Update Manifest 协议或你自己的实现),使用配置的公钥验证下载内容,安全替换正在运行的二进制文件,并通过标准 Wails 事件总线公开每次状态转换。

处于“更新已就绪”状态的默认更新器窗口——包括随状态变化的图标、版本标记(v1.0.0 → v2.0.1 · 8.8 MB)、经 Markdown 渲染且包含 GFM 表格的发行说明,以及一个主要操作按钮。

快速开始

main.go
go
package main

import (
    "context"
    "log"

    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/updater"
    "github.com/wailsapp/wails/v3/pkg/updater/providers/github"
)

func main() {
    app := application.New(application.Options{Name: "Demo"})

    gh, _ := github.New(github.Config{Repository: "myorg/myapp"})
    if err := app.Updater.Init(updater.Config{
        CurrentVersion: "1.0.0",
        Providers:      []updater.Provider{gh},
    }); err != nil {
        log.Fatal(err)
    }

    if err := app.Updater.CheckAndInstall(context.Background()); err != nil {
        log.Printf("update: %v", err)
    }

    _ = app.Run()
}

这会打开框架的更新窗口、检查 GitHub、下载对应平台的构件、进行验证、替换二进制文件,然后等待用户重启。

生命周期

app.Updater是一个状态机,包含以下状态(updater.State):

状态 何时进入
unconfigured 调用Init之前
idle 调用Init之后、进行任何检查之前
checking Check正在进行
up-to-date 最近一次提供程序响应表明调用方已是最新版本
available 已发现新版本,但尚未开始下载
downloading 正在以流式方式从提供程序接收字节
verifying 下载已完成,正在检查签名或摘要
installing 正在解包已验证的字节,并通过重命名将其移入暂存目录
ready 更新已暂存;调用Restart以应用更新
error 此前任一步骤失败

你可以随时通过app.Updater.State()读取当前状态。每次状态转换也会发出一个 Wails 事件(参见事件)。

Restart 会等待辅助进程执行到 application.New,然后才请求正在运行的应用退出。默认启动超时为 30 秒。如果应用在 application.New 之前需要较长时间进行初始化,请将 Config.HelperReadyTimeout 设置为更长的时长,例如 time.Minute。零值使用默认值;负时长会被拒绝。如果启动超时,Restart 会返回 updater.ErrHelperNotReady,并保持当前应用运行。

更新会暂存在已安装二进制文件旁边(macOS 上位于 .app 包旁边)的 wails-update-* 目录中,因此该目录必须可写。

在 AppImage 中,更新程序会替换并重新启动 $APPIMAGE 所指向的 .AppImage 文件,而不是只读挂载。

默认窗口会自动反映当前状态——例如,当Check返回没有可用升级时,用户会看到以下内容,并通过关闭将其关闭:

处于“已是最新版本”状态的默认更新器窗口——绿色对勾、“你使用的是最新版本”标题,以及唯一的“关闭”按钮。

提供程序

任何满足此接口的实现都可以作为Provider:

go
type Provider interface {
    Name() string
    Check(ctx context.Context, req CheckRequest) (*Release, error)
    Download(ctx context.Context, r *Release, dst io.Writer, onProgress func(written, total int64)) error
}

源代码树中随附了四种实现。

GitHub Releases — updater/providers/github

github provider
go
gh, err := github.New(github.Config{
    Repository:    "myorg/myapp",     // your owner/repo (required)
    Token:         "ghp_…",           // optional; raises rate limit + private repos
    Prerelease:    false,             // include pre-releases in latest lookup
    ChecksumAsset: "SHA256SUMS",      // optional sibling asset for digest verification
    BaseURL:       "",                // optional override (e.g. GitHub Enterprise)
    AssetMatcher:  nil,               // optional custom asset-picker; nil uses DefaultAssetMatcher
    HTTPClient:    nil,               // optional client override
})

默认构件匹配器根据文件名中包含的GOOS和GOARCH子字符串进行选择,并可识别常见别名(amd64 / x86_64 / x64、arm64 / aarch64、386 / i386 / x86 / ia32)。如需使用自定义命名方案:

go
gh, _ := github.New(github.Config{
    Repository: "myorg/myapp",
    AssetMatcher: func(req updater.CheckRequest, assets []github.ReleaseAsset) int {
        for i, a := range assets {
            if strings.Contains(a.Name, "my-naming-convention") &&
               strings.Contains(a.Name, req.Platform) {
                return i
            }
        }
        return -1 // no match
    },
})

ChecksumAsset是同一发行版本中另一个构件的名称,其内容为<sha256> <filename>格式的行(即sha256sum和shasum -a 256生成的格式)。提供程序会在Check期间获取该构件,找到与所选构件匹配的行,并填充Release.Verification.Digest,以便框架验证下载内容。

keygen.sh — updater/providers/keygen

keygen provider
go
kg, err := keygen.New(keygen.Config{
    Account:    "your-account-slug", // required
    Product:    "product-uuid",      // optional but recommended when account has multiple products
    Package:    "",                  // optional further narrowing
    Channel:    "stable",            // "stable" / "rc" / "beta" / "alpha" / "dev"
    Filetype:   "",                  // optional artifact filetype filter ("dmg", "exe", …)
    Token:      "prod-…",            // product / environment / user / admin token; wins over LicenseKey
    LicenseKey: "",                  // license key auth (used only when Token is empty)
    BaseURL:    "",                  // optional API base override
    HTTPClient: nil,                 // optional client override; redirect-strip wrapper still applied
})

提供程序会自动将 keygen.sh 针对每个构件的 SHA-512校验和及 Ed25519ph 签名映射到框架的Release.Verification块,无需额外配置。

令牌格式:keygen.sh 令牌带有角色前缀(admi- / prod- / envi- / user-)。你在仪表板中看到的原始 UUID 是令牌的标识符,并非其密钥值;密钥仅在创建令牌时可见。详情请参阅 keygen.sh 的身份验证文档。

Sparkle AppCast — updater/providers/appcast

appcast provider
go
ac, err := appcast.New(appcast.Config{
    URL:        "https://your.app/appcast.xml", // required
    Channel:    "stable",                       // optional sparkle:channel filter
    HTTPClient: nil,                            // optional client override
})

无需更改即可接入现有的 Sparkle / WinSparkle 基础设施。它从 Feed 中读取sparkle:shortVersionString、<enclosure url type length sparkle:os sparkle:edSignature>和sparkle:channel。

不支持 Sparkle 1的 DSA 签名(sparkle:dsaSignature);使用该签名方案的项目应轮换为 EdDSA(Sparkle 2)。

Wails 更新清单 — updater/providers/endpoint

endpoint provider
go
ep, err := endpoint.New(endpoint.Config{
    URL:        "https://updates.example.com/check", // required; supports {{platform}} / {{arch}} / {{version}} / {{channel}} placeholders
    Channel:    "stable",                            // optional channel filter
    Headers:    nil,                                 // optional headers, e.g. {"Authorization": "License <key>"}
    HTTPClient: nil,                                 // optional client override; redirect-strip wrapper still applied
})

使用开放的Wails Update Manifest 协议:通过一个 JSON 文档描述最新发行版本及其各平台构件,并内嵌校验和与签名。同一文档既可托管在静态文件主机上(S3、GitHub Pages 或任意 CDN——每个渠道发布一份列出所有平台的清单),也可由动态更新服务器提供(提供程序会在每次检查时发送platform、arch、version和channel,因此服务器可以只返回一个构件,或根据许可证决定是否提供更新)。

借助 URL 占位符,只需一行配置即可定义静态布局:

go
ep, _ := endpoint.New(endpoint.Config{
    URL: "https://cdn.example.com/updates/{{platform}}/{{arch}}/stable.json",
})

每次清单请求都会发送所配置的请求头;仅当构件位于清单自身所在的主机上,并且未从https降级到http时,构件下载才会复用这些请求头。任何跨源或降级重定向都会移除Authorization请求头。

发布端由 CLI 负责:wails3 updater manifest可通过一条命令计算摘要、签名并描述发行文件,而wails3 updater verify会在上传前重新检查结果。参见使用 wails3 CLI 发布。

回退链

Config.Providers是有序的。更新器会依次遍历:第一个返回发行版本的提供程序胜出;第一个报告“已是最新版本”的提供程序会使该链短路(回退用于“主要提供程序不可访问”,而非“提供程序意见不一致”)。发生错误时会继续尝试下一个提供程序。

go
app.Updater.Init(updater.Config{
    CurrentVersion: "1.0.0",
    Providers: []updater.Provider{
        kg, // primary: licensed customers
        gh, // fallback: public mirror
    },
})

编写自己的提供程序

典型实现只需三个方法、约150行代码。Updater 负责验证、原子暂存、替换和窗口;提供程序代码负责确定下一个版本并以流式方式传输其字节:

go
type CustomProvider struct { /* config */ }

func (p *CustomProvider) Name() string { return "custom" }

func (p *CustomProvider) Check(ctx context.Context, req updater.CheckRequest) (*updater.Release, error) {
    // Hit your update endpoint, decide whether req.CurrentVersion is current,
    // and return either nil (no upgrade) or a *Release with Artifact + optional
    // Verification populated.
    // Errors here drop through to the next provider in Config.Providers.
}

func (p *CustomProvider) Download(ctx context.Context, r *updater.Release, dst io.Writer, onProgress func(int64, int64)) error {
    // Stream the artifact's bytes to dst. Call onProgress(written, total) as
    // bytes flow past; the Updater debounces emits to ~10 Hz on the event bus.
}

请参考项目内置的提供程序——每个提供程序都仅由一个 Go 文件构成。

密码学验证

框架的验证器使用Config.PublicKey作为信任根来验证发布版本:

go
//go:embed publickey.pem
var publicKey []byte

app.Updater.Init(updater.Config{
    CurrentVersion: "1.0.0",
    Providers:      []updater.Provider{...},
    PublicKey:      publicKey,
})

支持的算法(Release.Verification.SignatureAlgo):

算法 签名内容 说明
ed25519 制品的 SHA-256摘要 供 Sparkle EdDSA 使用
ed25519ph 通过 Ed25519ph 的预哈希对完整制品签名(内部使用 SHA-512) 供 keygen.sh 使用
ecdsa-p256 制品的 SHA-256摘要 同时接受原始r∥s签名和 DER 签名

如果发布版本附带哈希但没有签名,还支持仅摘要验证(DigestAlgo:sha256 / sha512)。

Config.PublicKey是签名验证的唯一信任锚——发布源无法替换为自己的密钥。若发布版本带有Signature,但未配置Config.PublicKey,验证将以安全方式失败。验证器会在下载期间以流式方式计算摘要,因此即使更新包大小达到数 GB,验证也不会增加额外的磁盘遍历。

生成签名密钥

bash
wails3 updater genkey
# updater.key       — keep secret, use to sign releases
# updater.key.pub   — bundle in your app via go:embed

私钥采用 PKCS#8 PEM 格式,公钥采用 PKIX PEM 格式;Config.PublicKey可直接接受.pub文件(也接受原始32字节密钥或其 base64 编码,genkey会输出后者以便内联)。请使用wails3 updater manifest -key updater.key ...或wails3 updater sign为发布版本签名;请参阅使用 wails3 CLI 发布。

也可以使用 Go:

go
import "crypto/ed25519"
import "crypto/rand"

pub, priv, _ := ed25519.GenerateKey(rand.Reader)
// Persist `priv` securely (HSM, signing CI, etc.); embed `pub` in your binary.

制品格式

提供程序以流式方式传输您所发布文件的字节;框架随后会在替换前将其解包:

  • 单个二进制文件(例如myapp-linux-amd64)——直接使用,无需处理。在 Linux 上很常见。
  • .zip——就地解压。归档必须恰好包含一个顶层条目(通常是 macOS .app包或单个二进制文件)。这是 macOS 的推荐打包格式。
  • .tar.gz / .tgz——按照同样的单一顶层条目规则就地解压。适用于随二进制文件一同提供运行时目录树的 Linux 发行版。

包含多个顶层条目的归档会被拒绝:框架只替换一个磁盘目标,因此当归档包含多个对象时,“将此归档替换到目标位置”的含义并不明确。v1 不支持.dmg和.pkg(macOS)以及.msi(Windows);请改为分发该包的.zip。解压过程会防止 zip-slip 攻击、拒绝指向归档根目录之外的符号链接,并限制解压后的总大小(2 GiB)和条目数(50 000)。

默认窗口

app.Updater.CheckAndInstall(ctx)会打开一个由框架管理、尺寸为520×540的窗口,其中包含:

  • 随状态变化的主图标(有可用更新/正在下载时为蓝色 ↓,准备就绪/已是最新版本时为绿色 ✓,发生错误时为红色 !)
  • 版本标签:v1.0.0 → v2.0.1 · 8.8 MB
  • 可滚动的发布说明面板,其中显示渲染后的 Markdown(段落、粗体/斜体、列表、GFM 表格、行内代码、围栏代码块、h1–h3、链接)
  • 每种状态对应一个主要操作(安装 / 重启并应用 / 重试)
  • 幽灵样式的次要操作(跳过此版本 / 稍后提醒我)
  • 通过prefers-color-scheme支持深色/浅色模式
  • 总大小未知时显示不确定进度的流光效果

该窗口监听 Wails 事件总线上的updater:*事件,并将updater:user:*操作发送回 Go。

通过 CSS 变量设置主题

go
app.Updater.Init(updater.Config{
    // …
    Window: &updater.BuiltinWindow{
        CSS: `:root { --accent: #ff6f00; --bg: #1a1a1a; --fg: #fafafa; }`,
    },
})

默认样式表公开以下变量,您可以覆盖其中任意变量:

变量 默认值(浅色) 默认值(深色)
--bg #f8f8fa #1a1a1c
--surface #ffffff #232326
--surface-2 #f0f0f3 #2c2c30
--fg #1d1d1f #f5f5f7
--fg-dim #6b6b73 #b0b0b8
--fg-faint #99999f #7a7a82
--border #d6d6dc #3a3a3e
--accent #0a84ff #0a84ff
--accent-fg #ffffff —
--success #34c759 —
--error #ff3b30 —
--radius 10px —
--font 系统字体栈 —

替换模板

提供你自己的 HTML;它只需监听wails:updater:*事件并发出wails:updater:user:*操作:

go
app.Updater.Init(updater.Config{
    // …
    Window: &updater.BuiltinWindow{
        HTML: myCustomTemplate,
    },
})

InitialHTML 窗口在加载时没有资产服务器源,因此无法动态获取/wails/runtime.js。在这种窗口中与宿主通信有两种方式:

  1. 直接编写 HTML。对于任何在打开时设置了WebviewWindowOptions.AllowSimpleEventEmit = true和HTML的窗口,框架都会自动注入一个精简的window.wails.Events垫片——更新程序的内置路径和 BYO 路径正是如此。无需构建步骤。下面的示例采用此方式。
  2. 使用你偏好的打包工具(Vite、esbuild、Rollup)打包@wailsio/runtime,并在构建时将其导入自定义 HTML。Events.On完全在客户端运行,因此可以开箱即用;Events.Emit则通过运行时的 fetch 传输层,而 null 源会使其失效——因此,请通过运行时的setTransport钩子安装一个精简的 postMessage 传输层,经由window._wails.invoke("wails:event:emit:<name>")进行路由。如果window.wails.Events已在作用域中,框架注入不会执行任何操作,因此两种方式不会冲突。

无论采用哪种方式,自定义 HTML 中编写的 JS 都会调用相同的Events.On/Events.Emit API:

html
<script>
const { On, Emit } = window.wails.Events;

On("wails:updater:update-available", (e) => {
    const rel = e.data ?? e;
    document.getElementById("ver").textContent = rel.version;
});

document.getElementById("install").addEventListener("click",
    () => Emit("wails:updater:user:install"));

// Ask the host to replay the current state so we paint correctly on (re)open.
Emit("wails:updater:window:ready");
</script>

该垫片公开了使用裸名称事件所需的现代运行时功能子集:Events.On(name, cb)返回取消订阅函数,Events.Emit(nameOrEventObject)则通过受控的wails:event:emit: postMessage 路径路由到宿主。它会在页面加载时安装一次,并且先于你的任何内联脚本运行。

如果你希望覆盖该垫片(或者正通过其他方式加载完整运行时),请在页面的第一个<script>标签执行前设置window.wails.Events,注入便会自行跳过。

窗口外观

无需修改 HTML 即可覆盖窗口选项(尺寸、无边框、始终置顶):

go
Window: &updater.BuiltinWindow{
    Options: updater.WindowOptions{
        Title:         "My App Updater",
        Width:         640,
        Height:        480,
        Frameless:     true,
        AlwaysOnTop:   true,
        DisableResize: false,
    },
},

使用你自己的窗口

使用你自行创建的*application.WebviewWindow来驱动更新流程。更新程序会在你的窗口上调用Show()/Close()/EmitEvent()——由你的 HTML 决定渲染内容:

一个“使用你自己的窗口”的更新程序窗口,采用粉橙渐变背景、单张白色圆角卡片和自定义字体排印,并由相同的更新程序事件驱动可见状态。它展示了默认 UI 可以被多么彻底地替换。

go
myWin := app.Window.NewWithOptions(application.WebviewWindowOptions{
    Title:                "My Updater",
    Width:                520, Height: 460,
    HTML:                 myCustomHTML,
    AllowSimpleEventEmit: true,           // see security note below
})
app.Updater.Init(updater.Config{
    // …
    Window: updater.BYOWindow(myWin.AsUpdaterWindow()),
})

你的 HTML 与内置模板一样使用window.wails.Events.On/Events.Emit——只要窗口设置了AllowSimpleEventEmit: true,无论它归框架还是你所有,框架自动注入的垫片都会随之提供。有关该 API,请参阅替换模板。

无界面模式

go
app.Updater.Init(updater.Config{
    // …
    Window: updater.WindowNone,
})

整个过程中都不会打开窗口。请从你自己的 UI(或现有主窗口)订阅updater:*事件,并从按钮处理程序调用app.Updater.CheckAndInstall(ctx)。这适用于仅在发现更新时才应显示提示的定期后台检查,也适用于将更新流程集成到自定义设置面板中的应用。

事件

Go 和 JavaScript 都通过标准 Wails 事件总线订阅。不要手动输入传输字符串——请使用 updater 包(Go)或 runtime 包(JS)导出的常量。两层使用同一组名称,并通过回归测试保持同步。

从 Go 使用

这些常量位于github.com/wailsapp/wails/v3/pkg/updater中。通过app.Event.On(name, fn)订阅;回调会收到一个*application.CustomEvent,其Data字段是事件参考中列出的强类型载荷——请使用类型断言,不要进行 JSON 解码:

main.go
go
import (
    "log"

    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/updater"
)

// …

app.Event.On(updater.EventUpdateAvailable, func(e *application.CustomEvent) {
    rel, ok := e.Data.(*updater.Release)
    if !ok { return }
    log.Printf("update found: %s", rel.Version)
})

app.Event.On(updater.EventDownloadProgress, func(e *application.CustomEvent) {
    p, ok := e.Data.(updater.Progress)
    if !ok { return }
    log.Printf("%d / %d bytes (%.0f KB/s)", p.Written, p.Total, p.Rate/1024)
})

app.Event.On(updater.EventError, func(e *application.CustomEvent) {
    info, ok := e.Data.(updater.ErrorInfo)
    if !ok { return }
    log.Printf("update failed during %s: %s", info.Stage, info.Message)
})

所有可用的 Go 常量:

常量 传输字符串
updater.EventCheckStarted wails:updater:check-started
updater.EventUpdateAvailable wails:updater:update-available
updater.EventNoUpdate wails:updater:no-update
updater.EventDownloadStarted wails:updater:download-started
updater.EventDownloadProgress wails:updater:download-progress
updater.EventDownloadComplete wails:updater:download-complete
updater.EventVerifying wails:updater:verifying
updater.EventInstalling wails:updater:installing
updater.EventUpdateReady wails:updater:update-ready
updater.EventError wails:updater:error
updater.EventMeta wails:updater:meta
updater.EventWindowReady wails:updater:window:ready
updater.EventUserInstall wails:updater:user:install
updater.EventUserSkip wails:updater:user:skip
updater.EventUserRemind wails:updater:user:remind
updater.EventUserCancel wails:updater:user:cancel
updater.EventUserRestart wails:updater:user:restart

从 JavaScript

这些常量位于@wailsio/runtime的Updater.Events下。它们与 Go 中的名称相同,并按子命名空间(User.*、Window.*)组织,便于通过自动补全发现:

js
import { Events, Updater } from "@wailsio/runtime";

Events.On(Updater.Events.UpdateAvailable, (e) => {
    console.log("update found:", e.data.version);
});

Events.On(Updater.Events.DownloadProgress, (e) => {
    const p = e.data;
    console.log(`${p.written} / ${p.total} bytes (${(p.rate/1024).toFixed(1)} KB/s)`);
});

Events.On(Updater.Events.Error, (e) => {
    const info = e.data;
    console.error(`update failed during ${info.stage}: ${info.message}`);
});

自定义 HTML 向宿主回传的用户操作事件位于Updater.Events.User下:

js
import { Updater } from "@wailsio/runtime";

document.getElementById("install-btn").addEventListener("click", () => {
    // The framework window does this internally via the postMessage shim;
    // shown here for BYO templates that need to drive the flow themselves.
    window._wails.invoke("wails:event:emit:" + Updater.Events.User.Install);
});

事件参考

订阅端(宿主 → 页面):

常量(Go) 常量(JS) 载荷 触发时机
updater.EventCheckStarted Updater.Events.CheckStarted 无 每次Check往返之前
updater.EventUpdateAvailable Updater.Events.UpdateAvailable *Release Check发现了较新的版本
updater.EventNoUpdate Updater.Events.NoUpdate 无 Check确认当前已是最新版本
updater.EventDownloadStarted Updater.Events.DownloadStarted *Release 开始传输字节数据
updater.EventDownloadProgress Updater.Events.DownloadProgress Progress 下载期间约为10 Hz
updater.EventDownloadComplete Updater.Events.DownloadComplete *Release 所有字节均已写入,尚未验证
updater.EventVerifying Updater.Events.Verifying *Release 开始检查签名/摘要
updater.EventInstalling Updater.Events.Installing *Release 开始解包和暂存
updater.EventUpdateReady Updater.Events.UpdateReady *Release 等待重启
updater.EventError Updater.Events.Error ErrorInfo 任一阶段失败
updater.EventMeta Updater.Events.Meta Meta 每个会话一次,在重放快照之前

页面端(页面 → 宿主)——如果编写自定义模板,你的代码需订阅这些事件:

常量(Go) 常量(JS) 触发时机
updater.EventWindowReady Updater.Events.Window.Ready 窗口加载完成;宿主端会重放当前状态
updater.EventUserInstall Updater.Events.User.Install 处于available状态时的主要操作
updater.EventUserRestart Updater.Events.User.Restart 处于ready状态时的主要操作
updater.EventUserSkip Updater.Events.User.Skip “跳过此版本”
updater.EventUserRemind Updater.Events.User.Remind “稍后提醒我”
updater.EventUserCancel Updater.Events.User.Cancel 关闭按钮

API 参考

updater.Config

字段 类型 说明
CurrentVersion string 必填。与发布版本所用标签相同的字符串(不带v前缀)
Providers []updater.Provider 必填。按顺序尝试的回退链
PublicKey []byte PEM 或原始字节。可选,但签名发布版本在未提供该值时会以安全方式失败
CheckInterval time.Duration 非零值会启动后台轮询循环,并调用CheckAndInstall
Platform string 覆盖runtime.GOOS以选择构件
Arch string 覆盖runtime.GOARCH以选择构件
Channel string 目前仅供参考;通道筛选取决于具体提供程序
Window updater.WindowOption nil(内置默认值)、&BuiltinWindow{…}、BYOWindow(handle)或WindowNone

*updater.Updater的方法

签名 用途
Init(cfg Config) error 进行配置。第二次调用时返回ErrAlreadyConfigured
State() State 当前生命周期阶段
CurrentVersion() string 传给Init的版本
Check(ctx) (*Release, error) 依次遍历提供程序链。(rel, nil) = 发现更新,(nil, nil) = 已是最新版本,(nil, err) = 全部失败
DownloadAndInstall(ctx) error 以流式方式传输、验证、解压(如果是归档文件)并暂存。需要先调用Check
CheckAndInstall(ctx) error 便捷操作:打开窗口,调用Check;如果发现更新,再调用DownloadAndInstall
Restart(ctx) error 启动辅助程序,调用Host.Quit,然后退出;辅助程序会替换文件并重新启动应用
DownloadedPath() string 暂存更新在磁盘上的位置;如果没有暂存更新,则为""
SkipVersion(v string) 将v记录为已跳过;后续Check会将其视为已是最新版本
SkippedVersion() string 读取当前跳过的版本
StopPeriodicCheck() 取消由Config.CheckInterval启动的计时器,并等待循环返回

错误

哨兵值 返回方
ErrAlreadyConfigured 首次成功后调用Init
ErrNotConfigured 在Init之前执行的任何操作
ErrNoPendingRelease 未先调用Check便调用DownloadAndInstall
ErrDownloadInProgress 已有另一个流程运行时调用DownloadAndInstall
ErrNotReady 没有已暂存的更新时调用Restart

交换过程的工作原理

Restart会设置哨兵环境变量并重新执行当前二进制文件。application.New在启动时检测这些变量,并转入辅助程序模式:

  1. 辅助程序最多等待30秒,让父进程 PID 退出(在 Windows 上,platformIsAlive通过syscall.OpenProcess + GetExitCodeProcess轮询;在 Unix 上则通过os.FindProcess + proc.Signal(syscall.Signal(0))轮询)。
  2. 辅助程序会备份目标(文件采用复制,macOS .app包目录采用递归复制)。
  3. 辅助程序用已暂存的制品替换目标,最多重试20次,每次尝试之间退避500毫秒:
  • Unix — os.RemoveAll(target) + os.Rename(newPath, target)。指向旧 inode 的已打开文件描述符仍然有效。
  • Windows — os.Rename(target, target.old.<nanos>) + os.Rename(newPath, target)。Windows 允许重命名映像仍处于映射状态的文件,但不允许删除它们;下次更新时,辅助程序会清理所有遗留的.old.*同级文件,前提是拥有它们的内核映射已释放。
  1. 辅助程序会在新二进制文件上恢复原可执行文件的模式(下载的文件使用默认 umask 创建,这会在 Unix 上去除+x;在 Windows 上此操作不执行任何动作)。
  2. 辅助程序会清除辅助程序模式的环境变量,并重新启动已完成替换的二进制文件。
  3. 辅助程序退出。

如果启动失败,辅助程序会恢复备份。如果父进程未在30秒内退出,辅助程序会在接触目标前中止(因此,即使应用退出对话框阻塞了Quit,用户仍可保留可用的应用)。

对于以.zip形式分发的 macOS .app包(推荐的打包方式),系统会在验证与就绪之间解压归档,以便辅助程序获得一个真实目录并将其交换到目标位置。

定期检查

go
app.Updater.Init(updater.Config{
    // …
    CheckInterval: 6 * time.Hour,
})

当CheckInterval > 0时,后台 goroutine 会按配置的时间间隔调用CheckAndInstall。如果定时信号到达时已有另一个流程正在进行(检查、下载、验证或安装),该信号会被丢弃,因为不支持并发状态机。

若要在后台静默轮询且仅在发现内容时显示提示,请设置Window: updater.WindowNone,并在你自己的 UI 中响应EventUpdateAvailable。

跳过与稍后提醒

默认窗口中的“跳过此版本”按钮会通过SkipVersion(rel.Version)记录可用版本。后续Check会发现同一版本并将其视为已是最新版本(直到用户更新CurrentVersion;成功执行Restart后会自动更新)。“稍后提醒我”只会关闭窗口,不记录任何内容。

go
// Reading what the user skipped (e.g. to surface in app settings)
if v := app.Updater.SkippedVersion(); v != "" {
    log.Printf("user skipped %s", v)
}

// Programmatically clearing the skip:
app.Updater.SkipVersion("")

分发检查清单

发布将由更新程序安装的版本前:

  1. 选择正确的归档格式。macOS:包含.app包的.zip。Linux:单个二进制文件或.tar.gz。Windows:单个.exe或.zip。不支持.dmg、.msi或.pkg。
  2. 使用与Config.PublicKey匹配的私钥签名制品。对于由提供程序发布的源(keygen.sh、AppCast),请遵循各提供程序的签名流程。对于使用ChecksumAsset的 GitHub Releases,请使用sha256sum / shasum -a 256生成SHA256SUMS文件。
  3. 确保版本字符串匹配。Config.CurrentVersion必须与发布版本的版本标签完全匹配(例如1.0.0 ↔ 标签v1.0.0;提供程序端会移除开头的v)。
  4. 发布前,至少在目标平台上测试一次交换过程——代码签名、公证和 Gatekeeper 处理因平台而异,更新程序本身不涵盖这些操作。

故障排除

“签名需要公钥,但未配置公钥” — 发布版本包含Signature字段,但Config.PublicKey为空。请设置公钥,或修改发布流水线,使其不包含签名。

“摘要不匹配” — 下载的字节与提供程序所承诺的内容不匹配。通常是下载不完整(网络短暂故障)或制品损坏。重新运行通常可以解决。

窗口打开后立即消失,既无 Markdown,也无进度 — 你的自定义 HTML 没有调用wails:runtime:ready。请参阅替换模板中的垫片代码。

Windows 更新始终无法完成;辅助程序日志显示“remove old (attempt N): Access is denied” — 这只会发生在此 PR 的de764fb之前版本中;当前实现采用先重命名到一旁的方式,不会遇到此问题。请升级。

macOS Gatekeeper 阻止已交换的二进制文件 — 必须在端到端流程中保留代码签名。请为原始.app签名;如果你的构建流水线在更新时修改权利配置,还要并且重新签名将要重启的二进制文件。

另请参阅

编辑页面

最后更新: