Contents
開発環境の構築とバージョン確認
Tauri v2 アプリを作成するには、フロントエンド (Node.js) とバックエンド (Rust) がそれぞれ正しくインストールされていることが前提です。このセクションでは、OS を問わず共通で実施できるインストール手順と、インストール後に必ず確認すべきバージョン情報をまとめます。バージョンが期待通りであるかを確かめておくことで、テンプレート生成やビルド時の予期せぬエラーを未然に防げます。
Node.js と Rustup のインストール手順
- Node.js
- 公式サイト https://nodejs.org から LTS 系 (現在は v20 系) をダウンロードし、インストーラの指示に従ってインストールします。
- npm は Node と同梱されていますが、最新化しておくとトラブルが減ります。
bash
node -v # 例: v20.12.0
npm -v # 例: 10.5.0
npm install -g npm@latest # 任意で最新版に更新
- Rustup
- macOS / Linux はターミナル、Windows は PowerShell(管理者権限推奨)で以下を実行します。
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env # Linux/macOS のみ必要
rustc --version # 例: rustc 1.77.0 (2024‑07‑02)
cargo --version # 例: cargo 1.77.0
rustup がインストールされると、cargo(Rust のビルドツール)も自動的に利用可能になります。
Tauri CLI のインストールとバージョン確認
Tauri v2 用の CLI は tauri-cli パッケージとして提供されています。以下のコマンドでグローバルにインストールし、正しくインストールされたことを確認します。
|
1 2 |
cargo install tauri-cli --locked --features=v2 |
インストールが完了したら、バージョン表示コマンドで tauri-cli が出力されるか確かめます(cargo tauri --version は誤りです)。
|
1 2 |
tauri-cli --version # 例: tauri-cli 2.1.0 |
参考情報
Tauri の公式ドキュメント (https://v2.tauri.app) が常に最新のインストール手順を掲載しています。外部サイトの内容は執筆時点で確認できていないため、公式リファレンスを優先してください。
Tauri v2 プロジェクトの作成とディレクトリ構造
実務でテンプレートをすぐに利用できることは開発速度に直結します。このセクションでは create-tauri-app コマンドによってプロジェクト雛形を生成し、生成されたディレクトリの役割と主要ファイルを解説します。各フォルダが何を担当しているかを把握すれば、後続の実装作業がスムーズに進みます。
create-tauri-app によるプロジェクト雛形生成
以下のコマンドで最新のテンプレートを取得し、好きな名前のディレクトリに展開します。対話式の質問に答えるだけで、React・Vue・Svelte など任意のフロントエンドフレームワークが選択可能です。
|
1 2 3 |
npm create tauri-app@latest my-encryptor cd my-encryptor |
ポイント
-my-encryptorは任意のプロジェクト名に置き換えてください。
- 初回実行時はテンプレートリストが表示されるので、ここでは「Vanilla TS(TypeScript)」を例として選択します。
ディレクトリ構造とそれぞれの役割
| ディレクトリ | 主な内容 | 実務で意識すべきポイント |
|---|---|---|
src-tauri/ |
Rust エントリ (main.rs)・設定ファイル tauri.conf.json |
バックエンドロジック、プラグイン、ビルドフラッグはここに配置。 |
src/ |
フロントエンドの TypeScript / React コンポーネント | UI ロジックと Tauri の invoke 呼び出しを実装する領域。 |
public/ |
静的 HTML、CSS、アイコン等 | WebView が最初に読み込むファイル群。index.html にバンドルされた JS をリンクします。 |
備考
tauri.conf.jsonの必須項目は公式ガイド(https://v2.tauri.app/reference/config/)を参照し、bundle.identifierとsecurity.cspは少なくとも設定しておく必要があります。
コマンド実装と非同期処理
Tauri のコマンドシステムはフロントエンドから Rust 関数へ安全に橋渡しする仕組みです。この節では、同期コマンド と 非同期コマンド の実装例を示し、JSON でのデータ受け渡し方法も合わせて解説します。重い処理は UI スレッドをブロックしないよう async fn と Tokio を活用することがベストプラクティスです。
同期コマンドの基本形
|
1 2 3 4 5 6 |
// src-tauri/src/main.rs #[tauri::command] fn greet(name: String) -> String { format!("Hello, {}! 👋", name) } |
フロントエンド(TypeScript)側からは invoke でシンプルに呼び出せます。
|
1 2 3 4 5 6 7 |
import { invoke } from '@tauri-apps/api'; async function sayHi() { const msg = await invoke<string>('greet', { name: 'Tauri' }); console.log(msg); // => "Hello, Tauri! 👋" } |
非同期コマンドの実装例
|
1 2 3 4 5 6 |
#[tauri::command] async fn heavy_task(seconds: u64) -> Result<String, String> { tokio::time::sleep(std::time::Duration::from_secs(seconds)).await; Ok(format!("Completed after {} sec", seconds)) } |
TypeScript 側は Promise として受け取ります。
|
1 2 3 4 5 |
async function runTask() { const res = await invoke<string>('heavy_task', { seconds: 5 }); console.log(res); // "Completed after 5 sec" } |
JSON データのシリアライズ/デシリアライズ
任意の serde::Serialize / Deserialize を実装した型を引数・戻り値に指定できます。以下は設定オブジェクトをやり取りする例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Config { theme: String, auto_save: bool, } #[tauri::command] fn get_config() -> Config { Config { theme: "dark".into(), auto_save: true } } |
フロントエンド側の型定義と呼び出しは次の通りです。
|
1 2 3 4 5 6 |
interface Config { theme: string; auto_save: boolean; } async function fetchConfig(): Promise<Config> { return await invoke<Config>('get_config'); } |
注意点
- エラーはResult<T, String>の形で返すと、invokeが自動的に例外として伝搬します。
- 非同期コマンドでは#[tauri::command] async fn …と書くだけで Tokio ランタイムが内部的に使用されます。
状態管理とデータベース連携
実務アプリでは、認証情報やユーザー設定など複数のコマンドから共有すべき状態があります。Tauri では tauri::State がシングルトンとして全ウィンドウに注入でき、内部に Arc<Mutex<T>> を保持することでスレッド安全な共有が実現します。本節では SQLite と rusqlite を組み合わせた例を示し、状態構造体にデータベース接続フィールドを追加した完全な実装をご紹介します。
共有状態の定義と manage の呼び出し
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
use std::sync::{Arc, Mutex}; /// アプリ全体で共有するデータ。必要に応じてフィールドを増やす。 #[derive(Default)] struct AppState { counter: usize, /// SQLite 接続はオプションで保持。初期化時に `init_db` が設定する。 db: Option<rusqlite::Connection>, } /// Arc と Mutex でスレッド安全にラップした型エイリアス type SharedState = Arc<Mutex<AppState>>; fn main() { // 初期状態を生成し、Tauri ビルダーへ注入 let state: SharedState = Arc::new(Mutex::new(AppState::default())); tauri::Builder::default() .manage(state.clone()) .invoke_handler(tauri::generate_handler![ increment_counter, init_db, add_note, get_notes ]) .run(tauri::generate_context!()) .expect("failed to run tauri application"); } |
カウンタ増加コマンド(状態共有のシンプル例)
|
1 2 3 4 5 6 7 |
#[tauri::command] fn increment_counter(state: tauri::State<'_, SharedState>) -> usize { let mut guard = state.lock().unwrap(); guard.counter += 1; guard.counter } |
SQLite 初期化と CRUD 実装
Cargo.toml に依存を追加します。
|
1 2 3 4 |
[dependencies] rusqlite = { version = "0.30", features = ["bundled"] } serde = { version = "1.0", features = ["derive"] } |
DB 初期化コマンド
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
use rusqlite::{Connection, params}; #[tauri::command] fn init_db(state: tauri::State<'_, SharedState>) -> Result<(), String> { // データベースファイルを作成/オープン let conn = Connection::open("app.db").map_err(|e| e.to_string())?; // テーブル作成(存在しなければ作る) conn.execute( "CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL )", [], ) .map_err(|e| e.to_string())?; // 生成した Connection を SharedState に保存 let mut guard = state.lock().unwrap(); guard.db = Some(conn); Ok(()) } |
ノート追加コマンド
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#[tauri::command] fn add_note(state: tauri::State<'_, SharedState>, text: String) -> Result<u64, String> { let conn = { let guard = state.lock().unwrap(); guard.db.as_ref().ok_or("DB not initialized")?.clone() }; conn.execute("INSERT INTO notes (text) VALUES (?1)", params![text]) .map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid() as u64) } |
ノート取得コマンド(例示的にベクタで返す)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#[derive(serde::Serialize)] struct Note { id: i64, text: String, } #[tauri::command] fn get_notes(state: tauri::State<'_, SharedState>) -> Result<Vec<Note>, String> { let conn = { let guard = state.lock().unwrap(); guard.db.as_ref().ok_or("DB not initialized")?.clone() }; let mut stmt = conn.prepare("SELECT id, text FROM notes").map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(Note { id: row.get(0)?, text: row.get(1)? }) }) .map_err(|e| e.to_string())?; let mut notes = Vec::new(); for note in rows { notes.push(note.map_err(|e| e.to_string())?); } Ok(notes) } |
ポイント
-Arc<Mutex<AppState>>を使うことで、複数ウィンドウや非同期コマンドから同時に状態へアクセスできます。
- SQLite の接続はスレッドセーフではないため、ここではConnectionをクローンして使用しています(rusqlite::Connectionは内部でArc<Mutex<_>>を保持)。実際のプロダクトではプールライブラリを導入することも検討してください。
ファイル暗号化ツール実装とクロスプラットフォームビルド
本稿の中心サンプルは「AES‑GCM によるファイル暗号化/復号」です。Rust 側で非同期 I/O と暗号化ロジックを実装し、フロントエンドからはファイル選択 UI と invoke 呼び出しだけで完結します。最後に Windows・macOS・Linux 向けのビルド手順と、各プラットフォーム固有の注意点をまとめます。
依存クレートの追加
|
1 2 3 4 5 6 |
# Cargo.toml の [dependencies] 部分 aes-gcm = "0.10" # AEAD 暗号化(AES‑256‑GCM) hex = "0.4" rand = "0.8" tokio = { version = "1", features = ["fs"] } # 非同期ファイル I/O |
非同期暗号化コマンド
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
use aes_gcm::{Aes256Gcm, Key, Nonce}; use aes_gcm::aead::{Aead, NewAead}; use rand::RngCore; use std::path::PathBuf; #[tauri::command] pub async fn encrypt_file(path: String, key_hex: String) -> Result<(), String> { let path = PathBuf::from(&path); // ファイル全体を非同期で読み込む let mut data = tokio::fs::read(&path).await.map_err(|e| e.to_string())?; // 16進文字列から 32 バイトキーへ変換 let key_bytes = hex::decode(key_hex).map_err(|e| e.to_string())?; if key_bytes.len() != 32 { return Err("Key must be 32 bytes (64 hex chars)".into()); } let cipher = Aes256Gcm::new(Key::from_slice(&key_bytes)); // ランダム nonce(12 バイト)を生成 let mut nonce = [0u8; 12]; rand::thread_rng().fill_bytes(&mut nonce); // 暗号化。nonce は平文に含めて保存する。 let ciphertext = cipher .encrypt(Nonce::from_slice(&nonce), data.as_ref()) .map_err(|e| e.to_string())?; // 出力形式: [nonce][ciphertext] let mut out = Vec::with_capacity(nonce.len() + ciphertext.len()); out.extend_from_slice(&nonce); out.extend_from_slice(&ciphertext); // ".enc" 拡張子で保存 tokio::fs::write(path.with_extension("enc"), out) .await .map_err(|e| e.to_string()) } |
非同期復号コマンド
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#[tauri::command] pub async fn decrypt_file(enc_path: String, key_hex: String) -> Result<(), String> { let path = PathBuf::from(&enc_path); let data = tokio::fs::read(&path).await.map_err(|e| e.to_string())?; if data.len() < 12 { return Err("File too short to contain nonce".into()); } // 先頭 12 バイトが nonce、残りが暗号文 let (nonce_bytes, ciphertext) = data.split_at(12); let key_bytes = hex::decode(key_hex).map_err(|e| e.to_string())?; if key_bytes.len() != 32 { return Err("Key must be 32 bytes".into()); } let cipher = Aes256Gcm::new(Key::from_slice(&key_bytes)); let plaintext = cipher .decrypt(Nonce::from_slice(nonce_bytes), ciphertext) .map_err(|e| e.to_string())?; // 復号結果は ".dec" 拡張子で保存 tokio::fs::write(path.with_extension("dec"), plaintext) .await .map_err(|e| e.to_string()) } |
フロントエンドからの呼び出し例(React + TypeScript)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { invoke } from '@tauri-apps/api'; import { open } from '@tauri-apps/api/dialog'; async function selectAndEncrypt() { const file = await open({ multiple: false }); if (!file) return; // 32 バイトキーを 16 進文字列で用意(例: UI から取得) const keyHex = '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff'; await invoke('encrypt_file', { path: file, key_hex: keyHex }); alert('暗号化が完了しました'); } |
クロスプラットフォームビルド手順
- ターゲットの追加(各 OS 用に Rust のコンパイル対象をインストール)
bash
# Windows (x86_64)
rustup target add x86_64-pc-windows-msvc
# macOS Intel / Apple Silicon
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
# Linux (glibc)
rustup target add x86_64-unknown-linux-gnu
- ビルドコマンド(例:macOS Apple Silicon 用)
bash
cargo tauri build --target aarch64-apple-darwin
他のプラットフォームは --target に対応するトリップレットを指定すれば同様にビルドできます。
-
コードサイニング(配布時に推奨)
-
Windows:
signtool.exeで Authenticode 署名。自己署名証明書でもテストは可能です。 - macOS:Apple Developer ID の
codesign、続いて Notarization(Apple にアップロード)。 -
Linux:AppImage を作成する場合は GPG で署名し、配布ページに公開鍵を添付します。
-
インストーラ生成
Tauri が自動で以下の形式を生成します。tauri.conf.json > bundle に設定した情報がそのまま反映されます。
- Windows → NSIS (
*.exe) - macOS → DMG (
*.dmg) - Linux → AppImage / DEB / RPM
ビルド後の成果物は src-tauri/target/release/bundle/* に格納されています。
公式情報
Tauri のビルド・サイニングに関する最新ガイドは公式サイト https://v2.tauri.app/guides/building/ を参照してください。外部ブログの内容は執筆時点で確認できていないため、公式リファレンスを優先しています。
まとめ
- 開発環境:Node ≥ 18、Rustup、
cargo install tauri-cli --features=v2を行い、tauri-cli --versionが期待通り表示されることを確認します。 - プロジェクト生成:
npm create tauri-app@latest <project>でテンプレートを作成し、src-tauriとフロントエンドディレクトリの役割を把握しておきましょう。 - コマンド実装:
#[tauri::command]による同期・非同期関数を用意し、JSON でデータ受け渡しできることを確認します。重い処理はasync fnと Tokio の非同期 I/O を活用して UI フリーズを防ぎます。 - 状態管理 & DB:
tauri::State<'_, SharedState>にArc<Mutex<AppState>>を注入し、rusqliteで SQLite 接続を保持・CRUD できる実装例を示しました。エラーハンドリングはResult<T, String>でフロントへ伝搬させます。 - ファイル暗号化ツール:AES‑GCM (
aes-gcmクレート) と非同期 I/O により、大容量ファイルでもスムーズに暗号化/復号が可能です。フロントエンドはシンプルなファイル選択 UI とinvokeのみで完結します。 - クロスプラットフォームビルド:各 OS 用ターゲットを
rustup target addし、cargo tauri build --target …でバイナリ・インストーラを生成。コードサイニングとパッケージングのベストプラクティスに従うことで、エンドユーザーに信頼性の高い配布物が提供できます。
以上の手順とサンプルコードをそのままプロジェクトに組み込めば、Tauri v2 と Rust を活用した実務レベルのデスクトップアプリがすぐに構築可能です。ぜひリポジトリをクローンし、ローカルで動かしながら独自機能や UI のカスタマイズに挑戦してみてください。