自动更新
在 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.repository | target = "github" 时必填 | 公开 GitHub 仓库的 owner/name。 |
github.tagPrefix | 否 | release 标签为该前缀加版本号。默认 "v",因此版本 1.2.0 发布为 v1.2.0。 |
s3.bucket | target = "s3" 时必填 | bucket 名称。 |
s3.publicUrl | target = "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 加版本号。
| GitHub | S3 | |
|---|---|---|
更新 feed、install.sh、latest-linux-<arch>.txt | https://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 的 ZIP | Sparkle,仅在启用该 feature 时嵌入 |
| Windows | QuickGUI NSIS .exe 安装程序 | 助手等待退出,运行经过验证的安装程序,然后重新启动 |
| Linux | Type-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 组件渲染,也可以直接作为文本显示。 |
| macOS | Sparkle 的更新窗口会将它渲染为 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。
