lean_ctx/core/
ort_environment.rs1use std::path::{Path, PathBuf};
19use std::sync::OnceLock;
20
21use ort::ep::ExecutionProviderDispatch;
22
23pub fn ensure_ort_env(eps: &[ExecutionProviderDispatch]) -> anyhow::Result<()> {
32 static INIT: OnceLock<anyhow::Result<()>> = OnceLock::new();
33 match INIT.get_or_init(|| {
36 tracing::debug!("Initializing ONNX Runtime environment");
37 init_ort(eps)
38 }) {
39 Ok(()) => Ok(()),
40 Err(e) => Err(anyhow::anyhow!("{e}")),
42 }
43}
44
45fn 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
67fn 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
81fn resolve_ort_dylib() -> anyhow::Result<PathBuf> {
85 let name = dylib_filename();
86
87 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 #[cfg(target_os = "linux")]
110 if let Some(found) = nix_profile_search(name) {
111 return Ok(found);
112 }
113
114 if let Some(found) = well_known_paths(name) {
116 return Ok(found);
117 }
118
119 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#[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
157fn well_known_paths(name: &str) -> Option<PathBuf> {
159 let dirs: &[&str] = if cfg!(target_os = "linux") {
161 &[
162 "/usr/lib",
163 "/usr/lib64",
164 "/usr/local/lib",
165 "/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 &[]
174 } else {
175 &["/usr/lib", "/usr/local/lib"]
176 };
177
178 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 #[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 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
225fn 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 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 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 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 assert!(nix_profile_search("nonexistent.so").is_none());
303 }
304}