はじめに
最近、Tauri 2 を使ったデスクトップアプリの配布まわりを見ていて、
GitHub Actions で Windows / macOS / Linux 向けのインストーラや配布物をどう自動化するかがきれいだったので、メモとして整理しておきます。
デスクトップアプリは実装だけで終わりではなく、
最終的に各 OS 向けの成果物をどう作るかまで考えないと、リリース運用がすぐ重くなります。
特にマルチプラットフォーム対応では、次のあたりが気になりやすいです。
- 何をきっかけにリリースを始めるか
- Windows / macOS / Linux をどう分けてビルドするか
- OS ごとの依存をどこで解決するか
- 生成した成果物をどう公開するか
今回は、Tauri 2 + GitHub Actions でこの流れをどう組めるかを、workflow 中心に見ていきます。
先に前提だけ確認しておく
まず、Tauri 側の設定を少しだけ見ておくと、GitHub Actions 側の動きが理解しやすくなります。
{
"build": {
"frontendDist": "../dist",
"beforeBuildCommand": "npm run build"
},
"bundle": {
"active": true,
"targets": "all"
}
}
ここで押さえておきたいのは 2 点です。
- 配布ビルド前に
npm run buildを実行する - その OS で作成可能な bundle を対象にする
つまり CI 上で Tauri の配布ビルドが走るときは、ざっくり次の順番になります。
- フロントエンドをビルドする
- 生成した
distを Tauri が取り込む - Windows / macOS / Linux それぞれで成果物を作る
GitHub Actions は、この一連の流れを 3 プラットフォーム分まとめて回す役目です。
リリースの起点は Git tag にする
まず入口です。
on:
push:
tags:
- "v*"
ここはかなり分かりやすくて、v1.0.0 のようなタグを push したときだけ workflow が動きます。
この形のよいところは、
「どのタイミングを正式リリースとするか」を Git の操作にそのまま乗せられることです。
ブランチ push のたびに配布ビルドを走らせるより、
tag push を起点にしたほうが公開タイミングが明確になります。
3 プラットフォームのビルドは matrix で並列化する
次に、workflow の中心になる部分です。
jobs:
publish-tauri:
name: Publish ${{ matrix.label }}
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
include:
- label: Windows x64
platform: windows-latest
args: ""
- label: macOS universal
platform: macos-latest
args: "--target universal-apple-darwin --no-sign"
- label: Linux x64
platform: ubuntu-22.04
args: ""
ここでは、3 プラットフォーム分の配布ビルドを matrix で並列化しています。
- Windows は
windows-latest - macOS は
macos-latest - Linux は
ubuntu-22.04
こういう構成を見ると、
マルチプラットフォーム対応は特別な大きい仕組みではなく、
GitHub Actions の matrix に素直に乗せるだけでもかなり整理できる ことが分かります。
個別の job を 3 つコピペして管理するより、この形のほうが見通しがよいです。
macOS は universal build にしておくと扱いやすい
macOS だけ引数が付いています。
args: "--target universal-apple-darwin --no-sign"
ここでは Apple Silicon と Intel の両方に対応する universal build を作っています。
macOS 向け成果物を CPU 別に分ける構成もできますが、
配布する側から見ると universal のほうが説明が単純になります。
利用者から見ても、
- どちらの Mac でも基本的に同じ配布物を使える
- ダウンロードページが増えすぎない
というメリットがあります。
今回は --no-sign なので、署名なしの構成です。
まず GitHub Releases で配布するところまで作りたい、という段階なら、この形はかなり現実的だと思います。
Linux は system dependency を先に入れる
Linux だけは最初に依存を追加しています。
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
ここは地味ですが重要です。
Tauri の Linux ビルドでは WebKit 系ライブラリなどが必要になるので、
ローカルでは通っても CI ではここで詰まりやすいです。
こういう OS 固有の前提を workflow に書いておくと、
あとから見返したときにも「Linux だけなぜ準備が必要なのか」がすぐ分かります。
マルチプラットフォームの配布パイプラインでは、
この「依存を隠さずに書いておく」がかなり大事だと感じます。
Node と Rust の準備も job の中で完結させる
そのあとに、ビルドに必要なツールチェーンを入れます。
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm
cache-dependency-path: package-lock.json
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install macOS universal Rust targets
if: matrix.label == 'macOS universal'
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Install frontend dependencies
run: npm ci
ここでやっていることはシンプルです。
- フロントエンドのために Node.js を入れる
- Tauri のために Rust を入れる
- macOS universal build のときだけ Rust target を追加する
- フロントエンド依存は
npm ciで固定的に入れる
必要なものをすべて job の中で完結させているので、
runner の状態にあまり期待しない構成になっています。
CI は手元の環境と違って毎回きれいな状態から始まるので、
このくらい明示的なほうが安心です。
配布前にバージョンずれを止める
この workflow で特によいと思ったのが、配布前にバージョン整合性を見ているところです。
- name: Verify release version matches tag
shell: bash
run: |
node - <<'EOF'
const fs = require('fs');
const packageVersion = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;
const tauriVersion = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json', 'utf8')).version;
const cargo = fs.readFileSync('src-tauri/Cargo.toml', 'utf8');
const cargoMatch = cargo.match(/^version\s*=\s*"([^"]+)"/m);
const tagVersion = (process.env.GITHUB_REF_NAME || '').replace(/^v/, '');
if (!cargoMatch) {
console.error('Unable to read version from src-tauri/Cargo.toml.');
process.exit(1);
}
const cargoVersion = cargoMatch[1];
if (!tagVersion || packageVersion !== tagVersion || tauriVersion !== tagVersion || cargoVersion !== tagVersion) {
console.error(
`Tag and app versions must match. tag=${tagVersion} package=${packageVersion} tauri=${tauriVersion} cargo=${cargoVersion}`
);
process.exit(1);
}
EOF
ここで見ているのは次の 4 つです。
- Git tag
package.jsonsrc-tauri/tauri.conf.jsonsrc-tauri/Cargo.toml
Tauri ベースのアプリは、JavaScript 側と Rust 側でバージョン情報が分かれるので、
うっかりずれると「ビルドは通ったけど、公開物としては気持ち悪い」という状態が起こります。
なのでこのチェックはかなり実用的です。
単に配布物を作るだけではなく、ちゃんと公開してよい状態か を先に確認しています。
最後は tauri-action にまとめて任せる
最後に、実際のビルドと GitHub Releases へのアップロードです。
- name: Build app and upload release assets
uses: tauri-apps/tauri-action@action-v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: ${{ github.ref_name }}
releaseName: "Desktop App ${{ github.ref_name }}"
releaseBody: "See the assets to download this version and install."
generateReleaseNotes: true
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}
ここでやっていることはかなり明快です。
- 各 OS 向けの成果物をビルドする
- 生成した成果物を GitHub Releases にアップロードする
- リリース名や本文も workflow 側で管理する
運用としては、最終的に次の流れになります。
- バージョンをそろえる
- tag を push する
- GitHub Actions が Windows / macOS / Linux を並列ビルドする
- GitHub Releases に成果物が並ぶ
手元でビルドして zip や installer を集めて手動アップロードする運用より、
この形のほうがだいぶ安定します。
まとめ
今回見た構成では、マルチプラットフォームの desktop application 配布を次の流れで自動化していました。
- Git tag をリリースの起点にする
- matrix で Windows / macOS / Linux を並列ビルドする
- Linux 依存や macOS target を job 内で準備する
- 配布前にバージョン不整合を止める
-
tauri-actionで成果物生成と GitHub Releases への公開まで流す
マルチプラットフォームの installer / 配布物づくりは大変そうに見えますが、
GitHub Actions の matrix と tauri-action をうまく使うと、かなり見通しよく整理できます。
自分で desktop application の配布パイプラインを組むなら、
まずは tag push + matrix + tauri-action という形から始めるのが扱いやすそうだなと思いました。