🐧

Tauri 2でAppImageに利用クレートの.soファイルを含める方法

に公開

結構ハマったので、同じ問題に遭遇した人のために情報を残しておきます。
結論だけ知りたい方は まとめ に飛んでください。

概要

Tauri 2 で作成したアプリで、アプリが利用しているクレートが C/C++ 実装のネイティブライブラリをラップしています。そのクレートが利用するネイティブライブラリの動的リンクファイルがアプリの実行ファイルと同じく target/release に格納されます。

しかし Linux 用に AppImage を作成しようとすると、その動的リンクライブラリが見つからずにエラーになります。

具体的な状況

  • Tauri 2 でアプリを作成
  • sherpa-rs という音声系の AI を扱えるクレートを利用
  • sherpa-rs は sherpa-onnx という C++ 実装のライブラリの Rust ラッパー
  • sherpa-onnx は Microsoft の libonnxruntime を利用(C++ 実装)
  • リリースビルドすると、src-tauri/target/release に実行ファイルと一緒に libsherpa-onnx-c-api.so と libonnxruntime.so が出力される
  • cargo build で libsherpa-onnx-c-api.so とリンクできていない
  • AppImage にバンドルする際に libshera-onnx-c-api.so が見つからないというエラー

なおビルド時には詳細なエラーを確認するため、以下のように --verbose を付けています。

tauri build --verbose

npm などのパッケージマネージャで npm run build でビルドできるように script 設定している場合は、間に -- が余分に必要です(でないと npm のオプションとみなされる)。

npm run build -- --verbose

Cargo (rustc) に target/release 内の .so を認識させる

問題

なんかゴチャゴチャやっていたら、リンクエラーが出ずに実行ファイルはビルドできたりしたのですが・・・

$ ldd src-tauri/src/myapp | grep sherpa
libsherpa-onnx-c-api.so => not found

というように、実際にはリンクした .so ファイルの場所が設定されていませんでした。

一方で libsherpa-onnx-c-api.so 自体には、依存している libonnxruntime.so がちゃんと設定されています。

$ ldd src-tauri/src/libsherpa-onnx-c-api.so
        linux-vdso.so.1 (0x00007f694ea1c000)
        libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f694e9ce000)
        libonnxruntime.so => /home/username/Projects/myapp/src-tauri/target/release/libonnxruntime.so (0x00007f694d600000)
        libm.so.6 => /usr/lib/libm.so.6 (0x00007f694d4f2000)
        libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f694d200000)
        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f694e5d3000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f694ce00000)
        /usr/lib64/ld-linux-x86-64.so.2 (0x00007f694ea1e000)
        libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f694e9c7000)
        librt.so.1 => /usr/lib/librt.so.1 (0x00007f694e5ce000)

解決方法

以下の環境変数を設定しておきます。

RUSTFLAGS=-Clink-arg=-Wl,-rpath,$ORIGIN

export RUSTFLAGS=-Clink-arg=-Wl,-rpath,$ORIGIN しちゃうと他のプロジェクトにも影響が出るので、tauri build の前に RUSTFLAGS=-Clink-arg=-Wl,-rpath,$ORIGIN tauri build のように付ければオッケーです。

これで cargo build で libsherpa-onnx-c-api.so とリンクできていない が解消します。

$ ldd src-tauri/src/myapp | grep sherpa
libsherpa-onnx-c-api.so => /home/username/Projects/myapp/src-tauri/target/release/libsherpa-onnx-c-api.so (0x00007febf3e00000)

linuxdeploy に target/release 内の .so を認識させる

問題

Tauri は AppImage にバンドルする際に linuxdeploy というツールを使います。
こいつは実行ファイルのリンク情報を調べて、その .so ファイルを AppDir 以下にコピーしようとします。
しかし src-tauri/target/release の下は検索パスに含まれていないらしく、

ERROR: Could not find dependency: libsherpa-onnx-c-api.so

というエラーが発生します。

解決方法

環境変数 LD_LIBRARY_PATH を指定します。

LD_LIBRARY_PATH=/home/username/Projects/myapp/src-tauri/target/release

これは本来は Linux でファイルを実行する際に依存ライブラリを検索するパスを指定する環境変数です。
linuxdeploy は LD_LIBRARY_PATH に指定されたパスも検索するようです。

Ubuntu 22.04 環境を用意してビルド

問題

Tauri は Linux の場合は WebView として WebKitGTK を利用しますが、こいつが色々とバグが多くて厄介です。
自分の環境は Manjaro Linux で、Arch 系です。webkit2gtk 2.48.5 が入っていますが、2.48 は特に問題が多いようで、ターミナルから起動すると動くのに、ランチャーから起動すると画面真っ白になります。

