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

D Plus第三十一期AMA活动回顾—Papyrs如何打造数据权下放的Web3博客

文章来自于/D Plus 投稿、转载请联系/D Plus小助手 Papyrs是一个建立在IC上的Web3博客平台,其通过互联网计算机区块链的底层特性把数据权完全交还给用户,并消除传统Web2博客平台带来的摩擦点,让更多的创作者都能够在一个公平、透明且隐私的创作…

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

签名

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

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

    • 动态接口返回 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.mjsupdatelog.mjsupdater.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” 中加入 updaterrelease 命令:

      "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/[email protected]
    - 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/[email protected]
    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/[email protected]

    - name: Setup node
    uses: actions/[email protected]
    with:
    node-version: 16

    - name: Install Rust stable
    uses: actions-rs/[email protected]
    with:
    toolchain: stable

    # Rust cache
    - uses: Swatinem/[email protected]

    - 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/[email protected]
    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/[email protected]
    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/[email protected]
    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/[email protected]
    - run: yarn
    - run: yarn updater
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Deploy install.json
    uses: peaceiris/[email protected]
    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。

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

    常见问题

    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

    Filecoin NV17网络升级全面解读:初步实现FVM的可编程性!

    原力区原作 Filecoin NV17网络版本升级时间线暂未完全确定,当前公开时间线为: 测试网: 2022-10-20 主网: 2022-11-09因此次升级与Fil-Lisbon议程略有冲突,所以官方团队大概率会在当前对外公布的时间线推迟一周后正式进行本次…

    Click to rate this post!
    [Total: 0 Average: 0]

    人已赞赏
    小白百科每日优选行情分析

    DAOrayaki |代币发行的三个最新趋势

    2022-10-27 17:52:37

    名家说小白百科每日优选

    Tauri 进阶篇 - 自动更新源码阅读

    2022-10-27 18:04:41

    0 条回复 A文章作者 M管理员
      暂无讨论,说说你的看法吧
    个人中心
    购物车
    优惠劵
    今日签到
    有新私信 私信列表
    有新消息 消息中心
    搜索