暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Tauri 应用篇 - 自动通知应用升级

浮之静 2022-10-14
1671


这篇文章会有一点啰嗦,我希望想把解决问题的一些思路展现出来,给遇到问题无从下手的朋友带来一些启发。

签名

Tauri 通过签名来保证安全更新应用。 签名更新应用需要做两件事:

  1. 私钥 (privkey) 用于签署应用的更新,必须严密保存。此外,如果丢失了此密钥,将无法向当前用户群发布新的更新,将其保存在安全的地方至关重要。

  2. tauri.conf.json
    中添加公钥 (pubkey),以在安装前验证更新存档。

生成签名

使用 Tauri CLI 提供的命令可以生成密钥(.pub
后缀的文件为公钥):

tauri signer generate -w ~/.tauri/omb.key           

$ tauri signer generate -w Users/lencx/.tauri/omb.key
Generating new private key without password.
Please enter a password to protect the secret key.
Password:
Password (one more time):
Deriving a key from the password in order to encrypt the secret key... done

Your keypair was generated successfully
Private: /Users/lencx/.tauri/omb.key (Keep it secret!)
Public: /Users/lencx/.tauri/omb.key.pub
---------------------------

Environment variabled used to sign:
`TAURI_PRIVATE_KEY` Path or String of your private key
`TAURI_KEY_PASSWORD` Your private key password (optional)

ATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not works.
---------------------------

✨ Done in 39.09s.

复制

⚠️ 注意:如果丢失了私钥或密码,将无法签署更新包并且更新将无法正常工作(请妥善保管)。

tauri.conf.json 配置

{
"updater": {
"active": true,
"dialog": true,
"endpoints": ["https://releases.myapp.com/{{target}}/{{current_version}}"],
"pubkey": "YOUR_UPDATER_PUBKEY"
},
}

复制
  • active - 布尔值,是否启用,默认值为 false

  • dialog - 布尔值,是否启用内置新版本提示框,如果不启用,则需要在 JS 中自行监听事件并进行提醒

  • endpoints - 数组,通过地址列表来确定服务器端是否有可用更新,字符串 {{target}} 和 {{current_version}} 会在 URL 中自动替换。如果指定了多个地址,服务器在预期时间内未响应,更新程序将依次尝试。endpoints 支持两种格式:

    • 动态接口[1] - 服务器根据客户端的更新请求确定是否需要更新。 如果需要更新,服务器应以状态代码 200 OK
      进行响应,并在正文中包含更新 JSON。 如果不需要更新,服务器必须响应状态代码 204 No Content

    • 静态文件[2] - 备用更新技术使用纯 JSON 文件,将更新元数据存储在 gist[3]github-pages[4] 或其他静态文件存储中。

  • pubkey - 签名的公钥

实现步骤

拆解问题

要实现自动升级应用主要分为以下几个步骤:

  1. 生成签名(公私钥):

    • 私钥用于设置打包(tauri build
      )的环境变量

    • 公钥用于配置 tauri.conf.json -> updater.pubkey

  2. 向客户端推送包含签名及下载链接的更新请求,有两种形式:

    • 动态接口返回 json 数据

    • 静态资源返回 json 文件

  3. 将 2 中的更新请求地址配置在 tauri.conf.json -> updater.endpoints

  4. 通过将 tauri.conf.json -> updater.dialog
    配置为 true
    ,启用内置通知更新应用的弹窗。设置为 false 则需要自行通过 js 事件来处理(暂不推荐,喜欢折腾的朋友可以自行尝试)

因为应用的跨平台打包借助了 github action
的工作流来实现,具体可以参考【Tauri 入门篇 - 跨平台编译】[5],所以更新也同样使用 github action
来实现,充分发挥 github 的能力(简单来说,就是不需要借助其他第三方平台或服务就可以实现整个应用的自动化发布更新)。

梳理流程

  • 在本地生成公私钥

  • 加签名构建跨平台应用(通过 github action 设置签名环境变量)

  • 对构建出的安装包解析,生成静态资源文件(通过脚本实现安装包信息获取)

  • 推送更新请求采用静态资源的方式(可以将 json 文件存储在 github pages)

  • 将 github pages 的资源地址配置到 tauri.conf.json -> updater.endpoints

