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
466pub fn discovery_candidates(
472 extra_dirs: impl IntoIterator<Item = PathBuf>,
473) -> Vec<RuntimeCandidate> {
474 let mut out = Vec::new();
475 if let Some(path) = nonempty_env_path("ORT_DYLIB_PATH") {
476 out.push(RuntimeCandidate::new(path, CandidateSource::Explicit));
477 }
478 let name = runtime_filename();
479 if let Some(exe_dir) = std::env::current_exe()
480 .ok()
481 .and_then(|exe| exe.parent().map(Path::to_path_buf))
482 {
483 out.push(RuntimeCandidate::new(
484 exe_dir.join("runtime").join(name),
485 CandidateSource::Bundled,
486 ));
487 out.push(RuntimeCandidate::new(
488 exe_dir.join(name),
489 CandidateSource::Bundled,
490 ));
491 }
492 out.extend(
493 extra_dirs
494 .into_iter()
495 .filter(|dir| dir.is_absolute())
496 .map(|dir| RuntimeCandidate::new(dir.join(name), CandidateSource::AppData)),
497 );
498 out
499}
500
501pub fn discover_runtime(extra_dirs: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
505 discovery_candidates(extra_dirs)
506 .into_iter()
507 .map(|candidate| candidate.path)
508 .find(|path| path.is_file() && !is_system_runtime(path))
509}
510
511pub fn export_discovered_runtime(extra_dirs: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
515 if let Some(existing) = nonempty_env_path("ORT_DYLIB_PATH") {
516 return Some(existing);
517 }
518 let path = discover_runtime(extra_dirs)?;
519 std::env::set_var("ORT_DYLIB_PATH", &path);
520 if let (Some(dir), Some(existing)) = (path.parent(), std::env::var_os("PATH")) {
521 let mut paths = vec![dir.to_path_buf()];
522 paths.extend(std::env::split_paths(&existing));
523 if let Ok(joined) = std::env::join_paths(paths) {
524 std::env::set_var("PATH", joined);
525 }
526 }
527 Some(path)
528}
529
530fn nonempty_env_path(name: &str) -> Option<PathBuf> {
531 std::env::var_os(name)
532 .filter(|value| !value.is_empty())
533 .map(PathBuf::from)
534}
535
536fn canonical_if_file(path: &Path) -> Result<PathBuf, RuntimeError> {
537 if !path.is_absolute() {
538 return Err(RuntimeError::UnsafePath {
539 path: path.to_path_buf(),
540 });
541 }
542 path.canonicalize()
543 .map_err(|error| RuntimeError::Canonicalize {
544 path: path.to_path_buf(),
545 message: error.to_string(),
546 })
547}
548
549#[cfg(test)]
550mod discovery_tests {
551 use super::*;
552
553 #[test]
554 fn caller_dirs_follow_exe_locations_and_relative_dirs_are_skipped() {
555 let abs = std::env::temp_dir().join("rightkit-ort-discovery-absent");
556 let candidates = discovery_candidates([PathBuf::from("relative/runtime"), abs.clone()]);
557 let last = candidates.last().expect("caller candidate");
558 assert_eq!(last.path, abs.join(runtime_filename()));
559 assert_eq!(last.source, CandidateSource::AppData);
560 assert!(candidates
561 .iter()
562 .all(|c| !c.path.starts_with("relative/runtime")));
563 let exe_pos = candidates
564 .iter()
565 .position(|c| c.source == CandidateSource::Bundled);
566 assert!(exe_pos.is_none_or(|pos| pos < candidates.len() - 1));
567 }
568
569 #[test]
570 fn discovery_finds_a_regular_file_and_ignores_a_directory() {
571 let root = std::env::temp_dir().join(format!("rightkit-ort-disc-{}", std::process::id()));
572 let dir_hit = root.join("dir");
573 let file_hit = root.join("file");
574 std::fs::create_dir_all(dir_hit.join(runtime_filename())).unwrap();
575 std::fs::create_dir_all(&file_hit).unwrap();
576 std::fs::write(file_hit.join(runtime_filename()), b"x").unwrap();
577 let found = discover_runtime([dir_hit, file_hit.clone()]);
578 if nonempty_env_path("ORT_DYLIB_PATH").is_none_or(|p| !p.is_file()) {
580 assert_eq!(found, Some(file_hit.join(runtime_filename())));
581 }
582 let _ = std::fs::remove_dir_all(root);
583 }
584}
585
586#[cfg(test)]
587mod binding_tests {
588 use super::*;
589 use std::sync::atomic::{AtomicUsize, Ordering};
590
591 #[cfg(debug_assertions)]
592 #[test]
593 fn clearing_inherited_runtime_is_noop_in_debug() {
594 clear_inherited_runtime_with(|| panic!("debug must preserve inherited runtime"));
595 }
596
597 #[cfg(not(debug_assertions))]
598 #[test]
599 fn clearing_inherited_runtime_removes_it_in_release() {
600 let mut inherited = Some(PathBuf::from("shell-exported-dev-runtime"));
601 clear_inherited_runtime_with(|| inherited = None);
602 assert_eq!(inherited, None);
603 }
604
605 struct Fixture(PathBuf);
606
607 impl Fixture {
608 fn new() -> Self {
609 static NEXT: AtomicUsize = AtomicUsize::new(0);
610 let root = std::env::temp_dir().join(format!(
611 "rightkit-ort-binding-{}-{}",
612 std::process::id(),
613 NEXT.fetch_add(1, Ordering::Relaxed)
614 ));
615 std::fs::create_dir_all(&root).unwrap();
616 Self(root.canonicalize().unwrap())
617 }
618
619 fn candidate(&self, directory: &str, exists: bool) -> RuntimeCandidate {
620 let path = self.0.join(directory).join(runtime_filename());
621 if exists {
622 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
623 std::fs::write(&path, b"not a real ORT binary").unwrap();
624 }
625 RuntimeCandidate::new(path, CandidateSource::Explicit)
626 }
627 }
628
629 impl Drop for Fixture {
630 fn drop(&mut self) {
631 let _ = std::fs::remove_dir_all(&self.0);
632 }
633 }
634
635 #[test]
636 fn binding_defaults_preserve_conflicts_and_require_files() {
637 let options = RuntimeBindingOptions::default();
638 assert!(!options.force_replace);
639 assert!(!options.allow_missing);
640 let fixture = Fixture::new();
641 let first = fixture.candidate("first", true);
642 let second = fixture.candidate("second", true);
643 let error = configure_runtime_with_env([second], &options, Some(first.path), |_| {
644 panic!("must not overwrite")
645 })
646 .unwrap_err();
647 assert!(matches!(error, RuntimeError::AlreadyConfigured { .. }));
648 let error = configure_runtime_with_env(
649 [fixture.candidate("missing", false)],
650 &options,
651 None,
652 |_| panic!("must not bind missing path by default"),
653 )
654 .unwrap_err();
655 assert!(matches!(error, RuntimeError::NotFound { .. }));
656 }
657
658 #[test]
659 fn forced_replacement_overwrites_existing_or_invalid_configuration() {
660 let fixture = Fixture::new();
661 let first = fixture.candidate("first", true);
662 let selected = fixture.candidate("second", true);
663 for configured in [first.path, PathBuf::from("invalid-relative-path")] {
664 let mut written = None;
665 let report = configure_runtime_with_env(
666 [selected.clone()],
667 &RuntimeBindingOptions {
668 force_replace: true,
669 ..Default::default()
670 },
671 Some(configured),
672 |path| written = Some(path.to_path_buf()),
673 )
674 .unwrap();
675 let expected = selected.path.canonicalize().unwrap();
676 assert_eq!(written, Some(expected.clone()));
677 assert_eq!(report.selection.path, expected);
678 assert_eq!(report.status, RuntimeBindingStatus::Ready);
679 }
680 }
681
682 #[test]
683 fn missing_mode_binds_records_and_reuses_first_absent_candidate() {
684 let fixture = Fixture::new();
685 let first = fixture.candidate("missing-first", false);
686 let second = fixture.candidate("missing-second", false);
687 let options = RuntimeBindingOptions {
688 allow_missing: true,
689 ..Default::default()
690 };
691 let mut written = None;
692 let report = configure_runtime_with_env([first.clone(), second], &options, None, |path| {
693 written = Some(path.to_path_buf())
694 })
695 .unwrap();
696 assert_eq!(report.status, RuntimeBindingStatus::Missing);
697 assert_eq!(report.selection.path, first.path);
698 assert_eq!(written, Some(first.path.clone()));
699 assert_eq!(report.selection.diagnostics.len(), 2);
700 assert!(report
701 .selection
702 .diagnostics
703 .iter()
704 .all(|d| !d.exists && !d.is_file));
705 let repeated =
706 configure_runtime_with_env([first.clone()], &options, Some(first.path), |_| {
707 panic!("matching binding must be preserved")
708 })
709 .unwrap();
710 assert_eq!(repeated.status, RuntimeBindingStatus::Missing);
711 }
712
713 #[test]
714 fn missing_mode_prefers_available_files_and_never_binds_directories() {
715 let fixture = Fixture::new();
716 let available = fixture.candidate("available", true);
717 let options = RuntimeBindingOptions {
718 allow_missing: true,
719 ..Default::default()
720 };
721 let report = configure_runtime_with_env(
722 [fixture.candidate("absent", false), available.clone()],
723 &options,
724 None,
725 |_| {},
726 )
727 .unwrap();
728 assert_eq!(report.status, RuntimeBindingStatus::Ready);
729 assert_eq!(
730 report.selection.path,
731 available.path.canonicalize().unwrap()
732 );
733 let directory = fixture.candidate("directory", false);
734 std::fs::create_dir_all(&directory.path).unwrap();
735 let error = configure_runtime_with_env([directory], &options, None, |_| {
736 panic!("directory must not be bound")
737 })
738 .unwrap_err();
739 assert!(matches!(error, RuntimeError::NotFound { .. }));
740 }
741
742 #[test]
743 fn forced_missing_binding_overwrites_existing_configuration() {
744 let fixture = Fixture::new();
745 let previous = fixture.candidate("previous", true);
746 let missing = fixture.candidate("missing", false);
747 let mut written = None;
748 let report = configure_runtime_with_env(
749 [missing.clone()],
750 &RuntimeBindingOptions {
751 force_replace: true,
752 allow_missing: true,
753 },
754 Some(previous.path),
755 |path| written = Some(path.to_path_buf()),
756 )
757 .unwrap();
758 assert_eq!(report.status, RuntimeBindingStatus::Missing);
759 assert_eq!(written, Some(missing.path));
760 }
761
762 #[test]
763 fn permissive_options_still_reject_unsafe_paths_and_system_runtime() {
764 let fixture = Fixture::new();
765 let options = RuntimeBindingOptions {
766 force_replace: true,
767 allow_missing: true,
768 };
769 for (candidate, expected) in [
770 (
771 RuntimeCandidate::new(runtime_filename(), CandidateSource::Explicit),
772 "relative",
773 ),
774 (
775 RuntimeCandidate::new(fixture.0.join("wrong.bin"), CandidateSource::Explicit),
776 "filename",
777 ),
778 (fixture.candidate("System32", false), "system"),
779 ] {
780 let error = configure_runtime_with_env([candidate], &options, None, |_| {
781 panic!("unsafe candidate must not be bound")
782 })
783 .unwrap_err();
784 assert!(match expected {
785 "relative" => matches!(error, RuntimeError::UnsafePath { .. }),
786 "filename" => matches!(error, RuntimeError::UnexpectedFilename { .. }),
787 _ => matches!(error, RuntimeError::SystemRuntimeRejected { .. }),
788 });
789 }
790 }
791}