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