Skip to main content

rightkit_ort/
lib.rs

1//! Product-neutral ONNX Runtime dynamic-library resolution, environment and
2//! session setup for Right Suite apps.
3//!
4//! Merged from ScrapeRight `ort_common` (installed-bundle layout, system-DLL
5//! hazard) and HeardRight `heardright-onnx-asr` (environment, execution
6//! providers, session builder). Product environment variables and data roots
7//! stay in app adapters.
8
9#[cfg(feature = "session")]
10pub mod environment;
11#[cfg(feature = "session")]
12pub mod probe;
13#[cfg(feature = "session")]
14pub mod session;
15
16#[cfg(feature = "session")]
17pub use environment::{
18    cpu_thread_budget, init_environment, shared_pool_active, EnvironmentOptions, EnvironmentReport,
19    GlobalPool,
20};
21#[cfg(feature = "session")]
22pub use probe::{probe_providers, ProviderDiagnostic, ProviderKind};
23#[cfg(feature = "session")]
24pub use session::{BuiltSession, ExecutionProvider, SessionError, SessionOptions};
25
26use std::fmt;
27use std::path::{Path, PathBuf};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum CandidateSource {
31    Explicit,
32    Bundled,
33    AppData,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct RuntimeCandidate {
38    pub path: PathBuf,
39    pub source: CandidateSource,
40}
41
42impl RuntimeCandidate {
43    pub fn new(path: impl Into<PathBuf>, source: CandidateSource) -> Self {
44        Self {
45            path: path.into(),
46            source,
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct CandidateDiagnostic {
53    pub path: PathBuf,
54    pub source: CandidateSource,
55    pub absolute: bool,
56    pub expected_filename: bool,
57    pub exists: bool,
58    pub is_file: bool,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct RuntimeSelection {
63    pub path: PathBuf,
64    pub source: CandidateSource,
65    pub diagnostics: Vec<CandidateDiagnostic>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum RuntimeError {
70    NoCandidates,
71    UnsafePath {
72        path: PathBuf,
73    },
74    UnexpectedFilename {
75        path: PathBuf,
76        expected: &'static str,
77    },
78    NotFound {
79        diagnostics: Vec<CandidateDiagnostic>,
80    },
81    Canonicalize {
82        path: PathBuf,
83        message: String,
84    },
85    AlreadyConfigured {
86        configured: PathBuf,
87        selected: PathBuf,
88    },
89    /// The Windows-ML copy in `System32` hangs at session init; apps must
90    /// supply their bundled runtime instead.
91    SystemRuntimeRejected {
92        path: PathBuf,
93    },
94}
95
96impl fmt::Display for RuntimeError {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::NoCandidates => write!(f, "no ONNX Runtime candidates were supplied"),
100            Self::UnsafePath { path } => {
101                write!(f, "runtime path must be absolute: {}", path.display())
102            }
103            Self::UnexpectedFilename { path, expected } => {
104                write!(f, "runtime path {} must end in {expected}", path.display())
105            }
106            Self::NotFound { .. } => {
107                write!(f, "no supplied ONNX Runtime candidate is a regular file")
108            }
109            Self::Canonicalize { path, message } => {
110                write!(f, "cannot canonicalize {}: {message}", path.display())
111            }
112            Self::SystemRuntimeRejected { path } => write!(
113                f,
114                "runtime {} is the OS-provided copy; bundle and supply an app runtime",
115                path.display()
116            ),
117            Self::AlreadyConfigured {
118                configured,
119                selected,
120            } => write!(
121                f,
122                "ORT_DYLIB_PATH is already {}, refusing replacement with {}",
123                configured.display(),
124                selected.display()
125            ),
126        }
127    }
128}
129
130impl std::error::Error for RuntimeError {}
131
132pub fn runtime_filename() -> &'static str {
133    #[cfg(target_os = "windows")]
134    {
135        "onnxruntime.dll"
136    }
137    #[cfg(target_os = "macos")]
138    {
139        "libonnxruntime.dylib"
140    }
141    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
142    {
143        "libonnxruntime.so"
144    }
145}
146
147pub fn inspect_candidate(candidate: &RuntimeCandidate) -> CandidateDiagnostic {
148    CandidateDiagnostic {
149        path: candidate.path.clone(),
150        source: candidate.source,
151        absolute: candidate.path.is_absolute(),
152        expected_filename: candidate.path.file_name().and_then(|name| name.to_str())
153            == Some(runtime_filename()),
154        exists: candidate.path.exists(),
155        is_file: candidate.path.is_file(),
156    }
157}
158
159pub fn resolve_runtime(
160    candidates: impl IntoIterator<Item = RuntimeCandidate>,
161) -> Result<RuntimeSelection, RuntimeError> {
162    let candidates: Vec<_> = candidates.into_iter().collect();
163    if candidates.is_empty() {
164        return Err(RuntimeError::NoCandidates);
165    }
166    let mut diagnostics = Vec::with_capacity(candidates.len());
167    for candidate in candidates {
168        let diagnostic = inspect_candidate(&candidate);
169        if !diagnostic.absolute {
170            return Err(RuntimeError::UnsafePath {
171                path: candidate.path,
172            });
173        }
174        if !diagnostic.expected_filename {
175            return Err(RuntimeError::UnexpectedFilename {
176                path: candidate.path,
177                expected: runtime_filename(),
178            });
179        }
180        if is_system_runtime(&candidate.path) {
181            return Err(RuntimeError::SystemRuntimeRejected {
182                path: candidate.path,
183            });
184        }
185        diagnostics.push(diagnostic.clone());
186        if diagnostic.is_file {
187            let path =
188                candidate
189                    .path
190                    .canonicalize()
191                    .map_err(|error| RuntimeError::Canonicalize {
192                        path: candidate.path,
193                        message: error.to_string(),
194                    })?;
195            return Ok(RuntimeSelection {
196                path,
197                source: candidate.source,
198                diagnostics,
199            });
200        }
201    }
202    Err(RuntimeError::NotFound { diagnostics })
203}
204
205/// True when the path sits under a Windows `System32`/`SysWOW64` directory.
206/// The OS copy of `onnxruntime.dll` (Windows ML) hangs at session init, so it
207/// is never an acceptable candidate (ScrapeRight `ort_common` finding).
208pub fn is_system_runtime(path: &Path) -> bool {
209    path.components().any(|component| {
210        component
211            .as_os_str()
212            .to_str()
213            .map(|name| {
214                name.eq_ignore_ascii_case("system32") || name.eq_ignore_ascii_case("syswow64")
215            })
216            .unwrap_or(false)
217    })
218}
219
220/// Resources directory of an installed application bundle, derived from its
221/// executable only (no checkout or working-directory fallback):
222/// macOS `Foo.app/Contents/MacOS/foo` -> `Contents/Resources`; Windows the
223/// executable directory; elsewhere `<exe dir>/resources`.
224pub fn installed_resource_dir(executable: &Path) -> Option<PathBuf> {
225    if !executable.is_absolute() {
226        return None;
227    }
228    let dir = executable.parent()?.to_path_buf();
229    let is = |p: &Path, name: &str| {
230        p.file_name()
231            .and_then(|n| n.to_str())
232            .map(|n| n.eq_ignore_ascii_case(name))
233            .unwrap_or(false)
234    };
235    #[cfg(target_os = "macos")]
236    {
237        if is(&dir, "MacOS") {
238            if let Some(contents) = dir.parent() {
239                if is(contents, "Contents") {
240                    return Some(contents.join("Resources"));
241                }
242            }
243        }
244        if is(&dir, "Resources") {
245            return Some(dir);
246        }
247        Some(dir.join("Resources"))
248    }
249    #[cfg(target_os = "windows")]
250    {
251        let _ = is;
252        Some(dir)
253    }
254    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
255    {
256        if is(&dir, "resources") {
257            Some(dir)
258        } else {
259            Some(dir.join("resources"))
260        }
261    }
262}
263
264/// Bundled-runtime candidate for an installed app: `<resources>/<subdir>/<runtime file>`.
265/// Apps pass their own `subdir` (ScrapeRight/HeardRight/CodeRight all use `runtime`).
266pub fn installed_runtime_candidate(executable: &Path, subdir: &str) -> Option<RuntimeCandidate> {
267    let dir = installed_resource_dir(executable)?;
268    Some(RuntimeCandidate::new(
269        dir.join(subdir).join(runtime_filename()),
270        CandidateSource::Bundled,
271    ))
272}
273
274/// Configure ORT only after resolving a caller-supplied, absolute regular file.
275/// Existing configuration is preserved and must canonicalize to the same file.
276pub fn configure_runtime(
277    candidates: impl IntoIterator<Item = RuntimeCandidate>,
278) -> Result<RuntimeSelection, RuntimeError> {
279    let selection = resolve_runtime(candidates)?;
280    if let Some(configured) = nonempty_env_path("ORT_DYLIB_PATH") {
281        let configured = canonical_if_file(&configured)?;
282        if configured != selection.path {
283            return Err(RuntimeError::AlreadyConfigured {
284                configured,
285                selected: selection.path,
286            });
287        }
288        return Ok(selection);
289    }
290    std::env::set_var("ORT_DYLIB_PATH", &selection.path);
291    Ok(selection)
292}
293
294fn nonempty_env_path(name: &str) -> Option<PathBuf> {
295    std::env::var_os(name)
296        .filter(|value| !value.is_empty())
297        .map(PathBuf::from)
298}
299
300fn canonical_if_file(path: &Path) -> Result<PathBuf, RuntimeError> {
301    if !path.is_absolute() {
302        return Err(RuntimeError::UnsafePath {
303            path: path.to_path_buf(),
304        });
305    }
306    path.canonicalize()
307        .map_err(|error| RuntimeError::Canonicalize {
308            path: path.to_path_buf(),
309            message: error.to_string(),
310        })
311}