這篇文章會(huì)有一點(diǎn)啰嗦,我希望想把解決問題的一些思路展現(xiàn)出來,給遇到問題無從下手的朋友帶來一些啟發(fā)。
簽名
Tauri 通過簽名來保證安全更新應(yīng)用。 簽名更新應(yīng)用需要做兩件事:
私鑰 (privkey) 用于簽署應(yīng)用的更新,必須嚴(yán)密保存。此外,如果丟失了此密鑰,將無法向當(dāng)前用戶群發(fā)布新的更新,將其保存在安全的地方至關(guān)重要。
在 tauri.conf.json 中添加公鑰 (pubkey),以在安裝前驗(yàn)證更新存檔。
生成簽名
使用 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.
注意:如果丟失了私鑰或密碼,將無法簽署更新包并且更新將無法正常工作(請(qǐng)妥善保管)。
tauri.conf.json 配置
{ "updater": { "active": true, "dialog": true, "endpoints": ["https://releases.myapp.com/{{target}}/{{current_version}}"], "pubkey": "YOUR_UPDATER_PUBKEY" }, }
active - 布爾值,是否啟用,默認(rèn)值為 false
dialog - 布爾值,是否啟用內(nèi)置新版本提示框,如果不啟用,則需要在 JS 中自行監(jiān)聽事件并進(jìn)行提醒
endpoints - 數(shù)組,通過地址列表來確定服務(wù)器端是否有可用更新,字符串 {{target}} 和 {{current_version}} 會(huì)在 URL 中自動(dòng)替換。如果指定了多個(gè)地址,服務(wù)器在預(yù)期時(shí)間內(nèi)未響應(yīng),更新程序?qū)⒁来螄L試。endpoints 支持兩種格式:
動(dòng)態(tài)接口[1] - 服務(wù)器根據(jù)客戶端的更新請(qǐng)求確定是否需要更新。 如果需要更新,服務(wù)器應(yīng)以狀態(tài)代碼 200 OK 進(jìn)行響應(yīng),并在正文中包含更新 JSON。 如果不需要更新,服務(wù)器必須響應(yīng)狀態(tài)代碼 204 No Content。
靜態(tài)文件[2] - 備用更新技術(shù)使用純 JSON 文件,將更新元數(shù)據(jù)存儲(chǔ)在 gist[3],github-pages[4] 或其他靜態(tài)文件存儲(chǔ)中。
pubkey - 簽名的公鑰
實(shí)現(xiàn)步驟
拆解問題
要實(shí)現(xiàn)自動(dòng)升級(jí)應(yīng)用主要分為以下幾個(gè)步驟:
生成簽名(公私鑰):
私鑰用于設(shè)置打包(tauri build)的環(huán)境變量
公鑰用于配置 tauri.conf.json -> updater.pubkey
向客戶端推送包含簽名及下載鏈接的更新請(qǐng)求,有兩種形式:
動(dòng)態(tài)接口返回 json 數(shù)據(jù)
靜態(tài)資源返回 json 文件
將 2 中的更新請(qǐng)求地址配置在 tauri.conf.json -> updater.endpoints
通過將 tauri.conf.json -> updater.dialog 配置為 true,啟用內(nèi)置通知更新應(yīng)用的彈窗。設(shè)置為 false 則需要自行通過 js 事件來處理(暫不推薦,喜歡折騰的朋友可以自行嘗試)
因?yàn)閼?yīng)用的跨平臺(tái)打包借助了 github action 的工作流來實(shí)現(xiàn),具體可以參考【Tauri 入門篇 - 跨平臺(tái)編譯】[5],所以更新也同樣使用 github action 來實(shí)現(xiàn),充分發(fā)揮 github 的能力(簡單來說,就是不需要借助其他第三方平臺(tái)或服務(wù)就可以實(shí)現(xiàn)整個(gè)應(yīng)用的自動(dòng)化發(fā)布更新)。
梳理流程
在本地生成公私鑰
加簽名構(gòu)建跨平臺(tái)應(yīng)用(通過 github action 設(shè)置簽名環(huán)境變量)
對(duì)構(gòu)建出的安裝包解析,生成靜態(tài)資源文件(通過腳本實(shí)現(xiàn)安裝包信息獲?。?/p>
推送更新請(qǐng)求采用靜態(tài)資源的方式(可以將 json 文件存儲(chǔ)在 github pages)
將 github pages 的資源地址配置到 tauri.conf.json -> updater.endpoints
代碼實(shí)現(xiàn)
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
在項(xiàng)目根路徑下創(chuàng)建 scripts 目錄,然后在 scripts 下依次創(chuàng)建 release.mjs,updatelog.mjs,updater.mjs 三個(gè) .mjs[6] 文件:
scripts/release.mjs - 版本發(fā)布,因發(fā)布需涉及多處改動(dòng)(如版本,版本日志,打 tag 標(biāo)簽等等),故將其寫成腳本,減少記憶成本
scripts/updatelog.mjs - 版本更新日志處理,供 scripts/updater.mjs 腳本使用
scripts/updater.mjs - 生成應(yīng)用更新需要的靜態(tài)文件
# 安裝開發(fā)依賴 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') { // 補(bǔ)丁版本 c += 1; } else { console.log(`Invalid flag "${flag}"`); process.exit(1); } const nextVersion = `${a}.$.${c}`; packageJson.version = nextVersion; const nextTag = `v${nextVersion}`; await updatelog(nextTag, 'release'); // 將新版本寫入 package.json 文件 fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2)); // 提交修改的文件,打 tag 標(biāo)簽(tag 標(biāo)簽是為了觸發(fā) github action 工作流)并推送到遠(yuǎn)程 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 的詳細(xì)信息 const { data: latestRelease } = await github.rest.repos.getReleaseByTag({ ...options, tag: tag.name, }); // 需要生成的靜態(tài) json 文件數(shù)據(jù),根據(jù)自己的需要進(jìn)行調(diào)整 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)) { // 設(shè)置平臺(tái)簽名,檢測應(yīng)用更新需要驗(yàn)證簽名 if (sig) { updateData.platforms[platform].signature = sig; return; } // 設(shè)置下載鏈接 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'); } // 將數(shù)據(jù)寫入文件 fs.writeFileSync( './updater/install.json', JSON.stringify(updateData, null, 2) ); console.log('Generate updater/install.json'); } updater().catch(console.error); // 獲取簽名內(nèi)容 async function getSignature(url) { try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/octet-stream' }, }); return response.text(); } catch (_) { return ''; } }
在根路徑下創(chuàng)建 UPDATE_LOG.md 文件,通知用戶更新注意事項(xiàng),格式如下(使用版本號(hào)作為標(biāo)題,具體請(qǐng)查看 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 配置請(qǐng)參考之前的文章【Tauri 入門篇 - 跨平臺(tái)編譯】,此處新增環(huán)境設(shè)置簽名和靜態(tài)資源推送。
設(shè)置 Secret
配置變量 Repo -> Settings -> Secrets -> Actions -> New repository secret:
TAURI_PRIVATE_KEY - 私鑰,value 為 ~/.tauri/omb.key.pub 內(nèi)容
Name: TAURI_PRIVATE_KEY
Value: ******
TAURI_KEY_PASSWORD - 密碼,value 為生成簽名時(shí)的密碼
Name: TAURI_KEY_PASSWORD
Value: ******
設(shè)置 .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 }} # 生成靜態(tài)資源并將其推送到 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
發(fā)布應(yīng)用
功能開發(fā)完成,提交代碼后,只需執(zhí)行 yarn release 命令就可以自動(dòng)進(jìn)行應(yīng)用發(fā)布了。如果不想借助 github 打包和靜態(tài)資源存放,也可以參考上面的步驟,自行部署。
# 發(fā)布主版本,v1.x.x -> v2.x.x yarn release --major # 發(fā)布次版本,v1.0.x -> v1.1.x yarn release --minor # 發(fā)布補(bǔ)丁版本,patch 參數(shù)可省略,v1.0.0 -> v1.0.1 yarn release [--patch]
注意:每次執(zhí)行 yarn release 發(fā)布版本,主版本,次版本,補(bǔ)丁版本 都是自增 1。
常見問題
Error A public key has been found, but no private key
如果在 tauri.conf.json 中配置了 pubkey,但未設(shè)置環(huán)境變量會(huì)出現(xiàn)以下錯(cuò)誤:
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 設(shè)置環(huán)境變量: export TAURI_PRIVATE_KEY="********" # omb.key export TAURI_KEY_PASSWORD="********" # 生成公私鑰時(shí)在終端輸入的密碼,如果未設(shè)置密碼則無需設(shè)置此變量 # Windows 設(shè)置環(huán)境變量: set TAURI_PRIVATE_KEY="********" set TAURI_KEY_PASSWORD="********"
# 如果簽名打包成功會(huì)看到以下信息(以 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.
版本信息錯(cuò)誤
發(fā)布的應(yīng)用版本以 tauri.conf.json 中的 package.version 為準(zhǔn),在發(fā)布新版本時(shí)注意更新 version。
可能造成更新失敗的原因
使用 github pages 作為更新文件靜態(tài)資源存儲(chǔ)在國內(nèi)會(huì)因網(wǎng)絡(luò)限制導(dǎo)致更新失敗,無法看到更新彈窗提示,或者下載不響應(yīng)等問題,可以通過配置多個(gè) endpoints 地址來解決,安裝包也可以放在自建服務(wù)器來提高下載的穩(wěn)定性
靜態(tài) json 文件中的平臺(tái)簽名(platforms[platform].signature)是否完整,簽名內(nèi)容可以在tauri build 產(chǎn)生的 target/release/bundle//*.sig 文件中查看
審核編輯:湯梓紅
-
密鑰
+關(guān)注
關(guān)注
1文章
139瀏覽量
19766 -
命令
+關(guān)注
關(guān)注
5文章
684瀏覽量
22037 -
代碼
+關(guān)注
關(guān)注
30文章
4790瀏覽量
68654
原文標(biāo)題:Tauri 應(yīng)用篇 - 自動(dòng)通知應(yīng)用升級(jí)
文章出處:【微信號(hào):Rust語言中文社區(qū),微信公眾號(hào):Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論