From e81d93389244bad34f57356fae0f7e36f3375216 Mon Sep 17 00:00:00 2001 From: Max Schmieder Date: Sun, 10 May 2026 16:02:33 -0400 Subject: [PATCH] Fix macOS Moonshine runtime bundling --- BUILDING.md | 11 ++-- CHANGELOG.md | 6 +-- scripts/stage-bundle-resources.cjs | 82 ++++++++++++++++++++---------- src-tauri/Cargo.toml | 7 +-- src-tauri/build.rs | 39 ++++++++++++-- src-tauri/tauri.conf.json | 8 ++- 6 files changed, 111 insertions(+), 42 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 65f8215..e00065b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -103,9 +103,10 @@ codegen-units = 1 # better LTO at cost of build time panic = "abort" # smaller binary, no unwind tables ``` -Final binary size: ~80 MB on Linux, ~100 MB on Windows (sherpa-onnx -ships its DLLs alongside, see the Cargo.toml comment for why), ~60 MB -on macOS. About half of that is the statically-linked CT2 runtime. +Final binary size: ~80 MB on Linux, ~100 MB on Windows and macOS +(sherpa-onnx ships shared runtime libraries alongside, see the +Cargo.toml comments for why). About half of that is the statically- +linked CT2 runtime. ## Cross-compilation @@ -134,8 +135,8 @@ production build matrix. - `src/index.html`, `src/main.js`, `src/styles.css` — settings + dashboard UI. - `src/overlay.html`, `src/overlay.js` — listening waveform overlay. - `scripts/stage-bundle-resources.cjs` — staging script the Tauri - bundler invokes via `beforeBundleCommand` to copy Windows DLLs into - the resources dir. + bundler invokes via `beforeBundleCommand` to copy sherpa-onnx / + ONNX Runtime shared libraries into the resources dir. ## Releasing diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5b8d4..0a1c191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,9 +44,9 @@ backends, cross-platform installers. ### Tech notes -- Final binary size: ~80 MB Linux, ~100 MB Windows (sherpa-onnx - ships its DLLs alongside on Windows due to a prebuilt-vs-local - MSVC ABI mismatch — see Cargo.toml comment), ~60 MB macOS. +- Final binary size: ~80 MB Linux, ~100 MB Windows and macOS + (sherpa-onnx ships shared runtime libraries alongside due to + platform-specific static-link issues — see Cargo.toml comments). - First clean build: ~25–35 min (CT2 C++ compile dominates). Incremental: ~30 s. CI uses `Swatinem/rust-cache` to skip the cold path on subsequent runs. diff --git a/scripts/stage-bundle-resources.cjs b/scripts/stage-bundle-resources.cjs index 2d41d13..0337c9d 100644 --- a/scripts/stage-bundle-resources.cjs +++ b/scripts/stage-bundle-resources.cjs @@ -1,10 +1,11 @@ #!/usr/bin/env node -// Stage runtime DLLs for the Tauri bundler. +// Stage runtime shared libraries for the Tauri bundler. // // On Windows we link sherpa-onnx in `shared` mode (see Cargo.toml // for the MSVC ABI rationale), which means the final installer // has to ship `sherpa-onnx-c-api.dll`, `onnxruntime.dll`, and a -// few siblings next to `vibe-to-text.exe`. +// few siblings next to `vibe-to-text.exe`. macOS ships the matching +// `.dylib` files next to the app executable. // // `sherpa-onnx-sys`'s build script copies them into // `src-tauri/target//` so `cargo run` works in dev. For @@ -14,14 +15,15 @@ // `target/release/` doesn't exist before the link step. // // This script runs as a Tauri `beforeBundleCommand` and copies the -// DLLs from `target//` into `src-tauri/bundle-resources/`, -// where the resources glob picks them up. +// shared libraries from `target//` into +// `src-tauri/bundle-resources/`, where the bundle config picks them up. // -// Cross-platform: on macOS / Linux we use sherpa-onnx in `static` -// mode — there's nothing to stage and this script exits quietly. +// Cross-platform: on macOS, Linux, and Windows we use sherpa-onnx +// in `shared` mode and stage the platform runtime libraries here. const fs = require("fs"); const path = require("path"); +const { spawnSync } = require("child_process"); const repoRoot = path.resolve(__dirname, ".."); const tauriDir = path.join(repoRoot, "src-tauri"); @@ -31,20 +33,9 @@ const stageDir = path.join(tauriDir, "bundle-resources"); // missing-dir validation. fs.mkdirSync(stageDir, { recursive: true }); -// macOS uses `static` sherpa-onnx (clean ABI, single-file .app -// bundle). Windows + Linux use `shared` because sherpa-onnx-sys's -// prebuilt static libs ship their own copy of protobuf which -// collides with sentencepiece-sys's protobuf at link time. On -// shared builds, sherpa-onnx-sys's build script copies the runtime -// libs into target//; we re-stage them here so Tauri's -// `bundle.resources` glob can pick them up alongside the executable -// in the installer. -if (process.platform === "darwin") { - console.log( - "stage-bundle-resources: skipping on darwin (sherpa-onnx is static here)." - ); - process.exit(0); -} +// sherpa-onnx-sys's build script copies the runtime libs into +// target//; we re-stage them here so Tauri can pick them +// up alongside the executable in the installer. // Tauri sets TAURI_ENV_DEBUG=true for `tauri dev` and false (or // unset) for `tauri build`. Match that to the cargo profile. @@ -54,16 +45,24 @@ const profile = process.env.TAURI_ENV_DEBUG === "true" ? "debug" : "release"; // rather than `target//`. Tauri exposes the active triple // as TAURI_ENV_TARGET_TRIPLE. const triple = process.env.TAURI_ENV_TARGET_TRIPLE || ""; -const targetDir = triple +const preferredTargetDir = triple ? path.join(tauriDir, "target", triple, profile) : path.join(tauriDir, "target", profile); +const fallbackTargetDir = path.join(tauriDir, "target", profile); +const targetDir = fs.existsSync(preferredTargetDir) + ? preferredTargetDir + : fallbackTargetDir; // The list sherpa-onnx-sys's build script emits when the `shared` // feature is on. The actual file extension + naming differs per -// platform: `.dll` on Windows, `.so` (with optional version -// suffixes like `.so.1.13.0`) on Linux. We glob for sensible name -// patterns rather than hard-code each file. +// platform: `.dll` on Windows, `.dylib` on macOS, `.so` (with +// optional version suffixes like `.so.1.13.0`) on Linux. We glob for +// sensible name patterns rather than hard-code each file. const PLATFORM_PATTERNS = { + darwin: [ + /^libsherpa-onnx.*\.dylib$/i, + /^libonnxruntime.*\.dylib$/i, + ], win32: [ /^sherpa-onnx.*\.dll$/i, /^onnxruntime.*\.dll$/i, @@ -83,15 +82,21 @@ if (!patterns) { } if (!fs.existsSync(targetDir)) { + const cargoBuildHint = + profile === "release" ? "cargo build --release" : "cargo build"; console.error( `stage-bundle-resources: targetDir ${targetDir} doesn't exist.\n` + - ` Run \`cargo build --release\` first.` + ` Run \`${cargoBuildHint}\` first.` ); process.exit(1); } const entries = fs.readdirSync(targetDir); let staged = 0; +for (const name of fs.readdirSync(stageDir)) { + if (!patterns.some((re) => re.test(name))) continue; + fs.rmSync(path.join(stageDir, name), { force: true }); +} for (const name of entries) { if (!patterns.some((re) => re.test(name))) continue; const src = path.join(targetDir, name); @@ -105,7 +110,11 @@ if (staged === 0) { console.error( `stage-bundle-resources: no matching shared libs found in ${targetDir}.\n` + ` Expected sherpa-onnx + onnxruntime ${ - process.platform === "win32" ? "DLLs" : ".so files" + process.platform === "win32" + ? "DLLs" + : process.platform === "darwin" + ? ".dylib files" + : ".so files" }.\n` + ` Confirm the sherpa-onnx \`shared\` feature is enabled.` ); @@ -114,3 +123,24 @@ if (staged === 0) { console.log( `stage-bundle-resources: staged ${staged} shared lib(s) from ${targetDir} → ${stageDir}` ); + +if (process.platform === "darwin") { + const executable = path.join(targetDir, "vibe-to-text"); + if (fs.existsSync(executable)) { + const result = spawnSync( + "install_name_tool", + ["-add_rpath", "@executable_path", executable], + { encoding: "utf8" } + ); + if (result.status !== 0) { + const stderr = result.stderr || ""; + if (!stderr.includes("would duplicate path")) { + console.error(stderr.trim()); + process.exit(result.status || 1); + } + } + console.log( + `stage-bundle-resources: ensured @executable_path rpath on ${executable}` + ); + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dcbe5d6..88fc6c2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -177,9 +177,10 @@ ct2rs = { version = "0.9", default-features = false, features = [ "ruy", "accelerate", ] } -# sherpa-onnx static — Apple's toolchain has a stable ABI, k2-fsa's -# prebuilt macOS static libs link cleanly. Single-file .app bundle. -sherpa-onnx = { version = "1.13", default-features = false, features = ["static"] } +# sherpa-onnx shared — matches Windows/Linux and avoids uncaught C++ +# exceptions crossing the Rust/static-library boundary during ONNX +# Runtime session creation on macOS. +sherpa-onnx = { version = "1.13", default-features = false, features = ["shared"] } # --- Linux: same CUDA-dynamic-loading story as Windows. --- # We could enable `openmp-runtime-comp` here (gomp.lib ships with diff --git a/src-tauri/build.rs b/src-tauri/build.rs index cec5812..c5961a4 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,7 +1,38 @@ -// Tauri's standard build hook. Whisper-rs statically links whisper.cpp -// into the binary at build time (via CMake), so we no longer need to -// shuffle external DLLs (onnxruntime.dll, moonshine.dll) next to the -// exe — those were Moonshine-era artifacts and have been removed. +use std::{env, fs, path::PathBuf}; + +// Tauri validates every bundle.resources source path while compiling +// this build script, before `beforeBundleCommand` can stage the real +// shared libraries. Keep zero-byte placeholders in place so local +// macOS / Linux `cargo check`, `tauri dev`, and `tauri build` follow +// the same path as CI. scripts/stage-bundle-resources.cjs overwrites +// these files with the runtime libraries before the installer is +// created. +const BUNDLE_RESOURCE_STUBS: &[&str] = &[ + "sherpa-onnx-c-api.dll", + "sherpa-onnx-cxx-api.dll", + "onnxruntime.dll", + "onnxruntime_providers_shared.dll", + "libsherpa-onnx-c-api.dylib", + "libsherpa-onnx-cxx-api.dylib", + "libonnxruntime.dylib", + "libonnxruntime.1.24.4.dylib", +]; + fn main() { + ensure_bundle_resource_stubs(); tauri_build::build(); } + +fn ensure_bundle_resource_stubs() { + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo")); + let resource_dir = manifest_dir.join("bundle-resources"); + + fs::create_dir_all(&resource_dir).expect("create bundle resource staging dir"); + for name in BUNDLE_RESOURCE_STUBS { + let path = resource_dir.join(name); + if !path.exists() { + fs::File::create(&path).expect("create bundle resource validation stub"); + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 615fa31..1b77e37 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -75,7 +75,13 @@ } }, "macOS": { - "minimumSystemVersion": "11.0" + "minimumSystemVersion": "11.0", + "files": { + "MacOS/libsherpa-onnx-c-api.dylib": "bundle-resources/libsherpa-onnx-c-api.dylib", + "MacOS/libsherpa-onnx-cxx-api.dylib": "bundle-resources/libsherpa-onnx-cxx-api.dylib", + "MacOS/libonnxruntime.dylib": "bundle-resources/libonnxruntime.dylib", + "MacOS/libonnxruntime.1.24.4.dylib": "bundle-resources/libonnxruntime.1.24.4.dylib" + } }, "linux": { "deb": {