跳到正文

自动更新

在 quickgui crate 上启用 updater feature。更新器是 Rust 核心的一部分,因此 Rust 应用直接调用 quickgui::updater。Go 和 TypeScript 以扩展的形式加载同一套实现,使用相同的 feed、密钥和安装助手。

#配置

#1. 创建签名密钥

每个更新都用 Ed25519 密钥签名。密钥对只需创建一次,并在应用的整个生命周期内保留:已安装的应用只接受用构建时那把密钥签名的更新。

bunx @quickgui/cli keygen --out-dir "$HOME/.config/my-app/update-keys"
cp "$HOME/.config/my-app/update-keys/quickgui-update.pub" ./update-public-key.pub

该命令会写出 quickgui-update.pub 和 quickgui-update.key。公钥可以安全地提交;下面的配置从 update-public-key.pub 读取它。.key 文件是机密:请保存在密码管理器或 CI 的密钥存储中,绝不要放进仓库。一旦丢失,已安装的应用将永远无法再更新。

#2. 选择发布位置

把 updates.target 设为 "github" 或 "s3",并填写同名的配置段。target 决定 --upload 把文件发到哪里,以及应用检查哪些 URL,因此你无需自己填写 feed 或下载地址。两个配置段可以同时保留,只有 target 指定的那个会被使用。feed 地址会内置到应用中,所以之后修改 target 只影响修改之后构建的版本。在用户迁移完成之前,请继续向旧的 target 发布。

GitHub Releases 只需要仓库名。仓库必须是公开的,因为已安装的应用在下载更新时不带任何凭据。

import { readFileSync } from "node:fs";
import { defineConfig } from "@quickgui/cli";

export default defineConfig({
  language: "rust",
  name: "My App",
  identifier: "com.example.my-app",
  version: "1.0.0",
  entry: ".",
  updates: {
    target: "github", // publish to and update from GitHub Releases
    publicKey: readFileSync(
      new URL("./update-public-key.pub", import.meta.url),
      "utf8",
    ).trim(),
    automaticChecks: true, // default for new installs; users can change it
    changelog: "CHANGELOG.md", // optional; this file is used by default when it exists
    github: {
      repository: "example/my-app", // owner/name of a PUBLIC repository
      // tagPrefix: "v",            // release tag = tagPrefix + version, e.g. v1.2.0
    },
  },
});

Amazon S3 需要 bucket、所在区域,以及对外提供该 bucket 的公开 HTTPS 地址(bucket 自身的 URL,或其前面的 CloudFront 域名)。

import { readFileSync } from "node:fs";
import { defineConfig } from "@quickgui/cli";

export default defineConfig({
  language: "rust",
  name: "My App",
  identifier: "com.example.my-app",
  version: "1.0.0",
  entry: ".",
  updates: {
    target: "s3", // publish to and update from the bucket below
    publicKey: readFileSync(
      new URL("./update-public-key.pub", import.meta.url),
      "utf8",
    ).trim(),
    automaticChecks: true, // default for new installs; users can change it
    changelog: "CHANGELOG.md", // optional; this file is used by default when it exists
    s3: {
      bucket: "my-app-releases",
      region: "us-east-1",
      publicUrl: "https://my-app-releases.s3.us-east-1.amazonaws.com",
      // prefix: "stable",          // optional folder inside the bucket
    },
  },
});

Cloudflare R2、MinIO 等兼容 S3 的存储 还需要 endpoint,即 S3 API 的地址。publicUrl 仍然是用户下载时访问的地址,对 R2 来说是自定义域名或 bucket 的 r2.dev URL。

import { readFileSync } from "node:fs";
import { defineConfig } from "@quickgui/cli";

export default defineConfig({
  language: "rust",
  name: "My App",
  identifier: "com.example.my-app",
  version: "1.0.0",
  entry: ".",
  updates: {
    target: "s3", // publish to and update from the bucket below
    publicKey: readFileSync(
      new URL("./update-public-key.pub", import.meta.url),
      "utf8",
    ).trim(),
    automaticChecks: true, // default for new installs; users can change it
    changelog: "CHANGELOG.md", // optional; this file is used by default when it exists
    s3: {
      bucket: "my-app-releases",
      endpoint: "https://ACCOUNT_ID.r2.cloudflarestorage.com", // S3 API endpoint
      region: "auto",
      publicUrl: "https://downloads.example.com", // custom domain or https://pub-….r2.dev
      // prefix: "stable",
    },
  },
});

bucket 必须允许公开读取已上传的文件;QuickGUI 使用你的凭据上传,但不会修改 bucket 策略。