(他にも 2.48 に限らず音声関係の問題にぶちあたってます。audio タグの再生を押してから再生されるまで時間がかかるとか、gstreamer のプラグインを色々入れると音声再生がしょっちゅうスキップするようになるとか・・・。)

また Tauri 公式の説明に以下の説明があります。

Core libraries such as glibc frequently break compatibility with older systems. For this reason, you must build your Tauri application using the oldest base system you intend to support. A relatively old system such as Ubuntu 18.04 is more suited than Ubuntu 22.04, as the binary compiled on Ubuntu 22.04 will have a higher requirement of the glibc version, so when running on an older system, you will face a runtime error like /usr/lib/libc.so.6: version 'GLIBC_2.33' not found. We recommend using a Docker container or GitHub Actions to build your Tauri application for Linux.
-- 引用: TAURI - AppImage - Limitations

glibc とかのコアライブラリが頻繁に破壊的変更をするので、サポートする一番古いベースシステムを使ってビルドすべき、と。
そのために Docker コンテナ使うとか、GitHub Actions 使うとかして Linux 向けの Tauri アプリをビルドすること推奨、と。

解決方法 (Docker)

Dockerfile を用意します。Ubuntu 18.04 は記事執筆時点でサポートが切れているので、22.04 を使います。最新 LTS は 24.04 ですので、1つ前の LTS になります。

apt-get install -y で入れているパッケージには余計なものも混ざってるかもしれません。
うろ覚えですが、xgd-utils とか file とかは linuxdeploy が必要とするようになって入れたような・・・。
libclang-dev は sherpa-rs が必要とするので入れました。

FROM ubuntu:jammy-20250730

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y \
    curl \
    build-essential \
    libwebkit2gtk-4.1-dev \
    libappindicator3-dev \
    librsvg2-dev \
    patchelf \
    ca-certificates \
    git \
    xdg-utils \
    file \
    libclang-dev

RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
    apt-get install -y nodejs
RUN npm install -g @tauri-apps/cli

ARG UID=1000
ARG GID=1000
ARG USER_NAME=dev
ARG GROUP_NAME=dev

RUN if [ "$UID" -ne 0 ] && [ "$GID" -ne 0 ]; then \
    groupadd -g ${GID} ${GROUP_NAME} && \
    useradd -m -u ${UID} -g ${GID} ${USER_NAME}; \
    fi

USER ${USER_NAME}

RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/home/${USER_NAME}/.cargo/bin:${PATH}"

WORKDIR /app

docker-compose.yml を用意します。

services:
  linux-build:
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        - UID=${UID:-1000}
        - GID=${GID:-1000}
        - USER_NAME=${USER_NAME:-dev}
        - GROUP_NAME=${GROUP_NAME:-dev}
    volumes:
      - .:/app
    environment:
      - UID=${UID}
      - GID=${GID}
      - LD_LIBRARY_PATH=/app/src-tauri/target/release
      - RUSTFLAGS=-Clink-arg=-Wl,-rpath,$$ORIGIN
    user: '${UID}:${GID}'
    command: ["sh", "-c", "npm install && npm run build"]

プロジェクトルートを /app にボリュームマウントしています。つまりコンテナ側でビルドした結果はホスト側のプロジェクトにも反映されます。

Windows や macOS で Docker Desktop 使っている人は、Docker 環境では root なのに、ホスト側のファイルシステムではホスト側のユーザーが作ったことにしてくれます。
一方 Linux で普通に docker を使うと、Docker 環境のユーザーの UID/GID で作ったことになります。ホスト側でファイルの編集や削除などが root でないとできなくなって厄介です。
なので .env ファイルで UID, GID を指定できるようにしています。

Docker Desktop を使っていない場合は、以下でホスト側の UID と GID をコンテナ側でも利用するように設定します。

echo "UID=$(id -u)" > .env
echo "GID=$(id -g)" >> .env

Docker Desktop を使っている場合は、.env に以下を設定します。

UID=0
GID=0
USER_NAME=root
GROUP_NAME=root

あとは起動すればオッケー。

$ docker-compose up

解決方法 (GitHub Actions)

.github/workflows/publish.yml を作成します。
基本は TAURI - GitHub - Example Workflow の通りですが、以下の変更を加えます。

  • デバッグのために ubuntu-22.04 の場合に --verbose 引数を設定
  • ubuntu-22.04 でのインストールパッケージに足らないものを追加
  • ubuntu-22.04 の場合に RUSTFLAGSLD_LIBRARY_PATH 環境変数を追加
name: 'publish'

