1#[cfg(feature = "session")]
26pub use ort;
27
28#[cfg(feature = "half")]
30pub use half;
31
32#[cfg(feature = "ndarray")]
34pub use ndarray;
35
36#[cfg(feature = "session")]
37pub mod environment;
38#[cfg(feature = "session")]
39pub mod probe;
40#[cfg(feature = "session")]
41pub mod session;
42
43#[cfg(feature = "session")]
44pub use environment::{
45 cpu_thread_budget, init_environment, init_environment_with_options, shared_pool_active,
46 EnvironmentInitOptions, EnvironmentInitReport, EnvironmentOptions, EnvironmentReport,
47 EnvironmentStatus, GlobalPool,
48};
49#[cfg(feature = "session")]
50pub use probe::{probe_providers, ProviderDiagnostic, ProviderKind};
51#[cfg(feature = "session")]
52pub use session::{BuiltSession, ExecutionProvider, SessionError, SessionOptions};
53
54use std::fmt;
55use std::path::{Path, PathBuf};
56use std::sync::Mutex;
57
58static CONFIGURED_RUNTIME: Mutex<Option<PathBuf>> = Mutex::new(None);
59
60pub fn clear_inherited_runtime_for_release() {
68 clear_inherited_runtime_with(|| std::env::remove_var("ORT_DYLIB_PATH"));
69}
70
71fn clear_inherited_runtime_with(remove_path: impl FnOnce()) {
73 if cfg!(not(debug_assertions)) {
74 remove_path();
75 }
76}
77
78pub fn developer_override_candidate(env_name: &str) -> Option<RuntimeCandidate> {
82 if cfg!(debug_assertions) {
83 nonempty_env_path(env_name)
84 .map(|path| RuntimeCandidate::new(path, CandidateSource::Explicit))
85 } else {
86 None
87 }
88}
89
90pub fn configured_runtime() -> Option<PathBuf> {
95 let recorded = CONFIGURED_RUNTIME
96 .lock()
97 .unwrap_or_else(|poisoned| poisoned.into_inner())
98 .clone();
99 recorded.or_else(|| nonempty_env_path("ORT_DYLIB_PATH"))
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum CandidateSource {
104 Explicit,
105 Bundled,
106 AppData,
107 LegacyDefault,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct RuntimeCandidate {
113 pub path: PathBuf,
114 pub source: CandidateSource,
115}
116
117impl RuntimeCandidate {
118 pub fn new(path: impl Into<PathBuf>, source: CandidateSource) -> Self {
119 Self {
120 path: path.into(),
121 source,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct CandidateDiagnostic {
128 pub path: PathBuf,
129 pub source: CandidateSource,
130 pub absolute: bool,
131 pub expected_filename: bool,
132 pub exists: bool,
133 pub is_file: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct RuntimeSelection {
138 pub path: PathBuf,
139 pub source: CandidateSource,
140 pub diagnostics: Vec<CandidateDiagnostic>,
141}
142
143#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub struct RuntimeBindingOptions {
146 pub force_replace: bool,
149 pub allow_missing: bool,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum RuntimeBindingStatus {
156 Ready,
157 Missing,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct RuntimeBinding {
163 pub selection: RuntimeSelection,
164 pub status: RuntimeBindingStatus,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum RuntimeError {
169 NoCandidates,
170 UnsafePath {
171 path: PathBuf,
172 },
173 UnexpectedFilename {
174 path: PathBuf,
175 expected: &'static str,
176 },
177 NotFound {
178 diagnostics: Vec<CandidateDiagnostic>,
179 },
180 Canonicalize {
181 path: PathBuf,
182 message: String,
183 },
184 AlreadyConfigured {
185 configured: PathBuf,
186 selected: PathBuf,
187 },
188 SystemRuntimeRejected {
191 path: PathBuf,
192 },
193}
194
195impl fmt::Display for RuntimeError {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::NoCandidates => write!(f, "no ONNX Runtime candidates were supplied"),
199 Self::UnsafePath { path } => {
200 write!(f, "runtime path must be absolute: {}", path.display())
201 }
202 Self::UnexpectedFilename { path, expected } => {
203 write!(f, "runtime path {} must end in {expected}", path.display())
204 }
205 Self::NotFound { .. } => {
206 write!(f, "no supplied ONNX Runtime candidate is a regular file")
207 }
208 Self::Canonicalize { path, message } => {
209 write!(f, "cannot canonicalize {}: {message}", path.display())
210 }
211 Self::SystemRuntimeRejected { path } => write!(
212 f,
213 "runtime {} is the OS-provided copy; bundle and supply an app runtime",
214 path.display()
215 ),
216 Self::AlreadyConfigured {
217 configured,
218 selected,
219 } => write!(
220 f,
221 "ORT_DYLIB_PATH is already {}, refusing replacement with {}",
222 configured.display(),
223 selected.display()
224 ),
225 }
226 }
227}
228
229impl std::error::Error for RuntimeError {}
230
231pub fn runtime_filename() -> &'static str {
232 #[cfg(target_os = "windows")]
233 {
234 "onnxruntime.dll"
235 }
236 #[cfg(target_os = "macos")]
237 {
238 "libonnxruntime.dylib"
239 }
240 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
241 {
242 "libonnxruntime.so"
243 }
244}
245
246pub fn inspect_candidate(candidate: &RuntimeCandidate) -> CandidateDiagnostic {
247 CandidateDiagnostic {
248 path: candidate.path.clone(),
249 source: candidate.source,
250 absolute: candidate.path.is_absolute(),
251 expected_filename: candidate.path.file_name().and_then(|name| name.to_str())
252 == Some(runtime_filename()),
253 exists: candidate.path.exists(),
254 is_file: candidate.path.is_file(),
255 }
256}
257
258pub fn resolve_runtime(
259 candidates: impl IntoIterator<Item = RuntimeCandidate>,
260) -> Result<RuntimeSelection, RuntimeError> {
261 let candidates: Vec<_> = candidates.into_iter().collect();
262 if candidates.is_empty() {
263 return Err(RuntimeError::NoCandidates);
264 }
265 let mut diagnostics = Vec::with_capacity(candidates.len());
266 for candidate in candidates {
267 let diagnostic = inspect_candidate(&candidate);
268 if !diagnostic.absolute {
269 return Err(RuntimeError::UnsafePath {
270 path: candidate.path,
271 });
272 }
273 if !diagnostic.expected_filename {
274 return Err(RuntimeError::UnexpectedFilename {
275 path: candidate.path,
276 expected: runtime_filename(),
277 });
278 }
279 if is_system_runtime(&candidate.path) {
280 return Err(RuntimeError::SystemRuntimeRejected {
281 path: candidate.path,
282 });
283 }
284 diagnostics.push(diagnostic.clone());
285 if diagnostic.is_file {
286 let path =
287 candidate
288 .path
289 .canonicalize()
290 .map_err(|error| RuntimeError::Canonicalize {
291 path: candidate.path,
292 message: error.to_string(),
293 })?;
294 return Ok(RuntimeSelection {
295 path,
296 source: candidate.source,
297 diagnostics,
298 });
299 }
300 }
301 Err(RuntimeError::NotFound { diagnostics })
302}
303
304pub fn is_system_runtime(path: &Path) -> bool {
308 path.components().any(|component| {
309 component
310 .as_os_str()
311 .to_str()
312 .map(|name| {
313 name.eq_ignore_ascii_case("system32") || name.eq_ignore_ascii_case("syswow64")
314 })
315 .unwrap_or(false)
316 })
317}
318
319pub fn installed_resource_dir(executable: &Path) -> Option<PathBuf> {
324 if !executable.is_absolute() {
325 return None;
326 }
327 let dir = executable.parent()?.to_path_buf();
328 let is = |p: &Path, name: &str| {
329 p.file_name()
330 .and_then(|n| n.to_str())
331 .map(|n| n.eq_ignore_ascii_case(name))
332 .unwrap_or(false)
333 };
334 #[cfg(target_os = "macos")]
335 {
336 if is(&dir, "MacOS") {
337 if let Some(contents) = dir.parent() {
338 if is(contents, "Contents") {
339 return Some(contents.join("Resources"));
340 }
341 }
342 }
343 if is(&dir, "Resources") {
344 return Some(dir);
345 }
346 Some(dir.join("Resources"))
347 }
348 #[cfg(target_os = "windows")]
349 {
350 let _ = is;
351 Some(dir)
352 }
353 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
354 {
355 if is(&dir, "resources") {
356 Some(dir)
357 } else {
358 Some(dir.join("resources"))
359 }
360 }
361}
362
363pub fn installed_runtime_candidate(executable: &Path, subdir: &str) -> Option<RuntimeCandidate> {
366 let dir = installed_resource_dir(executable)?;
367 Some(RuntimeCandidate::new(
368 dir.join(subdir).join(runtime_filename()),
369 CandidateSource::Bundled,
370 ))
371}
372
373pub fn legacy_runtime_candidate(workspace_root: &Path) -> Result<RuntimeCandidate, RuntimeError> {
376 if !workspace_root.is_absolute() {
377 return Err(RuntimeError::UnsafePath {
378 path: workspace_root.to_path_buf(),
379 });
380 }
381 Ok(RuntimeCandidate::new(
382 workspace_root
383 .join("tools")
384 .join("bin")
385 .join(runtime_filename()),
386 CandidateSource::LegacyDefault,
387 ))
388}
389
390pub fn configure_runtime(
393 candidates: impl IntoIterator<Item = RuntimeCandidate>,
394) -> Result<RuntimeSelection, RuntimeError> {
395 configure_runtime_with_options(candidates, &RuntimeBindingOptions::default())
396 .map(|binding| binding.selection)
397}
398
399pub fn configure_runtime_with_options(
407 candidates: impl IntoIterator<Item = RuntimeCandidate>,
408 options: &RuntimeBindingOptions,
409) -> Result<RuntimeBinding, RuntimeError> {
410 let binding = configure_runtime_with_env(
411 candidates,
412 options,
413 nonempty_env_path("ORT_DYLIB_PATH"),
414 |path| std::env::set_var("ORT_DYLIB_PATH", path),
415 )?;
416 *CONFIGURED_RUNTIME
417 .lock()
418 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(binding.selection.path.clone());
419 Ok(binding)
420}
421
422fn configure_runtime_with_env(
424 candidates: impl IntoIterator<Item = RuntimeCandidate>,
425 options: &RuntimeBindingOptions,
426 configured: Option<PathBuf>,
427 set_path: impl FnOnce(&Path),
428) -> Result<RuntimeBinding, RuntimeError> {
429 let binding = match resolve_runtime(candidates) {
430 Ok(selection) => RuntimeBinding {
431 selection,
432 status: RuntimeBindingStatus::Ready,
433 },
434 Err(RuntimeError::NotFound { diagnostics }) if options.allow_missing => {
435 let Some(missing) = diagnostics.iter().find(|candidate| !candidate.exists) else {
436 return Err(RuntimeError::NotFound { diagnostics });
437 };
438 RuntimeBinding {
439 selection: RuntimeSelection {
440 path: missing.path.clone(),
441 source: missing.source,
442 diagnostics,
443 },
444 status: RuntimeBindingStatus::Missing,
445 }
446 }
447 Err(error) => return Err(error),
448 };
449 if let Some(configured) = configured.filter(|_| !options.force_replace) {
450 if options.allow_missing && configured == binding.selection.path {
451 return Ok(binding);
452 }
453 let configured = canonical_if_file(&configured)?;
454 if configured != binding.selection.path {
455 return Err(RuntimeError::AlreadyConfigured {
456 configured,
457 selected: binding.selection.path,
458 });
459 }
460 return Ok(binding);
461 }
462 set_path(&binding.selection.path);
463 Ok(binding)
464}
465
466fn nonempty_env_path(name: &str) -> Option<PathBuf> {
467 std::env::var_os(name)
468 .filter(|value| !value.is_empty())
469 .map(PathBuf::from)
470}
471
472fn canonical_if_file(path: &Path) -> Result<PathBuf, RuntimeError> {
473 if !path.is_absolute() {
474 return Err(RuntimeError::UnsafePath {
475 path: path.to_path_buf(),
476 });
477 }
478 path.canonicalize()
479 .map_err(|error| RuntimeError::Canonicalize {
480 path: path.to_path_buf(),
481 message: error.to_string(),
482 })
483}
484
485#[cfg(test)]
486mod binding_tests {
487 use super::*;
488 use std::sync::atomic::{AtomicUsize, Ordering};
489
490 #[cfg(debug_assertions)]
491 #[test]
492 fn clearing_inherited_runtime_is_noop_in_debug() {
493 clear_inherited_runtime_with(|| panic!("debug must preserve inherited runtime"));
494 }
495
496 #[cfg(not(debug_assertions))]
497 #[test]
498 fn clearing_inherited_runtime_removes_it_in_release() {
499 let mut inherited = Some(PathBuf::from("shell-exported-dev-runtime"));
500 clear_inherited_runtime_with(|| inherited = None);
501 assert_eq!(inherited, None);
502 }
503
504 struct Fixture(PathBuf);
505
506 impl Fixture {
507 fn new() -> Self {
508 static NEXT: AtomicUsize = AtomicUsize::new(0);
509 let root = std::env::temp_dir().join(format!(
510 "rightkit-ort-binding-{}-{}",
511 std::process::id(),
512 NEXT.fetch_add(1, Ordering::Relaxed)
513 ));
514 std::fs::create_dir_all(&root).unwrap();
515 Self(root.canonicalize().unwrap())
516 }
517
518 fn candidate(&self, directory: &str, exists: bool) -> RuntimeCandidate {
519 let path = self.0.join(directory).join(runtime_filename());
520 if exists {
521 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
522 std::fs::write(&path, b"not a real ORT binary").unwrap();
523 }
524 RuntimeCandidate::new(path, CandidateSource::Explicit)
525 }
526 }
527
528 impl Drop for Fixture {
529 fn drop(&mut self) {
530 let _ = std::fs::remove_dir_all(&self.0);
531 }
532 }
533
534 #[test]
535 fn binding_defaults_preserve_conflicts_and_require_files() {
536 let options = RuntimeBindingOptions::default();
537 assert!(!options.force_replace);
538 assert!(!options.allow_missing);
539 let fixture = Fixture::new();
540 let first = fixture.candidate("first", true);
541 let second = fixture.candidate("second", true);
542 let error = configure_runtime_with_env([second], &options, Some(first.path), |_| {
543 panic!("must not overwrite")
544 })
545 .unwrap_err();
546 assert!(matches!(error, RuntimeError::AlreadyConfigured { .. }));
547 let error = configure_runtime_with_env(
548 [fixture.candidate("missing", false)],
549 &options,
550 None,
551 |_| panic!("must not bind missing path by default"),
552 )
553 .unwrap_err();
554 assert!(matches!(error, RuntimeError::NotFound { .. }));
555 }
556
557 #[test]
558 fn forced_replacement_overwrites_existing_or_invalid_configuration() {
559 let fixture = Fixture::new();
560 let first = fixture.candidate("first", true);
561 let selected = fixture.candidate("second", true);
562 for configured in [first.path, PathBuf::from("invalid-relative-path")] {
563 let mut written = None;
564 let report = configure_runtime_with_env(
565 [selected.clone()],
566 &RuntimeBindingOptions {
567 force_replace: true,
568 ..Default::default()
569 },
570 Some(configured),
571 |path| written = Some(path.to_path_buf()),
572 )
573 .unwrap();
574 let expected = selected.path.canonicalize().unwrap();
575 assert_eq!(written, Some(expected.clone()));
576 assert_eq!(report.selection.path, expected);
577 assert_eq!(report.status, RuntimeBindingStatus::Ready);
578 }
579 }
580
581 #[test]
582 fn missing_mode_binds_records_and_reuses_first_absent_candidate() {
583 let fixture = Fixture::new();
584 let first = fixture.candidate("missing-first", false);
585 let second = fixture.candidate("missing-second", false);
586 let options = RuntimeBindingOptions {
587 allow_missing: true,
588 ..Default::default()
589 };
590 let mut written = None;
591 let report = configure_runtime_with_env([first.clone(), second], &options, None, |path| {
592 written = Some(path.to_path_buf())
593 })
594 .unwrap();
595 assert_eq!(report.status, RuntimeBindingStatus::Missing);
596 assert_eq!(report.selection.path, first.path);
597 assert_eq!(written, Some(first.path.clone()));
598 assert_eq!(report.selection.diagnostics.len(), 2);
599 assert!(report
600 .selection
601 .diagnostics
602 .iter()
603 .all(|d| !d.exists && !d.is_file));
604 let repeated =
605 configure_runtime_with_env([first.clone()], &options, Some(first.path), |_| {
606 panic!("matching binding must be preserved")
607 })
608 .unwrap();
609 assert_eq!(repeated.status, RuntimeBindingStatus::Missing);
610 }
611
612 #[test]
613 fn missing_mode_prefers_available_files_and_never_binds_directories() {
614 let fixture = Fixture::new();
615 let available = fixture.candidate("available", true);
616 let options = RuntimeBindingOptions {
617 allow_missing: true,
618 ..Default::default()
619 };
620 let report = configure_runtime_with_env(
621 [fixture.candidate("absent", false), available.clone()],
622 &options,
623 None,
624 |_| {},
625 )
626 .unwrap();
627 assert_eq!(report.status, RuntimeBindingStatus::Ready);
628 assert_eq!(
629 report.selection.path,
630 available.path.canonicalize().unwrap()
631 );
632 let directory = fixture.candidate("directory", false);
633 std::fs::create_dir_all(&directory.path).unwrap();
634 let error = configure_runtime_with_env([directory], &options, None, |_| {
635 panic!("directory must not be bound")
636 })
637 .unwrap_err();
638 assert!(matches!(error, RuntimeError::NotFound { .. }));
639 }
640
641 #[test]
642 fn forced_missing_binding_overwrites_existing_configuration() {
643 let fixture = Fixture::new();
644 let previous = fixture.candidate("previous", true);
645 let missing = fixture.candidate("missing", false);
646 let mut written = None;
647 let report = configure_runtime_with_env(
648 [missing.clone()],
649 &RuntimeBindingOptions {
650 force_replace: true,
651 allow_missing: true,
652 },
653 Some(previous.path),
654 |path| written = Some(path.to_path_buf()),
655 )
656 .unwrap();
657 assert_eq!(report.status, RuntimeBindingStatus::Missing);
658 assert_eq!(written, Some(missing.path));
659 }
660
661 #[test]
662 fn permissive_options_still_reject_unsafe_paths_and_system_runtime() {
663 let fixture = Fixture::new();
664 let options = RuntimeBindingOptions {
665 force_replace: true,
666 allow_missing: true,
667 };
668 for (candidate, expected) in [
669 (
670 RuntimeCandidate::new(runtime_filename(), CandidateSource::Explicit),
671 "relative",
672 ),
673 (
674 RuntimeCandidate::new(fixture.0.join("wrong.bin"), CandidateSource::Explicit),
675 "filename",
676 ),
677 (fixture.candidate("System32", false), "system"),
678 ] {
679 let error = configure_runtime_with_env([candidate], &options, None, |_| {
680 panic!("unsafe candidate must not be bound")
681 })
682 .unwrap_err();
683 assert!(match expected {
684 "relative" => matches!(error, RuntimeError::UnsafePath { .. }),
685 "filename" => matches!(error, RuntimeError::UnexpectedFilename { .. }),
686 _ => matches!(error, RuntimeError::SystemRuntimeRejected { .. }),
687 });
688 }
689 }
690}