启用 crate feature。crate 版本保持项目中已有的版本:

[dependencies]
quickgui = { version = "...", features = ["updater"] }

#3. updates 的全部选项

选项是否必填含义
publicKey是quickgui-update.pub 的内容。会嵌入应用,用于校验每一个更新。
target是"github" 或 "s3"。选择使用哪个配置段。
github.repositorytarget = "github" 时必填公开 GitHub 仓库的 owner/name。
github.tagPrefix否release 标签为该前缀加版本号。默认 "v",因此版本 1.2.0 发布为 v1.2.0。
s3.buckettarget = "s3" 时必填bucket 名称。
s3.publicUrltarget = "s3" 时必填对外提供该 bucket 的公开 HTTPS 地址。不能带查询字符串或凭据。
s3.endpoint否非 AWS 服务商的 S3 API 地址。使用 AWS 时省略。
s3.region否bucket 所在区域;R2 使用 "auto"。
s3.prefix否bucket 内的文件夹,同时会追加到 publicUrl 后面。
automaticChecks否新安装的应用是否在启动时检查更新。默认 true。用户自己的选择会被记住并优先生效。
changelog否覆盖所有版本的 Markdown 更新日志。与 version 匹配的 ## x.y.z 一节会成为发行说明。存在 CHANGELOG.md 时默认使用它。
ed25519SecretKey否本地构建使用的私钥文件路径。环境变量 QUICKGUI_UPDATER_PRIVATE_KEY 的优先级更高。
manifest否设为 true 时,每次生产构建都会签名并写出更新 feed,效果等同于 --update-manifest。

#URL 从何而来

target 决定了所有地址。<file> 是类似 appcast-darwin-arm64.xml 的文件名,<tag> 是 tagPrefix 加版本号。

GitHubS3
更新 feed、install.sh、latest-linux-<arch>.txthttps://github.com/<repository>/releases/latest/download/<file><publicUrl>/<prefix>/<file>
安装程序和更新归档https://github.com/<repository>/releases/download/<tag>/<file><publicUrl>/<prefix>/<file>

在 GitHub 上,feed 始终来自标记为 Latest 的 release。草稿和预发布永远不会成为 latest,因此在你把它们发布为正式 release 之前,用户不会收到它们。--upload 始终创建草稿。

CLI 会把版本、标识符、feed URL 和公钥嵌入应用。私钥绝不会被嵌入。

#应用用法

应用就绪后启动一个 Updater,并在应用线程上读取它的事件:

use std::rc::Rc;
use quickgui::updater::{UpdateStatus, Updater};

fn start_updates(&mut self, cx: &mut quickgui::EventContext) {
    let task = cx.spawn(|task_cx: quickgui::AsyncViewContext<Self>| async move {
        let Ok((updater, mut events)) = Updater::start(quickgui::updater_options!()).await else {
            return Ok(());
        };
        let updater = Rc::new(updater);
        task_cx.update({
            let updater = updater.clone();
            move |this, _| this.updater = Some(updater)
        }).await?;
        while let Some(event) = events.next().await {
            task_cx.update(move |this, cx| {
                if event.quit_required {
                    cx.exit();
                }
                this.update_available = event.status == UpdateStatus::Available;
                this.update_version = event.version.clone();
                cx.invalidate();
            }).await?;
        }
        Ok::<(), quickgui::AsyncContextError>(())
    });
    self.update_events = task.ok();
}

quickgui::updater_options!() 会展开为 quickgui build 嵌入的设置。直接 cargo run 时它为空,更新器会像所有开发构建一样报告 Disabled。

在“检查更新”操作中调用 updater.check().await,在更新横幅中调用 updater.install().await,在设置中调用 updater.set_automatic_checks(enabled).await。它们在命令被接受或拒绝时完成,结果以事件的形式到达。updater.state() 无需原生调用即可返回最新事件。

#生命周期

在进程生命周期内保留 Updater;它属于应用,而不是某个窗口。丢弃它会取消检查和下载。quit_required 表示经过验证的安装程序正在等待应用退出:保存工作后调用 cx.exit()。如果退出被取消,助手会超时并保留当前应用。新版本启动后,核心会通知助手;如果新版本始终没有启动,助手会恢复旧版本。

#平台支持

平台更新载荷安装方式
macOS包含已签名 .app 的 ZIPSparkle,仅在启用该 feature 时嵌入
WindowsQuickGUI NSIS .exe 安装程序助手等待退出,运行经过验证的安装程序,然后重新启动
LinuxType-2 .AppImage,或 install.sh 安装的 tarball助手等待退出并替换用户拥有的 AppImage 或安装目录;新版本启动失败时会恢复旧版本