on:
  workflow_dispatch:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: 'macos-latest' # for Arm based macs (M1 and above).
            args: '--target universal-apple-darwin'
          - platform: 'ubuntu-22.04'
            args: '--verbose'
          - platform: 'windows-latest'
            args: ''

    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v4

      - name: install dependencies (ubuntu only)
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libappindicator3-dev \
            librsvg2-dev \
            patchelf \
            xdg-utils \
            file \
            libclang-dev

      - name: setup node
        uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: 'npm'

      - name: install Rust stable
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: './src-tauri -> target'

      - name: install frontend dependencies
        run: npm install

      - name: Set Ubuntu-specific environment variables
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          echo "RUSTFLAGS=-Clink-arg=-Wl,-rpath,\$ORIGIN" >> $GITHUB_ENV
          echo "LD_LIBRARY_PATH=/home/runner/work/kotonoha-asr/kotonoha-asr/src-tauri/target/release" >> $GITHUB_ENV

      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tagName: v__VERSION__
          releaseName: 'v__VERSION__'
          releaseBody: 'See the assets to download this version and install.'
          releaseDraft: true
          prerelease: false
          args: ${{ matrix.args }}

最後の tauri-apps/tauri-action 実行時の envRUSTFLAGSLD_LIBRARY_PATH 環境変数を設定せずに、別途ステップを設けて $GITHUB_ENV に設定しているのは、Windows/macOS(特に macOS)への影響を避けるためです。

macOS の場合

macOS も *.dylib ファイルを含める必要があります。src-tauri/tauri.conf.jsonframeworks に設定します。

{
  // 略
  "bundle": {
    // 略
    "macOS": {
      "frameworks": [
        "target/release/libsherpa-onnx-c-api.dylib",
        "target/release/libonnxruntime.1.17.1.dylib"
      ]
    }
  }
}

これらのファイルは *.app 内の Contents/Frameworks に配置され、そこを動的リンク先に含めるように実行ファイルに設定されます。

Windows の場合

Windows も DLL ファイルを含める必要があります。これについては以下の方法で簡単にできました。

src-tauri/tauri.windows.conf.json を用意します。
Tauri はプラットフォームごとに設定ファイルを用意でき、デフォルトの src-tauri/tauri.conf.json との差分と上書きだけ設定すれば、ビルド時にマージして適用してくれます。

{
  "identifier": "com.k5-n.kotonoha.asr",
  "bundle": {
    "resources": {
      "target/release/sherpa-onnx-c-api.dll": "./",
      "target/release/onnxruntime.dll": "./"
    }
  }
}

identifier はなくても大丈夫なはずですが、書いておかないと IDE 上で「必須項目」だという警告が出るので、同じものですが一応指定しています。

Tauri 公式の説明だと配列で指定する例が載っていますが、それだとディレクトリも一緒に作られてその中に格納されます。辞書で指定すると、配置場所を指定できます。

まとめと感想

まとめ

target/release 下の .so ファイルをリンクする場合、以下の2つの環境変数の設定が必要。

RUSTFLAGS=-Clink-arg=-Wl,-rpath,$ORIGIN
LD_LIBRARY_PATH=/home/username/Projects/myapp/src-tauri/target/release

Linux でのビルドは、サポートする一番古い構成の環境を使って Docker やら GitHub Actions やらでビルドしましょう。

感想(読む必要なし)

今回ハマったのは、Rust のビルド周りや、linuxdeploy を使った AppImage の作成についての裏側の仕組みの知識が足りなかったためです。
諦めて static リンクしようとしたら、今度は sherpa-rs 側の問題でうまくいかず。フォークして build.rs を改造して対処しようとしたり・・・。
あと Docker コンテナのビルドに時間がかかる。しょっちゅう cargo clean してからビルドしてたので、アプリのビルドにも時間がかかる。linuxdeploy の AppImage バンドル作成も時間がかかる。GitHub Actions でのビルドも時間がかかる。という、Try & Error に時間がかかる問題。

分かってしまえば、単純なことだったのですが・・・。
linuxdeploy が LD_LIBRARLY_PATH を検索パスとして使うとか、、ドキュメントのどこにも記載されていないし。--library 引数で含めるライブラリを指定できると書いてあるけれど、今度は Tauri 側に linuxdeploy の引数を指定する仕組みがない。

Tauri は Linux が鬼門です。まず Linux で使われる WebView である WebKitGTK がイマイチすぎて、トラブルが多いです。そして linuxdeploy も。
そもそも Linux だと環境の違いを吸収するために AppImage とかで配布することになり、依存ライブラリを全部突っ込みます。OS 側の WebView を利用することによる配布サイズが小さくなる利点は完全に消え去ってしまいます。

それでも Tauri 2 + Svelte 5 (SvelteKit) の開発は気に入っていて、今後も個人で作るアプリでは積極的に利用すると思います。

開発環境整えたり、ビルド環境整えたりは、一度セットアップしてしまえば快適ですが、そこまで持っていくのに時間がかかりますね(Try & Error に時間がかかる場合は特に)。今回の問題解決では AI も役には立ったけれどイマイチ頼りにならず。

Discussion