Skip to main content

lean_ctx/core/
ort_environment.rs

1//! ONNX Runtime global environment: single init per process, runtime dylib loading.
2//!
3//! With the `load-dynamic` Cargo feature (always enabled in lean-ctx's `ort`
4//! dependency), `libonnxruntime` is loaded at runtime via [`ort::init_from`].
5//! This module resolves the library path across platforms, including NixOS.
6//!
7//! # Search order
8//!
9//! 1. `ORT_DYLIB_PATH` env var (resolved relative to the executable directory)
10//! 2. Nix profile paths — `/run/current-system/sw/lib/`, `~/.nix-profile/lib/` (Linux)
11//! 3. Well-known system directories per platform, including the active
12//!    `HOMEBREW_PREFIX` and the standard Homebrew/Linuxbrew lib dirs
13//! 4. `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH`
14//!
15//! If no copy is found, [`ensure_ort_env`] returns an eager error — session
16//! creation hangs rather than failing, so we fail fast.
17
18use std::path::{Path, PathBuf};
19use std::sync::OnceLock;
20
21use ort::ep::ExecutionProviderDispatch;
22
23/// Ensure the global ONNX Runtime environment is initialized.
24///
25/// On first call: resolves `libonnxruntime` via the search chain defined in
26/// `resolve_ort_dylib`, loads it with [`ort::init_from`], and registers GPU
27/// execution providers.  Subsequent calls are no-ops.
28///
29/// Returns an eager error when the shared library cannot be found (session
30/// creation would otherwise hang).
31pub fn ensure_ort_env(eps: &[ExecutionProviderDispatch]) -> anyhow::Result<()> {
32    static INIT: OnceLock<anyhow::Result<()>> = OnceLock::new();
33    // get_or_init runs the closure at most once; all subsequent calls return
34    // a reference to the stored Result.
35    match INIT.get_or_init(|| {
36        tracing::debug!("Initializing ONNX Runtime environment");
37        init_ort(eps)
38    }) {
39        Ok(()) => Ok(()),
40        // anyhow::Error is !Clone so we reconstitute from Display.
41        Err(e) => Err(anyhow::anyhow!("{e}")),
42    }
43}
44
45// ---------------------------------------------------------------------------
46// Initialisation
47// ---------------------------------------------------------------------------
48
49/// Load `libonnxruntime` at runtime via [`ort::init_from`].
50///
51/// The library path is resolved by [`resolve_ort_dylib`]; errors are
52/// propagated eagerly to avoid hanging on first session creation.
53fn init_ort(eps: &[ExecutionProviderDispatch]) -> anyhow::Result<()> {
54    let path = resolve_ort_dylib()?;
55
56    tracing::debug!("Loading libonnxruntime from {}", path.display());
57    ort::init_from(&path)
58        .map_err(|e| anyhow::anyhow!("ort::init_from({}) failed: {e}", path.display()))?
59        .with_name("lean-ctx")
60        .with_execution_providers(eps)
61        .commit();
62
63    tracing::info!("ONNX Runtime initialised ({})", path.display());
64    Ok(())
65}
66
67// ---------------------------------------------------------------------------
68// Library resolution
69// ---------------------------------------------------------------------------
70
71fn dylib_filename() -> &'static str {
72    if cfg!(target_os = "windows") {
73        "onnxruntime.dll"
74    } else if cfg!(target_os = "macos") {
75        "libonnxruntime.dylib"
76    } else {
77        "libonnxruntime.so"
78    }
79}
80
81/// Search for `libonnxruntime` across platform-specific locations.
82///
83/// Returns the first path found, or a descriptive error.
84fn resolve_ort_dylib() -> anyhow::Result<PathBuf> {
85    let name = dylib_filename();
86
87    // 1. ORT_DYLIB_PATH env var (resolved relative to exe dir)
88    if let Ok(p) = std::env::var("ORT_DYLIB_PATH") {
89        let path = PathBuf::from(&p);
90        if path.is_relative() {
91            let rel_to_exe = || -> Option<PathBuf> {
92                let exe = std::env::current_exe().ok()?;
93                let dir = exe.parent()?;
94                let abs = dir.join(&path);
95                abs.is_file().then_some(abs)
96            };
97            if let Some(abs) = rel_to_exe() {
98                return Ok(abs);
99            }
100        }
101        if path.is_file() {
102            return Ok(path);
103        }
104        anyhow::bail!("ORT_DYLIB_PATH={p} set but file does not exist");
105    }
106
107    // 2. Nix profile paths (Linux) — system & user profiles always point to
108    //    the currently activated version.
109    #[cfg(target_os = "linux")]
110    if let Some(found) = nix_profile_search(name) {
111        return Ok(found);
112    }
113
114    // 3. Well-known system paths (per platform)
115    if let Some(found) = well_known_paths(name) {
116        return Ok(found);
117    }
118
119    // 4. LD_LIBRARY_PATH / DYLD_LIBRARY_PATH
120    if let Some(found) = lib_path_search(name) {
121        return Ok(found);
122    }
123
124    anyhow::bail!(
125        "libonnxruntime not found.\n\
126         Set ORT_DYLIB_PATH=<path> to point to the shared library.\n\
127         Install:  pip install onnxruntime  (Python bundles the .so)\n\
128         NixOS:    nix-shell -p onnxruntime\n\
129         Homebrew: brew install onnxruntime\n\
130         Searched: ORT_DYLIB_PATH, Nix store, well-known system dirs, \
131         LD_LIBRARY_PATH/DYLD_LIBRARY_PATH"
132    )
133}
134
135// ---------------------------------------------------------------------------
136// Platform-specific searches
137// ---------------------------------------------------------------------------
138
139/// Check Nix profile symlinks for `libonnxruntime`.
140///
141/// Nix maintains `/run/current-system/sw/lib/` (system profile) and
142/// `~/.nix-profile/lib/` (user profile) as symlinks to the currently
143/// activated package versions — these are always authoritative.
144#[cfg(target_os = "linux")]
145fn nix_profile_search(name: &str) -> Option<PathBuf> {
146    let home = dirs::home_dir();
147    let user_profile = home
148        .as_ref()
149        .map(|h| h.join(".nix-profile").join("lib").join(name));
150    let candidates = [
151        Some(Path::new("/run/current-system/sw/lib").join(name)),
152        user_profile,
153    ];
154    candidates.into_iter().flatten().find(|c| c.is_file())
155}
156
157/// Check well-known system directories for `libonnxruntime`.
158fn well_known_paths(name: &str) -> Option<PathBuf> {
159    // Platform-specific hints.
160    let dirs: &[&str] = if cfg!(target_os = "linux") {
161        &[
162            "/usr/lib",
163            "/usr/lib64",
164            "/usr/local/lib",
165            // Linuxbrew default prefix (the `onnxruntime` formula symlinks its
166            // dylib here). A custom prefix is covered by HOMEBREW_PREFIX below.
167            "/home/linuxbrew/.linuxbrew/lib",
168        ]
169    } else if cfg!(target_os = "macos") {
170        &["/usr/local/lib", "/opt/homebrew/lib", "/opt/local/lib"]
171    } else if cfg!(target_os = "windows") {
172        // On Windows, check next to the executable and common install paths.
173        &[]
174    } else {
175        &["/usr/lib", "/usr/local/lib"]
176    };
177
178    // Also check next to the executable (common for portable installs, macOS
179    // Frameworks, Windows sibling layout, and Linux $ORIGIN setups).
180    let exe_relative = || -> Option<PathBuf> {
181        let exe = std::env::current_exe().ok()?;
182        let dir = exe.parent()?;
183        let sibling = dir.join(name);
184        if sibling.is_file() {
185            return Some(sibling);
186        }
187        // macOS app bundle: executable in MyApp.app/Contents/MacOS/,
188        // library in MyApp.app/Contents/Frameworks/
189        #[cfg(target_os = "macos")]
190        {
191            let parent = dir.parent()?;
192            let fw = parent.join("Frameworks").join(name);
193            if fw.is_file() {
194                return Some(fw);
195            }
196        }
197        None
198    };
199    if let Some(path) = exe_relative() {
200        return Some(path);
201    }
202
203    // Honor an active Homebrew environment. `brew shellenv` exports
204    // HOMEBREW_PREFIX, so a binary launched from a brew-configured shell can
205    // locate the dylib regardless of platform or custom prefix — Apple Silicon
206    // (/opt/homebrew), Intel (/usr/local) and Linuxbrew
207    // (/home/linuxbrew/.linuxbrew) all symlink `onnxruntime` into <prefix>/lib.
208    if let Ok(prefix) = std::env::var("HOMEBREW_PREFIX") {
209        let candidate = Path::new(&prefix).join("lib").join(name);
210        if candidate.is_file() {
211            return Some(candidate);
212        }
213    }
214
215    for dir in dirs {
216        let candidate = Path::new(dir).join(name);
217        if candidate.is_file() {
218            return Some(candidate);
219        }
220    }
221
222    None
223}
224
225/// Scan `LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) directories.
226fn lib_path_search(name: &str) -> Option<PathBuf> {
227    let var = if cfg!(target_os = "macos") {
228        "DYLD_LIBRARY_PATH"
229    } else {
230        "LD_LIBRARY_PATH"
231    };
232    let path = std::env::var(var).ok()?;
233    for segment in std::env::split_paths(&path) {
234        let candidate = segment.join(name);
235        if candidate.is_file() {
236            return Some(candidate);
237        }
238    }
239    None
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn dylib_filename_known_platform() {
248        let name = dylib_filename();
249        if cfg!(target_os = "linux") {
250            assert_eq!(name, "libonnxruntime.so");
251        } else if cfg!(target_os = "macos") {
252            assert_eq!(name, "libonnxruntime.dylib");
253        } else if cfg!(target_os = "windows") {
254            assert_eq!(name, "onnxruntime.dll");
255        }
256    }
257
258    #[test]
259    fn resolve_dylib_env_var_takes_precedence() {
260        // Set ORT_DYLIB_PATH to a known file (/tmp is guaranteed to exist,
261        // but the file itself won't — this should still error with a clear
262        // message about the file not existing).
263        crate::test_env::set_var("ORT_DYLIB_PATH", "/nonexistent/foo.so");
264        let err = resolve_ort_dylib().unwrap_err();
265        assert!(err.to_string().contains("ORT_DYLIB_PATH"));
266        crate::test_env::remove_var("ORT_DYLIB_PATH");
267    }
268
269    #[test]
270    fn lib_path_search_no_library() {
271        // Should not crash when the env var is unset.
272        assert!(lib_path_search("nonexistent.so.42").is_none());
273    }
274
275    #[test]
276    fn well_known_paths_returns_none_for_nonsense() {
277        assert!(well_known_paths("this-library-surely-does-not-exist.so").is_none());
278    }
279
280    #[test]
281    fn homebrew_prefix_lib_is_searched() {
282        // A dylib under $HOMEBREW_PREFIX/lib is discovered (covers Homebrew on
283        // any platform / custom prefix, incl. Linuxbrew). See issue #544.
284        let tmp = std::env::temp_dir().join(format!("lc-ort-hb-{}", std::process::id()));
285        let libdir = tmp.join("lib");
286        std::fs::create_dir_all(&libdir).unwrap();
287        let name = "libonnxruntime-test-marker.dylib";
288        std::fs::write(libdir.join(name), b"marker").unwrap();
289
290        crate::test_env::set_var("HOMEBREW_PREFIX", tmp.to_str().unwrap());
291        let found = well_known_paths(name);
292        crate::test_env::remove_var("HOMEBREW_PREFIX");
293        std::fs::remove_dir_all(&tmp).ok();
294
295        assert_eq!(found, Some(libdir.join(name)));
296    }
297
298    #[cfg(target_os = "linux")]
299    #[test]
300    fn nix_profile_search_no_panic() {
301        // When no Nix profile is present, returns None without crashing.
302        assert!(nix_profile_search("nonexistent.so").is_none());
303    }
304}