Tauriの基礎 - 自動アップデート

提供: MochiuWiki : SUSE, EC, PCB

概要

Tauriの自動アップデート機能 (updater) は、デスクトップアプリケーションにシームレスな更新体験を提供する公式プラグインである。

このプラグインを使用することで、ユーザはアプリケーションを手動で再インストールすることなく、最新バージョンへの更新が可能となる。

updaterプラグインの主な特徴は以下の通りである。

  • セキュリティ
    署名検証により、改ざんされていない正規の更新パッケージのみをインストールする。
  • クロスプラットフォーム
    Linux、Windows、MacOSの全プラットフォームで動作する。
  • 柔軟なサーバ構成
    静的ファイルサーバ、GitHub Releases、独自のAPIサーバ等に対応する。
  • 進捗表示
    ダウンロード進捗をリアルタイムで取得し、UIにフィードバックできる。
  • カスタマイズ可能なフロー
    更新チェックのタイミング、インストールの実行をアプリケーションの要件に合わせて制御できる。



updaterプラグインの導入

updaterプラグインを使用するには、Rustバックエンドとフロントエンドの両方にパッケージを追加して、設定ファイルを構成する必要がある。

インストール

updaterプラグインをインストールするには、以下に示す3つの手順を実行する。

Rust側のインストール

tauri add コマンドを実行してプラグインを追加する。

# Tauri CLIを使用してupdaterプラグインを追加
pnpm tauri add updater

# またはnpmを使用する場合
npm run tauri add updater


手動で追加する場合は、Cargo.tomlファイル ファイルに依存関係を記述する。

 # Cargo.toml
 [target."cfg(not(any(target_os = "android", target_os = "ios")))".dependencies]
 tauri-plugin-updater = "2.0.0"


フロントエンド側のインストール

JavaScript / TypeScriptバインディングをインストールする。

# pnpmを使用する場合
pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process

# npmを使用する場合
npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process


@tauri-apps/plugin-process は、更新後のアプリケーション再起動に必要である。

Rust側の登録

プラグインをTauriアプリケーションに登録する。

