Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ jobs:
echo "dist/ OK:"
ls -la dist/ dist/assets/

# NOTE: the MSVC CRT for the *installer* bundle is staged by
# build.rs (stage_vcredist) into src-tauri/vcredist/ and shipped via
# tauri.windows.conf.json's `vcredist` resource — so no staging step
# is needed here. The portable .zip gets its own copy next to the
# exe in the "Package portable binary" step below.

- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -202,6 +208,16 @@ jobs:
if [[ "$have_espeak" == 1 ]]; then
cp "$ESPEAK" "dist-bin/espeak-ng.exe"; payload+=(espeak-ng.exe)
fi
# Ship the MSVC CRT next to the exe so the fetched onnxruntime.dll
# loads on a PC without the VC++ redistributable — the exe's own
# directory is always searched for a DLL's dependencies.
for dll in vcruntime140.dll vcruntime140_1.dll msvcp140.dll; do
if cp "/c/Windows/System32/$dll" "dist-bin/$dll"; then
payload+=("$dll")
else
echo "::warning::couldn't stage $dll for the portable zip"
fi
done
# chdir before hashing so the `.sha256` sidecar records just the
# basename — matching how the GitHub asset is uploaded. Hashing
# `dist-bin/<file>` from the repo root would bake `dist-bin/`
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ src-tauri/gen/schemas/*-schema.json
# bundler can ship it as a sidecar (see tauri.conf.json::externalBin).
# Build artefact; the pinned source rev lives in `.myownmesh-rev`.
src-tauri/binaries/
# release.yml stages the redistributable MSVC CRT DLLs here on Windows
# (from the runner's System32) so the bundler ships them as resources;
# they're build artefacts pulled in at release time, never source.
src-tauri/vcredist/
.env
.env.local
.DS_Store
Expand Down
61 changes: 61 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ fn main() {
}
}

// Windows: stage the MSVC CRT the fetched onnxruntime.dll links
// against (vcruntime140 / msvcp140), so the bundle ships it via
// tauri.windows.conf.json's `vcredist` resource — a PC without the
// VC++ redistributable then still loads the speech engine. Runs for
// every Windows build (CI, `tauri dev`, release) so the bundler's
// resource check always sees a populated dir.
let target_triple = env::var("TARGET").unwrap_or_default();
if target_triple.contains("windows") {
if let Err(e) = stage_vcredist() {
println!("cargo:warning=could not stage vcredist MSVC runtime: {e:#}");
}
}

tauri_build::build();
}

Expand Down Expand Up @@ -139,6 +152,54 @@ fn write_espeak_stub() -> std::io::Result<()> {
Ok(())
}

/// Stage the redistributable MSVC runtime DLLs into `vcredist/` so the
/// Windows bundle ships them as a `resources` entry (see
/// tauri.windows.conf.json). The fetched `onnxruntime.dll` links these,
/// so shipping them lets a PC without the Visual C++ redistributable
/// load the speech engine instead of failing.
///
/// Best-effort: copies whatever the host's System32 holds and, if none
/// were staged (e.g. a cross-build host), drops a marker so the resource
/// dir is non-empty and `tauri_build`'s check still passes — mirroring
/// [`write_espeak_stub`]. With no bundled CRT the runtime simply falls
/// back to a system-installed one.
fn stage_vcredist() -> std::io::Result<()> {
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let dir = crate_dir.join("vcredist");
fs::create_dir_all(&dir)?;

let system32 = env::var("SystemRoot")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(r"C:\Windows"))
.join("System32");

let mut staged = 0;
for dll in ["vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll"] {
let dst = dir.join(dll);
if dst.exists() {
staged += 1;
continue;
}
let src = system32.join(dll);
if src.exists() {
match fs::copy(&src, &dst) {
Ok(_) => staged += 1,
Err(e) => println!("cargo:warning=couldn't stage {dll} into vcredist/: {e}"),
}
}
}

if staged == 0 {
// Keep the resource dir non-empty so the bundler's existence
// check passes even when no CRT could be sourced.
fs::write(
dir.join(".vcredist-stub"),
b"MSVC CRT not staged (no System32 source); runtime uses the system VC++ redistributable\n",
)?;
}
Ok(())
}

/// Build + stage the owned espeak-ng. Resolution order, mirroring the daemon
/// sidecar: a prebuilt prefix via `MYOWNLLM_ESPEAK_PREBUILT` (CI builds once
/// and points here), else a cached static build from source.
Expand Down
102 changes: 102 additions & 0 deletions src-tauri/src/ort_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ fn run_init() -> (OrtStatus, String) {
"[ort_setup] loading onnxruntime from {}…",
existing.display()
);
// On Windows, onnxruntime.dll links the MSVC runtime
// (vcruntime140 / msvcp140). Make any CRT we ship with the app
// discoverable so the load doesn't fail/hang on a consumer PC that
// lacks the VC++ redistributable — the failure this function would
// otherwise report below.
#[cfg(windows)]
prepare_msvc_runtime(existing.parent().unwrap_or_else(|| Path::new(".")));
let load_path = existing.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
Expand Down Expand Up @@ -318,6 +325,81 @@ fn run_init() -> (OrtStatus, String) {
}
}

/// Candidate directories that may hold the MSVC CRT DLLs we bundle with
/// the app (`vcruntime140.dll` & friends), most-specific first. The
/// portable zip ships them next to the exe; the Windows installer ships
/// them as Tauri resources (a `vcredist/` subdir). Kept pure so the
/// directory list is unit-testable on any OS — only the search-path
/// mutation in [`prepare_msvc_runtime`] is Windows-specific.
///
/// Gated to where it's actually referenced (the Windows caller or the
/// tests) so a Linux release build doesn't flag it dead.
#[cfg(any(windows, test))]
fn msvc_runtime_search_dirs(runtime_dir: &Path, exe_dir: Option<&Path>) -> Vec<PathBuf> {
let mut dirs = vec![runtime_dir.to_path_buf()];
if let Some(d) = exe_dir {
dirs.push(d.to_path_buf()); // portable: next to the exe
dirs.push(d.join("resources")); // tauri resource root (NSIS)
dirs.push(d.join("vcredist")); // resources mapped to vcredist/
dirs.push(d.join("resources").join("vcredist"));
}
dirs
}

/// Windows: ensure onnxruntime.dll's MSVC-runtime dependency resolves
/// from a CRT we ship with the app, so a PC without the VC++
/// redistributable can still load the speech engine. Finds the dir
/// holding the bundled CRT and prepends it to the DLL search path —
/// which also governs dependency resolution for the subsequent
/// `ort::init_from`. A no-op (leaving the existing "install the VC++
/// runtime" guidance to fire) if we didn't bundle one.
#[cfg(windows)]
fn prepare_msvc_runtime(runtime_dir: &Path) {
const PROBE: &str = "vcruntime140.dll";
let exe = std::env::current_exe().ok();
let exe_dir = exe.as_deref().and_then(Path::parent);
for dir in msvc_runtime_search_dirs(runtime_dir, exe_dir) {
if dir.join(PROBE).exists() {
if win::set_dll_directory(&dir) {
eprintln!(
"[ort_setup] bundled MSVC runtime found; added to DLL search path: {}",
dir.display()
);
} else {
eprintln!("[ort_setup] SetDllDirectory failed for {}", dir.display());
}
return;
}
}
eprintln!(
"[ort_setup] no bundled MSVC runtime DLLs found near the app; \
relying on a system-installed VC++ redistributable"
);
}

#[cfg(windows)]
mod win {
use std::os::windows::ffi::OsStrExt;
use std::path::Path;

#[allow(non_snake_case)]
extern "system" {
fn SetDllDirectoryW(path: *const u16) -> i32;
}

/// Prepend `dir` to the process DLL search path (and thus the
/// dependency search for DLLs loaded afterwards). Returns false on
/// failure. `SetDllDirectoryW` is exported by kernel32, which the
/// MSVC target links by default.
pub fn set_dll_directory(dir: &Path) -> bool {
let mut wide: Vec<u16> = dir.as_os_str().encode_wide().collect();
wide.push(0);
// SAFETY: `wide` is a valid NUL-terminated UTF-16 buffer that
// outlives the call; SetDllDirectoryW only reads from it.
unsafe { SetDllDirectoryW(wide.as_ptr()) != 0 }
}
}

/// Where to look for `libonnxruntime.{dylib,so,dll}`, in priority order.
/// `env_override` is the `ORT_DYLIB_PATH` env var the public wrapper reads;
/// passing it in (rather than reading the env directly) keeps the unit
Expand Down Expand Up @@ -505,6 +587,26 @@ mod tests {
);
}

#[test]
fn msvc_search_dirs_lists_runtime_and_bundle_locations() {
let rt = Path::new("/home/user/.myownllm/runtime");
let exe_dir = Path::new("/opt/app");
let dirs = msvc_runtime_search_dirs(rt, Some(exe_dir));
// The onnxruntime dir is checked first (DLLs next to the dylib).
assert_eq!(dirs.first().map(|p| p.as_path()), Some(rt));
// Portable layout: CRT next to the exe.
assert!(dirs.iter().any(|p| p == Path::new("/opt/app")));
// Installer layouts: a vcredist/ resource dir and the resource root.
assert!(dirs.iter().any(|p| p.ends_with("vcredist")));
assert!(dirs.iter().any(|p| p.ends_with("resources")));
}

#[test]
fn msvc_search_dirs_without_exe_is_just_runtime() {
let rt = Path::new("/tmp/rt");
assert_eq!(msvc_runtime_search_dirs(rt, None), vec![rt.to_path_buf()]);
}

#[test]
fn status_before_init_reports_not_initialized() {
let s = status();
Expand Down
Loading
Loading