這篇文章會有一點啰嗦,我希望想把解決問題的一些思路展現出來,給遇到問題無從下手的朋友帶來一些啟發。
簽名
Tauri 通過簽名來保證安全更新應用。 簽名更新應用需要做兩件事:
私鑰 (privkey) 用于簽署應用的更新,必須嚴密保存。此外,如果丟失了此密鑰,將無法向當前用戶群發布新的更新,將其保存在安全的地方至關重要。
在 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 - 簽名的公鑰
實現步驟
拆解問題
要實現自動升級應用主要分為以下幾個步驟:
生成簽名(公私鑰):
私鑰用于設置打包(tauri build)的環境變量
公鑰用于配置 tauri.conf.json -> updater.pubkey
向客戶端推送包含簽名及下載鏈接的更新請求,有兩種形式:
動態接口返回 json 數據
靜態資源返回 json 文件
將 2 中的更新請求地址配置在 tauri.conf.json -> updater.endpoints
通過將 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('
');
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('
').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//*.sig 文件中查看
審核編輯:湯梓紅
-
密鑰
+關注
關注
1文章
148瀏覽量
20920 -
命令
+關注
關注
5文章
755瀏覽量
23756 -
代碼
+關注
關注
30文章
4968瀏覽量
73965
原文標題:Tauri 應用篇 - 自動通知應用升級
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
非對稱密鑰生成和轉換規格詳解
Gemini CLI 中轉站配置使用教程
【MiCOKit試用體驗】+ cli命令行簡單分析
【MiCOKit試用體驗】+ 如何擴展MICO中的cli命令
如何安裝和升級了AWS CLI
基于灰色半解生成的密鑰分存方案
賽靈思研發提供能生成獨特的器件“指紋碼”的密鑰加密密鑰
芯片AES加密密鑰生成工具
使用Tauri CLI提供的命令生成密鑰
評論