src-tauri/src/lib.rs (または、main.rs) ファイルに以下に示す処理を追加する。

 // src-tauri/src/lib.rs
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    tauri::Builder::default()
       .setup(|app| {
          // デスクトップ環境でのみupdaterプラグインを有効化
          #[cfg(desktop)]
          {
             app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
          }
          Ok(())
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


#[cfg(desktop)] 属性により、モバイルプラットフォームではupdaterが無効化される。

tauri.conf.jsonの設定

tauri.conf.json ファイルでupdaterの設定を行う。

 {
   "bundle": {
     "createUpdaterArtifacts": true
   },
   "plugins": {
     "updater": {
       "pubkey": "CONTENT_FROM_PUBLICKEY_PEM",
       "endpoints": [
         "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
       ]
     }
   }
 }


下表に、設定項目の詳細を示す。

tauri.conf.jsonファイルのupdater設定項目
設定項目 説明
createUpdaterArtifacts ビルド時に更新アーティファクト (署名付きパッケージ) を生成するかどうか
true に設定する。
pubkey 公開鍵の内容 (ファイルパスではなく、鍵の内容を直接記述)
署名検証に使用する。
endpoints 更新チェック用のエンドポイントURL配列
複数指定した場合は順に試行される。
windows.installMode Windowsでのインストールモード
"passive" (ユーザ操作なし) または "basicUi" (基本UI表示)



署名鍵の生成と管理

updaterプラグインは、更新パッケージの署名検証を行うことでセキュリティを確保する。

鍵ペアの生成

署名鍵ペアを生成するには、Tauri CLIの signer generate コマンドを実行する。

# 鍵ペアを生成 (対話的にパスワード入力を求められる)
pnpm tauri signer generate -w ~/.tauri/myapp.key

# または環境変数でパスワードを指定
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=my_password pnpm tauri signer generate -w ~/.tauri/myapp.key


このコマンドにより、以下に示す2つのファイルが生成される。

生成される鍵ファイル
ファイル 説明 取扱い
myapp.key 秘密鍵 厳重に管理する。
バージョン管理システムにコミットしない。
myapp.key.pub 公開鍵 tauri.conf.json ファイルの pubkey に設定する。


環境変数の設定

ビルド時に署名を行うために、環境変数を設定する必要がある。

# 秘密鍵の内容を環境変数に設定 (bash/zshの場合)
export TAURI_SIGNING_PRIVATE_KEY=$(cat ~/.tauri/myapp.key)

# パスワードも設定
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=my_password


CI/CD環境では、シークレットとして設定することを推奨する。

 # GitHub Actionsの例
 env:
   TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
   TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}


公開鍵の設定

生成された公開鍵の内容を tauri.conf.json ファイルに設定する。

# 公開鍵の内容を確認
cat ~/.tauri/myapp.key.pub


出力された内容を pubkey フィールドに貼り付ける。

 {
   "plugins": {
     "updater": {
       "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkK..."
     }
   }
 }


セキュリティ考慮事項

署名鍵の管理に関する重要な注意事項を以下に示す。

  • 秘密鍵は絶対に公開しない。
    秘密鍵が漏洩すると、攻撃者が偽の更新パッケージを作成できる。
    バージョン管理システム (.git ディレクトリ等) に含めない。
  • パスワードは強力なものを使用する。
    秘密鍵のパスワードは推測困難な文字列を設定する。
  • CI/CDではシークレット機能を使用する。
    GitHub Secrets、GitLab CI Variables等の暗号化されたシークレット機能を使用する。
  • 本番環境ではHTTPSを使用する。
    更新エンドポイントは必ずHTTPSで配信する。
    HTTPの場合は中間者攻撃のリスクがある。



アップデートサーバの設定

updaterは、更新情報を提供するサーバ (エンドポイント) と通信して更新を確認する。

静的JSONサーバ

最も簡単な構成は、静的JSONファイルをWebサーバで配信する方法である。

JSON形式

更新情報のJSON形式を以下に示す。

 {
   "version": "1.2.0",
   "notes": "バグ修正とパフォーマンス改善",
   "pub_date": "2025-01-15T10:30:00Z",
   "platforms": {
     "linux-x86_64": {
       "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
       "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_amd64.AppImage"
     },
     "windows-x86_64": {
       "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
       "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_x64-setup.exe"
     },
     "darwin-x86_64": {
       "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
       "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_x64.dmg"
     },
     "darwin-aarch64": {
       "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
       "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_aarch64.dmg"
     }
   }
 }


各フィールドの説明を以下に示す。

JSONフィールドの説明
フィールド 説明 必須
version 最新バージョン番号 Yes
notes リリースノート (Markdown形式) No
pub_date 公開日時 (ISO 8601形式) No
platforms プラットフォーム別の更新情報 Yes
platforms.<プラットフォーム>.signature パッケージの署名 Yes
platforms.<プラットフォーム>.url パッケージのダウンロードURL Yes


プラットフォーム識別子

platforms オブジェクトのキーには、以下に示す形式を使用する。

{target}-{arch}


プラットフォーム識別子
target arch 説明
linux x86_64, aarch64 Linux
windows x86_64, i686 Windows
darwin x86_64, aarch64 MacOS


動的エンドポイント

tauri.conf.json ファイルのエンドポイントURLには、動的変数を使用できる。

エンドポイント変数
変数 説明
{{current_version}} 現在のアプリバージョン 1.0.0
{{target}} OS名 linux, windows, darwin
{{arch}} アーキテクチャ x86_64, aarch64, armv7


これらの変数を使用することにより、サーバ側でプラットフォームを判定できる。

 {
   "endpoints": [
     "https://api.myapp.com/update?version={{current_version}}&platform={{target}}&arch={{arch}}"
   ]
 }


動的サーバのレスポンス形式

動的サーバの場合、更新が存在する場合は 200 OK でJSONを返し、更新がない場合は 204 No Content を返す。

更新がある場合のレスポンス例を以下に示す。

 {
   "version": "1.2.0",
   "pub_date": "2025-01-15T10:30:00Z",
   "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_x64-setup.exe",
   "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
   "notes": "## 新機能\n- 機能Aを追加\n- 機能Bを改善"
 }


GitHub Releasesの活用

GitHub Releasesを使用して更新パッケージを配信する場合の構成例を示す。

 {
   "plugins": {
     "updater": {
       "pubkey": "YOUR_PUBLIC_KEY",
       "endpoints": [
         "https://github.com/user/myapp/releases/latest/download/latest.json"
       ]
     }
   }
 }


GitHub Actionsでビルド時にJSONを生成する例を以下に示す。

 # .github/workflows/release.yml
 - name: Generate update JSON
   run: |
     cat > latest.json << EOF
     {
       "version": "${{ env.VERSION }}",
       "platforms": {
         "linux-x86_64": {
           "signature": "$(cat myapp_${{ env.VERSION }}_amd64.AppImage.sig)",
           "url": "https://github.com/user/myapp/releases/download/v${{ env.VERSION }}/myapp_${{ env.VERSION }}_amd64.AppImage"
         }
       }
     }
     EOF



React / TypeScriptでの実装

フロントエンドで更新チェックとインストールを行う定義を示す。

基本的な更新チェック

最も簡単な更新チェックの例を以下に示す。

 // src/hooks/useUpdateCheck.ts
 import { check } from '@tauri-apps/plugin-updater';
 import { relaunch } from '@tauri-apps/plugin-process';
 import { useState, useCallback } from 'react';
 
 // 更新情報の型定義
 interface UpdateInfo {
   version: string;
   currentVersion: string;
   date?: string;
   body?: string;
 }
 
 export function useUpdateCheck() {
   const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
   const [isChecking, setIsChecking] = useState(false);
   const [error, setError] = useState<string | null>(null);
 
   // 更新チェックを実行
   const checkForUpdate = useCallback(async () => {
     setIsChecking(true);
     setError(null);
 
     try {
       const update = await check();
 
       if (update) {
         // 更新が存在する場合
         setUpdateInfo({
           version: update.version,
           currentVersion: update.currentVersion,
           date: update.date,
           body: update.body,
         });
       }
       else {
         // 更新がない場合
         setUpdateInfo(null);
       }
     }
     catch (err) {
       // エラーハンドリング
       const message = err instanceof Error ? err.message : String(err);
       setError(message);
     }
     finally {
       setIsChecking(false);
     }
   }, []);
 
   return { updateInfo, isChecking, error, checkForUpdate };
 }


進捗表示付きダウンロードとインストール

ダウンロード進捗を表示しながら更新をインストールする例を以下に示す。

 // src/hooks/useUpdateInstall.ts
 import { check } from '@tauri-apps/plugin-updater';
 import { relaunch } from '@tauri-apps/plugin-process';
 import { useState, useCallback } from 'react';
 
 // ダウンロード進捗の型
 interface DownloadProgress {
   downloaded: number;
   total: number | null;
   percentage: number;
 }
 
 export function useUpdateInstall() {
   const [isInstalling, setIsInstalling] = useState(false);
   const [progress, setProgress] = useState<DownloadProgress | null>(null);
   const [error, setError] = useState<string | null>(null);
 
   // 更新をダウンロードしてインストール
   const downloadAndInstall = useCallback(async () => {
     setIsInstalling(true);
     setError(null);
     setProgress({ downloaded: 0, total: null, percentage: 0 });
 
     try {
       const update = await check();
 
       if (!update) {
         throw new Error('利用可能な更新がありません');
       }
 
       // ダウンロードとインストールを実行 (進捗コールバック付き)
       await update.downloadAndInstall((event) => {
         switch (event.event) {
           case 'Started':
             // ダウンロード開始
             setProgress({
               downloaded: 0,
               total: event.data.contentLength,
               percentage: 0,
             });
             break;
           case 'Progress':
             // ダウンロード進捗更新
             setProgress((prev) => {
               if (!prev) return null;
               const downloaded = prev.downloaded + event.data.chunkLength;
               const percentage = prev.total
                 ? Math.round((downloaded / prev.total) * 100)
                 : 0;
               return { ...prev, downloaded, percentage };
             });
             break;
           case 'Finished':
             // ダウンロード完了
             setProgress((prev) => prev ? { ...prev, percentage: 100 } : null);
             break;
         }
       });
 
       // アプリケーションを再起動
       await relaunch();
     }
     catch (err) {
       const message = err instanceof Error ? err.message : String(err);
       setError(message);
     }
     finally {
       setIsInstalling(false);
     }
   }, []);
 
   return { isInstalling, progress, error, downloadAndInstall };
 }


更新通知UIコンポーネント

更新通知を表示するReactコンポーネントの例を以下に示す。

 // src/components/UpdateNotification.tsx
 import { useState, useEffect } from 'react';
 import { check } from '@tauri-apps/plugin-updater';
 import { relaunch } from '@tauri-apps/plugin-process';
 
 interface UpdateNotificationProps {
   autoCheck?: boolean;        // 自動チェックを有効にするか
   checkInterval?: number;     // チェック間隔 (ミリ秒)
 }
 
 export function UpdateNotification({
   autoCheck = true,
   checkInterval = 3600000,    // デフォルト: 1時間
 }: UpdateNotificationProps) {
   const [update, setUpdate] = useState<Awaited<ReturnType<typeof check>>>();
   const [downloading, setDownloading] = useState(false);
   const [progress, setProgress] = useState(0);
   const [dismissed, setDismissed] = useState(false);
 
   // 更新チェック関数
   const checkUpdate = async () => {
     const result = await check();
     setUpdate(result);
   };
 
   // 初回チェックと定期チェック
   useEffect(() => {
     if (!autoCheck) return;
 
     // 初回チェック
     checkUpdate();
 
     // 定期チェック
     const interval = setInterval(checkUpdate, checkInterval);
     return () => clearInterval(interval);
   }, [autoCheck, checkInterval]);
 
   // 更新をインストール
   const handleInstall = async () => {
     if (!update) return;
 
     setDownloading(true);
     try {
       await update.downloadAndInstall((event) => {
         if (event.event === 'Progress') {
           // 進捗を計算 (簡易版)
           setProgress((prev) => Math.min(prev + 10, 90));
         }
       });
       setProgress(100);
       await relaunch();
     }
     catch (error) {
       console.error('更新のインストールに失敗しました:', error);
       setDownloading(false);
     }
   };
 
   // 更新がない、または、却下された場合は表示しない
   if (!update || dismissed) return null;
 
   return (
     <div className="update-notification">
       <div className="update-content">
         <h3>新しいバージョンが利用可能です</h3>
         <p>
           バージョン {update.version} がリリースされました
           (現在: {update.currentVersion})
         </p>
         {update.body && (
           <div className="release-notes">
             <pre>{update.body}</pre>
           </div>
         )}
       </div>
 
       <div className="update-actions">
         {downloading ? (
           <div className="progress-bar">
             <div
               className="progress-fill"
               style={{ width: `${progress}%` }}
             />
             <span>{progress}%</span>
           </div>
         ) : (
           <>
             <button
               className="btn-primary"
               onClick={handleInstall}
             >
               今すぐ更新
             </button>
             <button
               className="btn-secondary"
               onClick={() => setDismissed(true)}
             >
               後で
             </button>
           </>
         )}
       </div>
     </div>
   );
 }



アップデートフローのカスタマイズ

updaterプラグインは、アプリケーションの要件に合わせて更新フローをカスタマイズできる。

自動更新チェック

アプリケーション起動時に自動で更新をチェックする実装例を以下に示す。

 // src/App.tsx
 import { useEffect, useRef } from 'react';
 import { check } from '@tauri-apps/plugin-updater';
 
 export function App() {
   const hasChecked = useRef(false);
 
   useEffect(() => {
     // 既にチェック済みの場合はスキップ
     if (hasChecked.current) return;
     hasChecked.current = true;
 
     // 起動時に更新チェック (バックグラウンド)
     check().then((update) => {
       if (update) {
         console.log(`更新があります: v${update.version}`);
         // 通知状態を更新する等の処理
       }
     }).catch((error) => {
       // エラーは静かに処理 (ユーザ体験を損なわない)
       console.warn('更新チェックに失敗しました:', error);
     });
   }, []);
 
   return (
     <div>
       {/* アプリケーションコンテンツ */}
     </div>
   );
 }


強制更新と任意更新

セキュリティ修正等重要な更新の場合は、ユーザに更新を強制できる。

 // src/utils/updateManager.ts
 import { check } from '@tauri-apps/plugin-updater';
 import { relaunch } from '@tauri-apps/plugin-process';
 
 // 更新の重要度レベル
 type UpdateSeverity = 'optional' | 'recommended' | 'critical';
 
 interface UpdateResult {
   available: boolean;
   severity: UpdateSeverity;
   version?: string;
   install: () => Promise<void>;
 }
 
 // バージョン比較関数
 function parseVersion(version: string): number[] {
   return version.split('.').map(Number);
 }
 
 function compareVersions(v1: string, v2: string): number {
   const parts1 = parseVersion(v1);
   const parts2 = parseVersion(v2);
 
   for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
     const p1 = parts1[i] || 0;
     const p2 = parts2[i] || 0;
     if (p1 > p2) return 1;
     if (p1 < p2) return -1;
   }
   return 0;
 }
 
 // 重要度を判定 (サーバ側で設定するか、バージョン差分で判定)
 function determineSeverity(
   currentVersion: string,
   newVersion: string
 ): UpdateSeverity {
   const current = parseVersion(currentVersion);
   const latest = parseVersion(newVersion);
 
   // メジャーバージョンが異なる場合は重要
   if (latest[0] > current[0]) {
     return 'critical';
   }
 
   // マイナーバージョンが異なる場合は推奨
   if (latest[1] > current[1]) {
     return 'recommended';
   }
 
   // パッチレベルは任意
   return 'optional';
 }
 
 // 更新チェック関数
 export async function checkUpdateSeverity(): Promise<UpdateResult> {
   const update = await check();
 
   if (!update) {
     return {
       available: false,
       severity: 'optional',
       install: async () => {},
     };
   }
 
   const severity = determineSeverity(
     update.currentVersion,
     update.version
   );
 
   return {
     available: true,
     severity,
     version: update.version,
     install: async () => {
       await update.downloadAndInstall();
       await relaunch();
     },
   };
 }