代码实现

Step1

生成公私钥

tauri signer generate -w ~/.tauri/omb.key

复制

配置公钥 pubkey
~/.tauri/omb.key.pub
)及资源地址 endpoints
(github pages 地址):

{
"package": {
"productName": "OhMyBox",
"version": "../package.json"
},
"tauri": {
"updater": {
"active": true,
"dialog": true,
"endpoints": ["https://lencx.github.io/OhMyBox/install.json"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU5MEIwREEzNDlBNzdDN0MKUldSOGZLZEpvdzBMNmFOZ2cyY2NPeTdwK2hsV3gwcWxoZHdUWXRZWFBpQTh1dWhqWXhBdkl0cW8K"
}
}
}

复制

Step2

在项目根路径下创建 scripts
目录,然后在 scripts
下依次创建 release.mjs
updatelog.mjs
updater.mjs
三个 .mjs[6] 文件:

  • scripts/release.mjs
    - 版本发布,因发布需涉及多处改动(如版本,版本日志,打 tag 标签等等),故将其写成脚本,减少记忆成本

  • scripts/updatelog.mjs
    - 版本更新日志处理,供 scripts/updater.mjs
    脚本使用

  • scripts/updater.mjs
    - 生成应用更新需要的静态文件

# 安装开发依赖
yarn add -D node-fetch @actions/github

复制
// scripts/release.mjs

import { createRequire } from 'module';
import { execSync } from 'child_process';
import fs from 'fs';

import updatelog from './updatelog.mjs';

const require = createRequire(import.meta.url);

async function release() {
const flag = process.argv[2] ?? 'patch';
const packageJson = require('../package.json');
let [a, b, c] = packageJson.version.split('.').map(Number);

if (flag === 'major') { // 主版本
a += 1;
b = 0;
c = 0;
} else if (flag === 'minor') { // 次版本
b += 1;
c = 0;
} else if (flag === 'patch') { // 补丁版本
c += 1;
} else {
console.log(`Invalid flag "${flag}"`);
process.exit(1);
}

const nextVersion = `${a}.${b}.${c}`;
packageJson.version = nextVersion;

const nextTag = `v${nextVersion}`;
await updatelog(nextTag, 'release');

// 将新版本写入 package.json 文件
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2));

// 提交修改的文件,打 tag 标签(tag 标签是为了触发 github action 工作流)并推送到远程
execSync('git add ./package.json ./UPDATE_LOG.md');
execSync(`git commit -m "v${nextVersion}"`);
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
execSync(`git push`);
execSync(`git push origin v${nextVersion}`);
console.log(`Publish Successfully...`);
}

release().catch(console.error);

复制
// scripts/updatelog.mjs

import fs from 'fs';
import path from 'path';

const UPDATE_LOG = 'UPDATE_LOG.md';

export default function updatelog(tag, type = 'updater') {
const reTag = /## v[\d\.]+/;

const file = path.join(process.cwd(), UPDATE_LOG);

if (!fs.existsSync(file)) {
console.log('Could not found UPDATE_LOG.md');
process.exit(1);
}

let _tag;
const tagMap = {};
const content = fs.readFileSync(file, { encoding: 'utf8' }).split('\n');

content.forEach((line, index) => {
if (reTag.test(line)) {
_tag = line.slice(3).trim();
if (!tagMap[_tag]) {
tagMap[_tag] = [];
return;
}
}
if (_tag) {
tagMap[_tag].push(line);
}
if (reTag.test(content[index + 1])) {
_tag = null;
}
});

if (!tagMap?.[tag]) {
console.log(
`${type === 'release' ? '[UPDATE_LOG.md] ' : ''}Tag ${tag} does not exist`
);
process.exit(1);
}

return tagMap[tag].join('\n').trim() || '';
}

复制
// scripts/updater.mjs

import fetch from 'node-fetch';
import { getOctokit, context } from '@actions/github';
import fs from 'fs';

import updatelog from './updatelog.mjs';

const token = process.env.GITHUB_TOKEN;

async function updater() {
if (!token) {
console.log('GITHUB_TOKEN is required');
process.exit(1);
}

// 用户名,仓库名
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(token);

// 获取 tag
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});

