マルチコンフィグによるアプリバリアント
オーバーレイ設定ファイルを使用して、共有コードから複数の Tauri アプリバリアントをビルドする方法
マルチコンフィグによるアプリバリアント
単一の Tauri コードベースから、オーバーレイ設定ファイルを使用して複数のアプリバリアント(異なる名前、識別子、アイコン)を生成できる。ベースの tauri.conf.json に完全な設定を記述し、バリアント設定は異なるフィールドのみをオーバーライドする。
問題
「zudotext」というテキストエディタアプリがある。ここで「ztoffice」という2つ目のアプリを以下の条件で作成したいとする。
異なる名前とアイコン
異なる macOS バンドル識別子
同じ Rust コードとフロントエンド
プロジェクト全体をコピーすることもできるが、それはすべてを2重にメンテナンスすることを意味する。代わりに、設定のオーバーレイを使用する。
設定オーバーレイの仕組み
Tauri の --config フラグは、ベースの tauri.conf.json の上にマージされる追加の JSON ファイルを受け付ける。オーバーレイにはオーバーライドしたいフィールドのみを含めればよい。
# Build the base app
cargo tauri build
# Build the variant app
cargo tauri build --config tauri.conf.ztoffice.json実際の例
ベース設定:tauri.conf.json
すべてのフィールドを含む完全な設定。
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "zudotext",
"version": "0.1.0",
"identifier": "com.takazudo.zudotext",
"build": {
"beforeDevCommand": "pnpm exec vite --config vite.config.ts",
"beforeBuildCommand": "pnpm exec vite build --config vite.config.ts",
"devUrl": "http://localhost:37461",
"frontendDist": "./dist-renderer"
},
"app": {
"macOSPrivateApi": true,
"windows": [],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"category": "DeveloperTool",
"macOS": {
"minimumSystemVersion": "10.15"
}
}
}バリアント設定:tauri.conf.ztoffice.json
異なるフィールドのみを記述する。
{
"productName": "ztoffice",
"identifier": "com.takazudo.ztoffice"
}ファイルはこれだけである -- たった2つのフィールド。cargo tauri build --config tauri.conf.ztoffice.json を実行すると、Tauri は以下の処理を行う。
ベースの
tauri.conf.jsonを読み込むtauri.conf.ztoffice.jsonをディープマージするproductName: "ztoffice"とidentifier: "com.takazudo.ztoffice"でビルドその他すべて(ビルドコマンド、バンドル設定など)はベースから取得
出力は以下のようになる。
target/ release/ bundle/ macos/ ztoffice. appオーバーライド可能なフィールド
tauri.conf.json の任意のフィールドをバリアント設定でオーバーライドできる。一般的なオーバーライドは以下の通りである。
| フィールド | 用途 |
|---|---|
productName | アプリ名(タイトルバー、Dock に表示) |
identifier | macOS バンドル識別子(アプリごとに一意である必要がある) |
bundle.icon | アプリアイコン |
build.beforeDevCommand | バリアント用の異なる開発コマンド |
build.frontendDist | 異なるフロントエンドアセット |
バリアントごとのワークスペース解決
マルチコンフィグパターンは、Rust コードがアプリ名に基づいて動作を適応させる場合に特に強力になる。テキストエディタアプリのプロジェクトルート解決を考えてみよう。
// In production, derive app name from the .app bundle path
// /Applications/ztoffice.app/Contents/MacOS/zudotext
// ^^^^^^^^^^ this is the app_name
let app_name = std::env::current_exe()
.ok()
.and_then(|exe| {
exe.ancestors()
.find(|p| p.extension().map(|ext| ext == "app").unwrap_or(false))
.and_then(|app_dir| {
app_dir.file_stem()
.map(|s| s.to_string_lossy().to_string())
})
})
.unwrap_or_else(|| "default".to_string());これにより、以下のようになる。
zudotext.appはワークスペースとして~/を使用Documents/ zudo- text/ zudotext/ ztoffice.appはワークスペースとして~/を使用Documents/ zudo- text/ ztoffice/
各バリアントは独自の分離されたワークスペースを持ち、以下のように設定される。
~/ . config/ zudotext/
zudotext/
config. json # workspace for the base app
ztoffice/
config. json # workspace for the variantTip
設定ディレクトリはバイナリ名(zudotext)を使用し、サブディレクトリはアプリバンドル名を使用することに注意。これは、同じバイナリのすべてのバリアントが設定の名前空間を共有することを意味しており、意図的な設計である -- これらは同じアプリのバリアントだからである。
ビルドスクリプト
利便性のため、各バリアント用のビルドスクリプトを作成する。
#!/bin/bash
# scripts/build-zudotext.sh
set -e
cargo clean -p zudotext
cargo tauri build
killall zudotext 2>/dev/null || true
sleep 1
rm -rf /Applications/zudotext.app
cp -r target/release/bundle/macos/zudotext.app /Applications/
xattr -cr /Applications/zudotext.app
echo "Installed zudotext.app"#!/bin/bash
# scripts/build-ztoffice.sh
set -e
cargo clean -p zudotext
cargo tauri build --config tauri.conf.ztoffice.json
killall ztoffice 2>/dev/null || true
sleep 1
rm -rf /Applications/ztoffice.app
cp -r target/release/bundle/macos/ztoffice.app /Applications/
xattr -cr /Applications/ztoffice.app
echo "Installed ztoffice.app"Note
両方のスクリプトで cargo clean -p zudotext(プロダクト名ではなくクレート名)を使用していることに注意。クレート名はバリアント間で変更されない -- 変わるのはプロダクト名のみである。
バリアントの開発モード
開発時にも --config を使用できる。
cargo tauri dev --config tauri.conf.ztoffice.jsonこれにより、オーバーライドされたプロダクト名と識別子でバリアントが開発モードで実行される。
バリアント固有のフロントエンド設定
バリアントごとに異なるフロントエンドの動作が必要な場合、設定からフロントエンドに情報を渡すことができる。1つのアプローチは Vite の環境変数を使用することである。
# In tauri.conf.ztoffice.json
{
"productName": "ztoffice",
"identifier": "com.takazudo.ztoffice",
"build": {
"beforeBuildCommand": "VITE_APP_VARIANT=ztoffice pnpm exec vite build --config vite.config.ts"
}
}フロントエンド側では以下のように参照する。
const appVariant = import.meta.env.VITE_APP_VARIANT || 'zudotext';あるいは、Rust コードが IPC コマンドを通じてアプリ名を公開し、フロントエンドが起動時にクエリする方法もある。
制限事項
同じ Rust バイナリ -- すべてのバリアントは同じ Rust バイナリにコンパイルされる。設定だけではバリアント固有の Rust コードを持つことはできない(それにはフィーチャーフラグを使用する)
同じバンドルリソース --
bundle.iconやfrontendDistをオーバーライドしない限り、すべてのバリアントは同じリソースを共有する同じ Cargo.toml --
Cargo.tomlのクレート名は変更されず、変わるのは Tauri のプロダクト名のみである
実行時の落とし穴
上記の current_exe() によるアプリ名の導出は、インストール済みの .app バンドルでは問題なく動作するが、これを破綻させる実行時の状況が2つある。どちらも明示的に処理しなければ、あるバリアントが別のバリアントの状態を静かに破壊してしまう。
開発モードの落とし穴:.app 祖先が存在しない
開発ビルドは .app バンドルからではなく target/debug/ から実行される。current_exe() のパスには .app の祖先が存在しないため、導出は何も見つけられずフォールバックに流れる。そのフォールバックがたまたまインストール済みアプリの名前に解決されると、開発時の実行がインストール済みアプリのワークスペースレジストリを読み書きしてしまう -- 開発セッションが静かに本番の状態を変更することになる。
修正方法は、cfg!(debug_assertions) でショートサーキットし、導出のフォールバックに頼らず "default" をハードコードすることである。
pub fn resolve_app_config_dir() -> std::path::PathBuf {
let home = dirs::home_dir().expect("could not determine home directory");
let app_name = if cfg!(debug_assertions) {
// Dev mode: do not reuse the binary's .app ancestor because there
// often isn't one (dev builds run from target/debug/). Anchor the
// registry under "default/" so dev work does not touch any
// installed app's registry.
"default".to_string()
} else {
resolve_app_name()
};
let dir = home.join(".config/zudotext").join(&app_name);
std::fs::create_dir_all(&dir).ok();
dir
}Warning
cfg!(debug_assertions) の分岐がなければ、開発セッションとインストール済みアプリのレジストリとの間に立ちはだかるのは、フォールバックが何に解決されるか次第ということになる。開発時を "default/" 配下にアンカーすることで、分離が偶然ではなく明示的なものになる。
マネージャモードのゲーティング
同じバイナリがマネージャ(アプリごとのバリアントを一覧表示して開くランチャーアプリ)としても出荷される場合、マネージャビルドは通常のバリアントと同じ起動コードを実行する -- だがマネージャ自身のワークスペースは存在しない。完全な起動シーケンスを実行すると、起動するだけで default ワークスペースレジストリに書き込み、ファイルウォッチャーを起動し、ポート 3001 で REST アダプタをバインドしてしまう。さらに悪いことに、そのポートバインドは同じくポート 3001 を使おうとする並行する開発セッションと競合する。
マネージャモードはビルド時に埋め込まれた productName(実行時には package_info().name として表面化する)から検出し、書き込みアプリ側の副作用をスキップする。
pub const MANAGER_PRODUCT_NAME: &str = "zudotext";
pub fn is_manager_mode<R: tauri::Runtime, M: Manager<R>>(handle: &M) -> bool {
handle.package_info().name == MANAGER_PRODUCT_NAME
}let manager_mode = app_mode::is_manager_mode(app.handle());
// Workspace-registry write — skipped in manager mode so the manager
// does not clobber the default text-app workspace.
if !manager_mode {
register_workspace(&app_config_dir);
}
// REST adapter (axum on port 3001) — dev only, and never in the
// manager, where binding 3001 would race a real text-app dev session.
if cfg!(debug_assertions) && !manager_mode {
tauri::async_runtime::spawn(async move {
http_server::start(http_state, http_handle, 3001).await;
});
}
// File watchers — the manager has no workspace to watch.
if !manager_mode {
commands::watchers::start_messages_watcher(&state_arc, app.handle());
}この定数は実行時にフリップできないよう、フィーチャーフラグから読み込むのではなくハードコードされている。ベースの tauri.conf.json はマネージャをビルドし(productName: "zudotext")、バリアントごとの --config オーバーライドは productName を別の値に変更するため、定数と一致しないものはすべて書き込みアプリとして扱われる。
1つのフロントエンドから2つの Tauri クレート
--config オーバーレイは、名前、アイコン、識別子といった表面的なメタデータのみが異なるバリアントをカバーする。しかし、1つのフロントエンドから根本的に異なる2つの起動モードを出荷するアプリもあり、オーバーレイではそれを表現できない。この場合の正しい構造は、オーバーレイを持つ1つのクレートではなく、2つのクレートディレクトリである。
動機となるケースは、オフラインリーダー(事前ビルドされたサイトをバンドルし、開発サーバーなし)と設定可能な開発ラッパー(ローカルのローディングページを通じてライブの開発サーバーを読み込む)の両方を1つのフロントエンドから出荷するアプリである。これらは同じランタイムの2つの見た目ではない -- frontendDist と開発サーバーの動作が中核で異なる。
クレート1:src-tauri -- オフラインリーダー
事前ビルドされた dist/ を自己完結型アプリにバンドルする。バイナリ以外のバンドル出力はなく、ライブのフロントエンドもない。
{
"productName": "ZudoDoc",
"version": "0.1.0",
"identifier": "com.zudolab.zudo-doc",
"build": {
"frontendDist": "../dist"
},
"app": {
"windows": [],
"security": { "csp": null }
},
"bundle": {
"active": false,
"targets": "all",
"icon": [],
"category": "DeveloperTool",
"macOS": { "minimumSystemVersion": "10.15" }
}
}リーダーは frontendDist を .(ビルド済みのサイト)に向け、bundle.active: false を設定する -- これは配布用インストーラではなく、静的アセットを包む実行時ラッパーである。
クレート2:src-tauri-dev -- 設定可能な開発ラッパー
フロントエンドが開発サーバーを起動するローカルのローディングページである、出荷可能なアプリ。親ディレクトリから pnpm dev を実行し、グローバルな Tauri API をローディングページに公開する。
{
"productName": "zudo-doc dev",
"version": "0.1.0",
"identifier": "com.takazudo.zudo-doc-dev",
"build": {
"frontendDist": "./frontend",
"beforeDevCommand": "cd .. && pnpm dev",
"devUrl": "http://localhost:4321/"
},
"app": {
"windows": [],
"withGlobalTauri": true,
"security": { "csp": null }
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/icon.png"],
"category": "DeveloperTool",
"macOS": { "minimumSystemVersion": "10.15" }
}
}リーダーとの違いは見た目ではなく構造的なものである。
| フィールド | リーダー(src-tauri) | 開発ラッパー(src-tauri-dev) |
|---|---|---|
frontendDist | .(ビルド済みサイト) | .(ローカルのローディングページ) |
beforeDevCommand | なし | cd .. && pnpm dev |
bundle.active | false | true |
bundle.icon | [] | ["icons/ |
withGlobalTauri | 未設定 | true |
2つ目のクレートがオーバーレイに勝るとき
--config オーバーレイではなく2つ目のクレートディレクトリを選ぶのは、名前、アイコン、識別子だけでなく、frontendDist や開発サーバーの動作そのものが異なるときである。
Tip
判断基準はシンプルである。バリアントが productName、identifier、bundle.icon だけで異なるなら、--config オーバーレイを使う -- DRY を保ち、1つのクレートを共有できる。フロントエンドの読み込み方(静的バンドルかライブの開発サーバーか)や、開発コマンドを実行するかどうかが変わるなら、2つ目のクレートディレクトリにフォークする。異なる起動モードをオーバーレイで表現しようとすると、複数の設定間で frontendDist と beforeDevCommand を切り替え続けることになり、2つの明示的なクレートディレクトリよりも理解しづらくなる。