Rust側での更新処理

Rustバックエンドで更新処理を行うことも可能である。

 // src-tauri/src/lib.rs
 use tauri_plugin_updater::UpdaterExt;
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    tauri::Builder::default()
       .setup(|app| {
          #[cfg(desktop)]
          {
             // updaterプラグインを登録
             app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
 
             // バックグラウンドで更新チェック
             let handle = app.handle().clone();
             tauri::async_runtime::spawn(async move {
                if let Err(e) = check_and_update(handle).await {
                   eprintln!("更新チェックエラー: {}", e);
                }
             });
          }
          Ok(())
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }
 
 #[cfg(desktop)]
 async fn check_and_update(app: tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
    // 更新をチェック
    if let Some(update) = app.updater()?.check().await? {
       println!("更新が見つかりました: {}", update.version);
 
       let mut downloaded = 0;
       let content_length = update.content_length.unwrap_or(0);
 
       // ダウンロードとインストール
       update
          .download_and_install(
             |chunk_length, _| {
                downloaded += chunk_length;
                println!("ダウンロード中: {} / {} bytes", downloaded, content_length);
             },
             || {
                println!("ダウンロード完了");
             },
          )
          .await?;
 
       println!("更新完了。再起動します。");
       app.restart();
    }
 
    Ok(())
 }



トラブルシューティング

updaterプラグイン使用時によく発生する問題と解決方法を以下に示す。

署名検証エラー

Signature verification failed エラーが発生する場合の確認事項を以下に示す。

  • 公開鍵が正しく設定されているか確認する。
    tauri.conf.json ファイルの pubkey フィールドが、ビルド時に使用した秘密鍵と対になっているか確認する。
  • 環境変数が正しく設定されているか確認する。
    TAURI_SIGNING_PRIVATE_KEY に秘密鍵の内容が設定されているか確認する。
  • JSONの署名が正しいか確認する。
    サーバのJSONに含まれる signature が、ビルド時に生成された署名と一致するか確認する。


エンドポイント接続エラー

Failed to fetch update エラーが発生する場合の確認事項を以下に示す。

  • URLが正しいか確認する。
    エンドポイントURLに誤字がないか、変数が正しく展開されているか確認する。
  • CORS設定を確認する。
    サーバがCORSヘッダーを返しているか確認する。
  • HTTPSを使用しているか確認する。
    本番環境ではHTTPSが必須である。
    開発環境でHTTPを使用する場合は、dangerousInsecureTransportProtocol を設定する。


 {
   "plugins": {
     "updater": {
       "dangerousInsecureTransportProtocol": true
     }
   }
 }


更新が検出されない

check()null を返す場合の確認事項を以下に示す。

  • バージョン番号を確認する。
    サーバのJSONの version が、現在のアプリバージョンより新しいか確認する。
  • プラットフォーム情報を確認する。
    JSONに現在のプラットフォーム (linux-x86_64等) のエントリが存在するか確認する。
  • サーバが 204 No Content を返していないか確認する。
    動的サーバの場合、更新がない時は 204 を返すのが正しい動作である。


デバッグ方法

updaterのデバッグには、以下に示す方法を活用する。

ログ出力の有効化

Rust側でログ出力を有効にする。

 // Cargo.tomlに追加
 [dependencies]
 log = "0.4"
 env_logger = "0.10"
 
 // main.rs/lib.rsの先頭に追加
 env_logger::init();


環境変数でログレベルを設定する。

# Linux / MacOS
RUST_LOG=debug pnpm tauri dev

# Windows (PowerShell)
$env:RUST_LOG="debug"; pnpm tauri dev


ネットワークリクエストの確認

Webブラウザの開発者ツール や curlコマンドでエンドポイントにアクセスして、レスポンスを確認する。

# エンドポイントのレスポンスを確認
curl -v https://releases.myapp.com/latest.json


手動での更新チェック

webブラウザのコンソールまたは開発ツールで手動実行する。

 // ブラウザコンソールで実行
 import('@tauri-apps/plugin-updater').then(({ check }) => {
   check().then((update) => console.log(update));
 });



参考リンク