// 过滤包含 `v` 版本信息的 tag
const tag = tags.find((t) => t.name.startsWith('v'));
// console.log(`${JSON.stringify(tag, null, 2)}`);

if (!tag) return;

// 获取此 tag 的详细信息
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});

// 需要生成的静态 json 文件数据,根据自己的需要进行调整
const updateData = {
version: tag.name,
// 使用 UPDATE_LOG.md,如果不需要版本更新日志,则将此字段置空
notes: updatelog(tag.name),
pub_date: new Date().toISOString(),
platforms: {
win64: { signature: '', url: '' }, // compatible with older formats
linux: { signature: '', url: '' }, // compatible with older formats
darwin: { signature: '', url: '' }, // compatible with older formats
'darwin-aarch64': { signature: '', url: '' },
'darwin-x86_64': { signature: '', url: '' },
'linux-x86_64': { signature: '', url: '' },
'windows-x86_64': { signature: '', url: '' },
// 'windows-i686': { signature: '', url: '' }, // no supported
},
};

const setAsset = async (asset, reg, platforms) => {
let sig = '';
if (/.sig$/.test(asset.name)) {
sig = await getSignature(asset.browser_download_url);
}
platforms.forEach((platform) => {
if (reg.test(asset.name)) {
// 设置平台签名,检测应用更新需要验证签名
if (sig) {
updateData.platforms[platform].signature = sig;
return;
}
// 设置下载链接
updateData.platforms[platform].url = asset.browser_download_url;
}
});
};

const promises = latestRelease.assets.map(async (asset) => {
// windows
await setAsset(asset, /.msi.zip/, ['win64', 'windows-x86_64']);

// darwin
await setAsset(asset, /.app.tar.gz/, [
'darwin',
'darwin-x86_64',
'darwin-aarch64',
]);

// linux
await setAsset(asset, /.AppImage.tar.gz/, ['linux', 'linux-x86_64']);
});
await Promise.allSettled(promises);

if (!fs.existsSync('updater')) {
fs.mkdirSync('updater');
}

// 将数据写入文件
fs.writeFileSync(
'./updater/install.json',
JSON.stringify(updateData, null, 2)
);
console.log('Generate updater/install.json');
}

updater().catch(console.error);

// 获取签名内容
async function getSignature(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
});
return response.text();
} catch (_) {
return '';
}
}

复制

在根路径下创建 UPDATE_LOG.md
文件,通知用户更新注意事项,格式如下(使用版本号作为标题,具体请查看 scripts/updatelog.mjs
):

# Updater Log

## v0.1.7

- feat: xxx
- fix: xxx

## v0.1.6

test

复制

修改 package.json
,在 "scripts" 中加入 updater
release
命令:

  "scripts": {
"dev": "vite --port=4096",
"build": "rsw build && tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"rsw": "rsw",
"updater": "node scripts/updater.mjs", // ✅ 新增
"release": "node scripts/release.mjs" // ✅ 新增
},

复制

Step3

Action 配置请参考之前的文章【Tauri 入门篇 - 跨平台编译】,此处新增环境设置签名和静态资源推送。

设置 Secret

配置变量 Repo -> Settings -> Secrets -> Actions -> New repository secret

  • TAURI_PRIVATE_KEY - 私钥,value 为 ~/.tauri/omb.key.pub
    内容

    • Name: TAURI_PRIVATE_KEY

    • Value: ******

  • TAURI_KEY_PASSWORD - 密码,value 为生成签名时的密码

    • Name: TAURI_KEY_PASSWORD

    • Value: ******

设置 .github/workflows/release.yml
name: Release CI

on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
create-release:
runs-on: ubuntu-latest
outputs:
RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }}

steps:
- uses: actions/checkout@v2
- name: Query version number
id: get_version
shell: bash
run: |
echo "using version tag ${GITHUB_REF:10}"
echo ::set-output name=version::"${GITHUB_REF:10}"

- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: '${{ steps.get_version.outputs.VERSION }}'
release_name: 'OhMyBox ${{ steps.get_version.outputs.VERSION }}'
body: 'See the assets to download this version and install.'

build-tauri:
needs: create-release
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]

runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2

