Tauri 2.x デスクトップの落とし穴 6 ヶ月分:静的 CRT / FFmpeg サイドカー / Turbopack symlink
Tauri 2.x デスクトップの落とし穴 6 ヶ月分:静的 CRT / FFmpeg サイドカー / Turbopack symlink
結論から: Tauri 2.x は Electron に比べてバイナリサイズ・パフォーマンス・セキュリティで明確に優位ですが、「クロスプラットフォーム」という看板の裏には公式ドキュメントが書いていない罠が多数あります。Gemini Desktop で 6 ヶ月出荷してきた経験から、もっとも再発する・診断に時間がかかる 7 件を、単発の fix ではなく 診断プロトコル としてまとめました。
一行で: Tauri 2.x の罠の 80% は Tauri そのものではなく、Rust エコシステム(sherpa-onnx / portable-pty / scap)と JS エコシステム(Turbopack / tauri-plugin-store)の継ぎ目に潜んでいます。
1. Windows MSVC CRT 静的リンク(+crt-static)
最大の落とし穴。Windows で sherpa-onnx(ローカル音声認識)を統合しました。その C++ 依存は /MT(静的 CRT)、一方で openssl-sys や ring などの Rust crate はデフォルト /MD(動的 CRT)。リンク段階で LNK2038: 'RuntimeLibrary' mismatch エラーが大量発生。
最初は「1 対 1 で修正」を試みました — mismatch するたびに該当 crate を /MT に強制。失敗です。理由:
- cc-rs ビルドの C++ crate はそれぞれ独自のコンパイラフラグを持つ
- 各 -sys crate(openssl-sys、ring、libsqlite3-sys)の build.rs は独立記述
- 一つ直しても次の依存更新で
/MDに戻る
正解:グローバル +crt-static。プロジェクトルートの .cargo/config.toml に追加:
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
Rust コード全体を静的 CRT にするため sherpa-onnx のアンカー制約が満たされ、他の crate も自動的に揃います。代償はバイナリ +2〜4 MB のみ、許容範囲。
2. CRT mismatch 診断プロトコル
LNK2038 'RuntimeLibrary' mismatch を見たらコードに飛び込まずプロトコル通りに:
- cc-rs 依存 crate を列挙
cargo tree -e build | grep cc - -sys crate を列挙
cargo tree | grep -E "\-sys" - 「動かせない制約」を特定 —
/MTまたは/MDを固定要求する crate(通常は ML / オーディオモデル、低レベル C++ SDK) - グローバルアンカー選択:
/MT制約があれば+crt-static、無ければデフォルト/MD - 検証:
cargo build --release --target x86_64-pc-windows-msvc緑、dumpbin /dependentsで VCRUNTIME140.dll 外部依存が無いことを確認
プロジェクトで 3 回(sherpa-onnx、libsodium、ffmpeg-next)適用、毎回 20 分以内で解決しました。
3. FFmpeg sidecar vs PATH
Tauri 2.x 公式は FFmpeg をサイドカーとして同梱することを推奨。方向は正しいが細部に罠。
最初のヘルパー:
fn get_ffmpeg_path() -> PathBuf {
// WRONG: dev モードでプレースホルダを返す
app_handle.path_resolver().resolve_resource("ffmpeg").unwrap()
}
問題:resolve_resource は tauri dev モードで プレースホルダパス を返し最終 sidecar パスではない。Command::new(get_ffmpeg_path()).spawn() だと dev ではシステム PATH の ffmpeg を拾って成功するが、release では見つからない。
正解:すべての ffmpeg 呼び出しを tauri::Command::new_sidecar("ffmpeg") に統一、Tauri ランタイムに dev vs release の解決を任せる:
use tauri::api::process::Command;
pub async fn run_ffmpeg_sidecar(args: Vec<String>) -> Result<String> {
let (mut rx, _child) = Command::new_sidecar("ffmpeg")?
.args(args)
.spawn()?;
// ... collect output
}
run_ffmpeg_sidecar として唯一のエントリポイントにし、散在していた 7 箇所の直接呼び出しを置換しました。
4. Turbopack symlink 制限
Tauri フロントは Next.js + Turbopack(monorepo の workspace package)。Turbopack はプロジェクトルート外への symlink を許容しません。
具体ケース:複数 worktree で node_modules を共有するため ln -s /main/repo/node_modules ./node_modules を試行 → Next 起動時エラー:
Error: Resolved path is outside of project root
設定での回避不可。最終方針:各 worktree で bun install --frozen-lockfile を必ず実行、symlink のショートカット禁止。worktree あたり 2〜3 GB 増加するが奇妙なエラーを避けられます。
Tauri 側で対処不可ですが monorepo + Tauri + Next.js の三重奏では安定して再現します。
5. portable-pty は 0.8 にピン — 0.9 リグレッション
Windows で portable-pty を使って shell を起動。0.9 にアップグレードしたら Windows コンソール出力(バックスペース、カラー ESC)でクラッシュ・ハング。
タイムライン:
- portable-pty 0.8.x:安定
- portable-pty 0.9.0:新 Windows ConPTY バックエンド、複雑な ANSI エスケープでデッドロック
- portable-pty 0.9.1/0.9.2:リグレッション未修正
対応:Cargo.toml で 0.8.1 にピン、コメント付き:
# Pinned to 0.8.1 due to Windows ConPTY deadlock in 0.9.x
# See: https://github.com/wez/wezterm/issues/XXXX
portable-pty = "=0.8.1"
教訓:Windows PTY が絡む依存のアップグレードは Windows で手動回帰必須。Linux CI 緑でも Windows は別物です。
6. tauri-plugin-store の Rust 側読み書き戦略
tauri-plugin-store の Rust API は「JS 側が store、Rust はプラグイン経由」という前提。しかし背景スレッド(同期、スケジュール)から頻繁に settings を読み書きするため、AppHandle + 非同期パスは重すぎる。
迂回策:Rust 側は .settings.json を直接読み書き、プラグインを経由しない:
use serde_json::{Value, from_reader, to_writer_pretty};
pub fn read_settings(path: &Path) -> Result<Value> {
let file = File::open(path)?;
Ok(from_reader(BufReader::new(file))?)
}
pub fn write_settings(path: &Path, v: &Value) -> Result<()> {
let tmp = path.with_extension("json.tmp");
to_writer_pretty(BufWriter::new(File::create(&tmp)?), v)?;
fs::rename(&tmp, path)?; // atomic swap
Ok(())
}
JS は引き続き tauri-plugin-store 経由、両者同じファイルを読む。書き込みは atomic rename(tmp → rename → target)で中途クラッシュによる破損を回避。「両者が同一抽象を共有しない」アーキコストを受け入れる代わりに Rust 側が大幅に簡潔になります。
7. scap vs CGDisplayCreateImage:macOS 画面キャプチャ選定
2026 年の macOS 画面キャプチャ選択肢:
| 選択肢 | 長所 | 短所 |
|---|---|---|
| CGDisplayCreateImage(レガシー) | シンプル、同期 API | macOS 15+ で deprecated、将来撤去 |
| ScreenCaptureKit(Apple 公式) | 推奨、将来安泰 | Swift 中心、Rust バインディング少 |
| scap(Rust crate) | クロスプラットフォーム、活発な保守 | Windows の安定性が怪しい |
判断:macOS は scap、Windows は当面画面キャプチャを実装しない。理由:
- scap の macOS 実装は ScreenCaptureKit ラッパーで最も公式に近い Rust 選択肢
- scap の Windows 実装は Windows.Graphics.Capture 依存、一部 GPU + マルチディスプレイで黒画面
- 画面キャプチャは P0 機能ではなく、Windows 専用 2 週間投入に値しない
Tauri 2.x のデスクトップを作るなら day 1 で「Windows + macOS 両方画面キャプチャ」をコミットしてはいけない。scap の Windows 問題はエコシステム問題で Tauri の問題ではありません。デスクトップ AI の製品決定は Gemini Mac App に欠けている 6 機能 にも詳述しています。
8. 新しい Tauri 2.x プロジェクトへの day-1 推奨 7 条
今もう一度 Tauri 2.x プロジェクトを始めるなら、day 1 で次の 7 つを即座に行います:
- +crt-static を強制:Windows target で
.cargo/config.tomlに追加、後の CRT 反復を省く - FFmpeg sidecar 唯一エントリを封装:すべて
run_ffmpeg_sidecarを通す、Command::new禁止 - 既知リグレッション crate のバージョンピン:portable-pty 0.8.1、tauri-plugin-store 安定版、理由コメント
- monorepo で node_modules の symlink 禁止:各 worktree で真面目にインストール
- PATH 依存の外部ツール禁止:ffmpeg、yt-dlp、ripgrep 全部サイドカー化
- Rust と JS で config を別々に読む:Rust は JSON 直接、JS は tauri-plugin-store
- 画面キャプチャは単一プラットフォームにコミット:macOS か Windows のいずれか、半端に両対応しない
最新情報は Tauri 公式サイト で確認を。ただし上記 7 条は 2026 年 4 月時点でも現場で通用します。
よくある質問
Q1: Electron と Tauri 2.x、どちらを選ぶ?
A: Electron は罠が少ないがバイナリ 150 MB+・メモリ重め。Tauri 2.x は罠が多いがバイナリ 10〜20 MB・ネイティブ性能。チームに Rust 経験があり製品がサイズ/性能を重視なら Tauri、JS/TS のみで機能速度重視なら Electron が安全です。
Q2: +crt-static でバイナリ肥大化する?
A: 実測 18 MB → 22 MB で +20%。デスクトップでは許容範囲、Electron の 150 MB ベースラインの前では無視可能です。
Q3: Tauri の isolation pattern で sidecar を代替できない?
A: Isolation は JS 層のセキュリティサンドボックスで、sidecar と直交します。Sidecar は「外部ツールを同梱」する問題を解決するもので、セキュリティ問題ではありません。両方併用可能です。
Q4: portable-pty 0.9 のリグレッションは issue 報告済み?
A: 複数あります。メンテナの人員が限られ ConPTY は複雑。短期は 0.8.1 ピン、長期は alacritty / wezterm 系の ConPTY 改善を追うのが現実的です。
まとめ
Tauri 2.x は非常に有望なデスクトップフレームワークですが、「クロスプラットフォーム」の約束を実現するためには各プラットフォーム固有の罠を自分でぶつかる必要があります。本記事は Tauri コアの批判ではありません — コアは安定しています。難所は Rust エコシステム(音声、PTY、画面キャプチャ)と JS エコシステム(Turbopack、store plugin)の継ぎ目です。各落とし穴をチーム内 runbook に落として、一度だけ払ったコストを再発させないことが重要です。