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