Linux .deb 包、系统二进制文件以及 root 拥有的安装会报告 Disabled。这些情况请使用平台包管理器。

quickgui build 还会生成 <Name>-<version>-linux-<arch>.tar.gz、install.sh 和 latest-linux-<arch>.txt。--upload 会发布这三个文件,使用 GitHub 时脚本有固定的 URL:curl -fsSL https://github.com/<owner>/<repo>/releases/latest/download/install.sh | sh 无需 root 即可安装到 ~/.local/<package>.app,并把应用加入应用程序菜单,这种安装会自动保持更新。设置 linux.tarball = false 可跳过它。

#发布更新

#1. 在本机发布

先提高配置中的 version:应用只会安装比自身更高的版本。在 CHANGELOG.md 中为它添加 ## <version> 一节。然后把私钥交给构建,并带上 --upload 运行:

export QUICKGUI_UPDATER_PRIVATE_KEY="$(cat ~/.config/my-app/update-keys/quickgui-update.key)"
bunx @quickgui/cli build --upload

使用 GitHub 时,先用 gh auth login 登录一次(或设置 GH_TOKEN)。使用 S3 时,设置 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY。

--upload 会构建应用、为更新文件签名、写出 feed 并发布所有内容,完成后打印每个公开 URL;使用 GitHub 时,这些 URL 在你发布草稿 release 之后才可访问。如果只想签名并写出 feed 而不上传(例如先检查 dist/),请改用 --update-manifest。每个目标平台运行一次该命令;每个目标都有自己的 feed,因此 Mac 永远不会下载到 Linux 构建。

#2. 会上传哪些文件

目标文件
macOS给新用户的 <Name>.dmg、用于更新的 <Name>-<version>-<target>.zip、appcast-<target>.xml
Windows<Name>-<version>-<target>.exe(安装与更新共用)、appcast-<target>.xml
Linux.AppImage、<Name>-<version>-<target>.tar.gz、已配置时的 .deb、install.sh、latest-linux-<arch>.txt、appcast-<target>.xml

带版本号的文件先上传,feed 最后上传,因此 feed 永远不会指向尚不存在的文件。在 GitHub 上,第一个目标会以草稿形式创建 release <tag>,并以该版本的更新日志一节作为描述,之后的目标把自己的文件追加进去。重新运行会替换同名文件。

草稿对用户不可见,更新 feed 也不会变化,因此尚未上传完整的 release 永远不会被推送给用户。所有目标都上传完成后,检查草稿并将其发布——可以在 release 页面操作,也可以运行:

gh release edit v1.2.0 --repo example/my-app --draft=false

发布后它会成为最新 release,已安装的应用会在下次检查时发现更新。S3 没有草稿阶段:文件上传后立即生效。

#3. 通过 GitHub Actions 发布

把私钥添加为名为 QUICKGUI_UPDATER_PRIVATE_KEY 的仓库 secret(Settings → Secrets and variables → Actions),然后推送一个与配置中版本号一致的标签:

# .github/workflows/release.yml
name: Release
on:
  push:
    tags: ["v*"]

permissions:
  contents: write # lets GITHUB_TOKEN create the release and upload assets

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-latest
            target: darwin-arm64
          - os: ubuntu-22.04
            target: linux-x64
          - os: windows-latest
            target: windows-x64
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - uses: dtolnay/rust-toolchain@stable
      - run: bun install
      - run: bunx @quickgui/cli build --target ${{ matrix.target }} --upload
        env:
          QUICKGUI_UPDATER_PRIVATE_KEY: ${{ secrets.QUICKGUI_UPDATER_PRIVATE_KEY }}
          GH_TOKEN: ${{ github.token }}
          # For updates.s3 instead of GH_TOKEN:
          # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

  # updates.github only: the uploads above go into a draft release. Publish it once every
  # target succeeded. Remove this job to review and publish drafts by hand.
  publish:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false
        env:
          GH_TOKEN: ${{ github.token }}

三个构建 job 会上传到同一个草稿 release,只有它们全部成功后 publish 才会让它生效,因此用户永远不会拿到缺少某个平台的 release。GitHub 的 runner 已预装 gh CLI。对外分发的 macOS 构建还应加上 --sign 和 --notarize;Windows 需要 runner 上有 NSIS(makensis);Linux 只有在安装了 appimagetool 时才会生成 AppImage(tarball 不需要任何工具)。

#发行说明