- name: Setup node
uses: actions/setup-node@v1
with:
node-version: 16

- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable

# Rust cache
- uses: Swatinem/rust-cache@v1

- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf

# Install wasm-pack
- uses: jetli/wasm-pack-action@v0.3.0
with:
# Optional version of wasm-pack to install(eg. 'v0.9.1', 'latest')
version: v0.9.1

- name: Install rsw
run: cargo install rsw

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"

- name: Yarn cache
uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install app dependencies and build it
run: yarn && yarn build
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
releaseId: ${{ needs.create-release.outputs.RELEASE_UPLOAD_ID }}

# 生成静态资源并将其推送到 github pages
updater:
runs-on: ubuntu-latest
needs: [create-release, build-tauri]

steps:
- uses: actions/checkout@v2
- run: yarn
- run: yarn updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Deploy install.json
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./updater
force_orphan: true

复制

发布应用

功能开发完成,提交代码后,只需执行 yarn release
命令就可以自动进行应用发布了。如果不想借助 github 打包和静态资源存放,也可以参考上面的步骤,自行部署。

# 发布主版本,v1.x.x -> v2.x.x
yarn release major

# 发布次版本,v1.0.x -> v1.1.x
yarn release minor

# 发布补丁版本,patch 参数可省略,v1.0.0 -> v1.0.1
yarn release [patch]

复制

注意:每次执行 yarn release
发布版本,主版本
次版本
补丁版本
都是自增 1。

常见问题

Error A public key has been found, but no private key

如果在 tauri.conf.json
中配置了 pubkey
,但未设置环境变量会出现以下错误:

tauri build
# ...
Compiling omb v0.1.0 (/Users/lencx/github/lencx/OhMyBox/src-tauri)
Finished release [optimized] target(s) in 21.27s
Bundling OhMyBox.app (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app)
Bundling OhMyBox_0.1.1_x64.dmg (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/dmg/OhMyBox_0.1.1_x64.dmg)
Running bundle_dmg.sh
Bundling /Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz)
Finished 3 bundles at:
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/dmg/OhMyBox_0.1.1_x64.dmg
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz (updater)

Error A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable.
error Command failed with exit code 1.

复制

解决方案:

  • Use environment variables in Terminal on Mac[7]

  • Set Environment Variable in Windows[8]

# macOS 设置环境变量:
export TAURI_PRIVATE_KEY="********" # omb.key
export TAURI_KEY_PASSWORD="********" # 生成公私钥时在终端输入的密码,如果未设置密码则无需设置此变量

# Windows 设置环境变量:
set TAURI_PRIVATE_KEY="********"
set TAURI_KEY_PASSWORD="********"

复制
# 如果签名打包成功会看到以下信息(以 macOS 为例)
Info 1 updater archive at:
Info /Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz.sig
✨ Done in 58.55s.

复制

版本信息错误

发布的应用版本以 tauri.conf.json
中的 package.version
为准,在发布新版本时注意更新 version

可能造成更新失败的原因

  • 使用 github pages 作为更新文件静态资源存储在国内会因网络限制导致更新失败,无法看到更新弹窗提示,或者下载不响应等问题,可以通过配置多个 endpoints
    地址来解决,安装包也可以放在自建服务器来提高下载的稳定性

  • 静态 json 文件中的平台签名(platforms[platform].signature
    )是否完整,签名内容可以在tauri build
    产生的 target/release/bundle/<platform>/*.sig
    文件中查看

References

[1]

动态接口: https://tauri.app/v1/guides/distribution/updater#server-support

[2]

静态文件: https://tauri.app/v1/guides/distribution/updater#update-file-json-format

[3]

gist: https://gist.github.com/

[4]

github-pages: https://pages.github.com/

[5]

【Tauri 入门篇 - 跨平台编译】: https://github.com/lencx/OhMyBox/discussions/8

[6]

.mjs: https://docs.fileformat.com/web/mjs/

[7]

Use environment variables in Terminal on Mac: https://support.apple.com/guide/terminal/use-environment-variables-apd382cc5fa-4f58-4449-b20a-41c53c006f8f/2.12/mac/11.0

[8]

Set Environment Variable in Windows: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1


文章转载自浮之静,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论