lean_ctx/core/
ort_environment.rs1use std::ffi::{CStr, c_char, c_void};
24use std::path::{Path, PathBuf};
25use std::sync::OnceLock;
26
27use ort::ep::ExecutionProviderDispatch;
28
29pub fn ensure_ort_env(eps: &[ExecutionProviderDispatch]) -> anyhow::Result<()> {
38 static INIT: OnceLock<anyhow::Result<()>> = OnceLock::new();
39 match INIT.get_or_init(|| {
42 tracing::debug!("Initializing ONNX Runtime environment");
43 init_ort(eps)
44 }) {
45 Ok(()) => Ok(()),
46 Err(e) => Err(anyhow::anyhow!("{e}")),
48 }
49}
50
51fn init_ort(eps: &[ExecutionProviderDispatch]) -> anyhow::Result<()> {
60 let path = resolved_ort_dylib_path()?;
61
62 tracing::debug!("Loading libonnxruntime from {}", path.display());
63 validate_ort_dylib_version(&path)?;
64 tracing::debug!("Calling ort::init_from");
65 let init = ort::init_from(&path)
66 .map_err(|e| anyhow::anyhow!("ort::init_from({}) failed: {e}", path.display()))?;
67 tracing::debug!("ort::init_from returned; committing ONNX Runtime environment");
68 init.with_name("lean-ctx")
69 .with_execution_providers(eps)
70 .commit();
71 tracing::debug!("ONNX Runtime environment commit returned");
72
73 tracing::info!("ONNX Runtime initialised ({})", path.display());
74 Ok(())
75}
76
77pub(crate) fn resolved_ort_dylib_path() -> anyhow::Result<PathBuf> {
78 resolve_ort_dylib()
79}
80
81type OrtGetApiBase = unsafe extern "C" fn() -> *const OrtApiBase;
82type GetVersionString = unsafe extern "C" fn() -> *const c_char;
83
84#[repr(C)]
85struct OrtApiBase {
86 get_api: *const c_void,
87 get_version_string: GetVersionString,
88}
89
90fn validate_ort_dylib_version(path: &Path) -> anyhow::Result<()> {
91 let lib = unsafe { libloading::Library::new(path) }
95 .map_err(|e| anyhow::anyhow!("failed to load {}: {e}", path.display()))?;
96 let get_api_base: libloading::Symbol<OrtGetApiBase> = unsafe { lib.get(b"OrtGetApiBase") }
99 .map_err(|_| anyhow::anyhow!("{} does not export OrtGetApiBase", path.display()))?;
100 let base = unsafe { get_api_base() };
103 anyhow::ensure!(
104 !base.is_null(),
105 "OrtGetApiBase returned null for {}",
106 path.display()
107 );
108
109 let version = unsafe { ((*base).get_version_string)() };
112 let version = unsafe { CStr::from_ptr(version) }.to_string_lossy();
115 let minor = version
116 .split('.')
117 .nth(1)
118 .and_then(|part| part.parse::<u32>().ok())
119 .unwrap_or(0);
120 anyhow::ensure!(
121 minor >= ort::MINOR_VERSION,
122 "{} is ONNX Runtime {version}, but this lean-ctx build requires ONNX Runtime >= 1.{}.x; install a matching onnxruntime package or point ORT_DYLIB_PATH at a newer libonnxruntime",
123 path.display(),
124 ort::MINOR_VERSION,
125 );
126 Ok(())
127}
128
129fn dylib_filename() -> &'static str {
134 if cfg!(target_os = "windows") {
135 "onnxruntime.dll"
136 } else if cfg!(target_os = "macos") {
137 "libonnxruntime.dylib"
138 } else {
139 "libonnxruntime.so"
140 }
141}
142
143fn resolve_ort_dylib() -> anyhow::Result<PathBuf> {
147 let name = dylib_filename();
148
149 if let Ok(p) = std::env::var("ORT_DYLIB_PATH") {
151 let path = PathBuf::from(&p);
152 if path.is_relative() {
153 let rel_to_exe = || -> Option<PathBuf> {
154 let exe = std::env::current_exe().ok()?;
155 let dir = exe.parent()?;
156 let abs = dir.join(&path);
157 abs.is_file().then_some(abs)
158 };
159 if let Some(abs) = rel_to_exe() {
160 return Ok(abs);
161 }
162 }
163 if path.is_file() {
164 return Ok(path);
165 }
166 anyhow::bail!("ORT_DYLIB_PATH={p} set but file does not exist");
167 }
168
169 if let Some(found) = crate::core::addons::ort_provision::managed_dylib_path() {
174 return Ok(found);
175 }
176
177 #[cfg(target_os = "linux")]
180 if let Some(found) = nix_profile_search(name) {
181 return Ok(found);
182 }
183
184 if let Some(found) = well_known_paths(name) {
186 return Ok(found);
187 }
188
189 if let Some(found) = lib_path_search(name) {
191 return Ok(found);
192 }
193
194 anyhow::bail!(
195 "libonnxruntime not found.\n\
196 Managed: lean-ctx embeddings provision (official CPU runtime, sha256-pinned)\n\
197 Or set ORT_DYLIB_PATH=<path> to point to the shared library.\n\
198 Install: pip install onnxruntime (Python bundles the .so)\n\
199 NixOS: nix-shell -p onnxruntime\n\
200 Homebrew: brew install onnxruntime\n\
201 Searched: ORT_DYLIB_PATH, managed runtime dir, Nix store, \
202 well-known system dirs, LD_LIBRARY_PATH/DYLD_LIBRARY_PATH"
203 )
204}
205
206#[cfg(target_os = "linux")]
217fn nix_profile_search(name: &str) -> Option<PathBuf> {
218 let home = dirs::home_dir();
219 let user_profile = home
220 .as_ref()
221 .map(|h| h.join(".nix-profile").join("lib").join(name));
222 let candidates = [
223 Some(Path::new("/run/current-system/sw/lib").join(name)),
224 nix_per_user_lib(Path::new("/etc/profiles/per-user"), name),
225 user_profile,
226 ];
227 candidates.into_iter().flatten().find(|c| c.is_file())
228}
229
230#[cfg(target_os = "linux")]
236fn nix_per_user_lib(base: &Path, name: &str) -> Option<PathBuf> {
237 let user = std::env::var("USER").ok()?;
238 if user.is_empty() || user.contains('/') || user.contains('\0') || user.contains("..") {
239 return None;
240 }
241 let candidate = base.join(&user).join("lib").join(name);
242 candidate.is_file().then_some(candidate)
243}
244
245fn well_known_paths(name: &str) -> Option<PathBuf> {
247 let dirs: &[&str] = if cfg!(target_os = "linux") {
249 &[
250 "/usr/lib",
251 "/usr/lib64",
252 "/usr/local/lib",
253 "/home/linuxbrew/.linuxbrew/lib",
256 ]
257 } else if cfg!(target_os = "macos") {
258 &["/usr/local/lib", "/opt/homebrew/lib", "/opt/local/lib"]
259 } else if cfg!(target_os = "windows") {
260 &[]
262 } else {
263 &["/usr/lib", "/usr/local/lib"]
264 };
265
266 let exe_relative = || -> Option<PathBuf> {
269 let exe = std::env::current_exe().ok()?;
270 let dir = exe.parent()?;
271 let sibling = dir.join(name);
272 if sibling.is_file() {
273 return Some(sibling);
274 }
275 #[cfg(target_os = "macos")]
278 {
279 let parent = dir.parent()?;
280 let fw = parent.join("Frameworks").join(name);
281 if fw.is_file() {
282 return Some(fw);
283 }
284 }
285 None
286 };
287 if let Some(path) = exe_relative() {
288 return Some(path);
289 }
290
291 if let Ok(prefix) = std::env::var("HOMEBREW_PREFIX") {
297 let candidate = Path::new(&prefix).join("lib").join(name);
298 if candidate.is_file() {
299 return Some(candidate);
300 }
301 }
302
303 for dir in dirs {
304 let candidate = Path::new(dir).join(name);
305 if candidate.is_file() {
306 return Some(candidate);
307 }
308 }
309
310 None
311}
312
313fn lib_path_search(name: &str) -> Option<PathBuf> {
315 let var = if cfg!(target_os = "macos") {
316 "DYLD_LIBRARY_PATH"
317 } else {
318 "LD_LIBRARY_PATH"
319 };
320 let path = std::env::var(var).ok()?;
321 for segment in std::env::split_paths(&path) {
322 let candidate = segment.join(name);
323 if candidate.is_file() {
324 return Some(candidate);
325 }
326 }
327 None
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn dylib_filename_known_platform() {
336 let name = dylib_filename();
337 if cfg!(target_os = "linux") {
338 assert_eq!(name, "libonnxruntime.so");
339 } else if cfg!(target_os = "macos") {
340 assert_eq!(name, "libonnxruntime.dylib");
341 } else if cfg!(target_os = "windows") {
342 assert_eq!(name, "onnxruntime.dll");
343 }
344 }
345
346 #[test]
347 fn resolve_dylib_env_var_takes_precedence() {
348 let _env_lock = crate::core::data_dir::test_env_lock();
349 crate::test_env::set_var("ORT_DYLIB_PATH", "/nonexistent/foo.so");
353 let err = resolve_ort_dylib().unwrap_err();
354 assert!(err.to_string().contains("ORT_DYLIB_PATH"));
355 crate::test_env::remove_var("ORT_DYLIB_PATH");
356 }
357
358 #[test]
359 fn lib_path_search_no_library() {
360 assert!(lib_path_search("nonexistent.so.42").is_none());
362 }
363
364 #[test]
365 fn well_known_paths_returns_none_for_nonsense() {
366 assert!(well_known_paths("this-library-surely-does-not-exist.so").is_none());
367 }
368
369 #[test]
370 fn homebrew_prefix_lib_is_searched() {
371 let _env_lock = crate::core::data_dir::test_env_lock();
372 let tmp = std::env::temp_dir().join(format!("lc-ort-hb-{}", std::process::id()));
375 let libdir = tmp.join("lib");
376 std::fs::create_dir_all(&libdir).unwrap();
377 let name = "libonnxruntime-test-marker.dylib";
378 std::fs::write(libdir.join(name), b"marker").unwrap();
379
380 crate::test_env::set_var("HOMEBREW_PREFIX", tmp.to_str().unwrap());
381 let found = well_known_paths(name);
382 crate::test_env::remove_var("HOMEBREW_PREFIX");
383 std::fs::remove_dir_all(&tmp).ok();
384
385 assert_eq!(found, Some(libdir.join(name)));
386 }
387
388 #[cfg(target_os = "linux")]
389 #[test]
390 fn nix_profile_search_no_panic() {
391 assert!(nix_profile_search("nonexistent.so").is_none());
392 }
393
394 #[cfg(target_os = "linux")]
395 #[test]
396 fn nix_per_user_lib_discovers_file_under_base() {
397 let _env_lock = crate::core::data_dir::test_env_lock();
398 let tmp = std::env::temp_dir().join(format!("lc-nix-pu-{}", std::process::id()));
399 let name = "libonnxruntime-test-marker.so";
400 let user = "testuser";
401 let libdir = tmp.join(user).join("lib");
402 std::fs::create_dir_all(&libdir).unwrap();
403 std::fs::write(libdir.join(name), b"marker").unwrap();
404
405 crate::test_env::set_var("USER", user);
406 let found = nix_per_user_lib(&tmp, name);
407 crate::test_env::remove_var("USER");
408 std::fs::remove_dir_all(&tmp).ok();
409
410 assert_eq!(found, Some(libdir.join(name)));
411 }
412
413 #[cfg(target_os = "linux")]
414 #[test]
415 fn nix_per_user_lib_rejects_traversal_in_user() {
416 let _env_lock = crate::core::data_dir::test_env_lock();
417 let tmp = std::env::temp_dir().join(format!("lc-nix-trv-{}", std::process::id()));
418 std::fs::create_dir_all(&tmp).unwrap();
419
420 for bad in ["", "../etc", "foo/bar"] {
423 crate::test_env::set_var("USER", bad);
424 assert!(
425 nix_per_user_lib(&tmp, "lib.so").is_none(),
426 "USER={bad:?} should be rejected"
427 );
428 }
429 crate::test_env::remove_var("USER");
430 std::fs::remove_dir_all(&tmp).ok();
431 }
432}