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