发行说明来自一个覆盖所有版本的 Markdown 更新日志。发布 1.2.0 版本时,构建会取出 ## 1.2.0 标题下的那一节,并随更新一起发布。

updates.changelog 指定该文件,路径相对于项目。不设置时,如果项目目录中存在 CHANGELOG.md,就会使用它。

# Changelog

## 1.2.0 - 2026-09-19

Faster startup and a fix for lost drafts.

- Search inside a project with **Ctrl+Shift+F**
- Windows remember their size and position
- Fix drafts being lost when the app quits during a save

## 1.1.0

- Add a tray icon
- Fix a crash when opening an empty project
  • 版本标题的格式是 ## x.y.z,其中 x.y.z 是配置中的 version。后面可以跟日期:## x.y.z - 2026-09-19。
  • 说明内容是该标题之下、直到下一个 ## 标题之前的全部内容。第一个版本标题之前的文字(例如标题或简介)永远不会被发布。
  • 如果需要,可以用 ### 划分“新增”“修复”等分组;它们属于当前这一节。
  • 每一节最多 16 KiB。
  • 最新的版本放在最上面。请面向应用的使用者来写:对他们来说有什么变化,而不是提交历史。

如果缺少该标题,或者该节内容为空,发布会失败并提示 CHANGELOG.md has no notes for this release. Add a `## 1.2.0` section,这样就不会带着错误的说明发布。完全没有更新日志的项目会不带说明发布;更新仍会提供,只显示版本号。构建还会把提取出的这一节写入 dist/<target>/release-notes.md。

出现的位置显示方式
Windows 和 Linux你的应用会原样收到这段 Markdown(event.notes)。可以用 Markdown 组件渲染,也可以直接作为文本显示。
macOSSparkle 的更新窗口会将它渲染为 Markdown。
GitHub release 页面(target = "github")它会成为该 release 的描述。

#S3 凭据

变量含义
AWS_ACCESS_KEY_ID 或 S3_ACCESS_KEY_ID有权向该 bucket 写入对象的访问密钥
AWS_SECRET_ACCESS_KEY 或 S3_SECRET_ACCESS_KEY对应的密钥
AWS_SESSION_TOKEN 或 S3_SESSION_TOKEN仅用于临时凭据

对于 R2,请创建对该 bucket 具有 Object Read & Write 权限的 API token,并使用它的 access key ID 和 secret。如果 bucket 前面有 CDN 缓存,请为 appcast-*.xml、latest-linux-<arch>.txt 和 install.sh 设置较短的缓存时间,因为它们每次发布都会变化。

#Linux 安装命令

Linux 构建还会发布 install.sh。它无需 root 即可把 tarball 安装到 ~/.local/<package>.app,把应用加入应用程序菜单,并且这种安装会自动保持更新。只需给用户一行命令:

# updates.github
curl -fsSL https://github.com/example/my-app/releases/latest/download/install.sh | sh
# updates.s3
curl -fsSL https://downloads.example.com/install.sh | sh

在同一 URL 后加上 | sh -s -- --uninstall 即可卸载。设置 linux.tarball = false 可跳过 tarball 和脚本。

#故障排查

  • 应用提示已是最新版本。 已发布的 version 必须高于已安装的版本;GitHub 上的 release 必须已经发布(--upload 创建的是草稿),并且不能标记为预发布;并且该目标的 feed 必须存在。用浏览器打开上表中的 feed URL 进行检查。
  • Set updates.publicKey and QUICKGUI_UPDATER_PRIVATE_KEY。 构建时没有私钥。请导出该变量或设置 ed25519SecretKey。
  • Update signing key does not match updates.publicKey。 私钥与配置的公钥不是一对。请使用最初的密钥;已发布出去的应用只信任它。
  • CHANGELOG.md has no notes for this release。 添加一个与 version 完全一致的 ## x.y.z 标题,并在其下至少写一行内容。
  • 状态为 disabled。 开发构建永远不会更新,.deb、系统级或 root 拥有的 Linux 安装也不会。请用生产构建测试。
  • GitHub upload failed。 检查 gh auth status、token 是否有仓库写权限(Actions 中为 contents: write),以及仓库名是否正确。
  • S3 upload failed。 检查凭据、region 和 endpoint。下载时出现 403 表示 bucket 不允许公开读取。

#原生产物

更新器被链接进可执行文件。quickgui build 会检测该 feature,并从相同版本的 @quickgui/extension-updater 中取出安装助手(quickgui-updater-helper,macOS 上为 Sparkle.framework)放在可执行文件旁。当前预构建包覆盖 macOS;在其他主机上,请先在源码检出中运行 bun packages/native/build.ts --extension updater。