Skip to main content

dora_core/
lib.rs

1//! **Internal to dora — not a public API.**
2//!
3//! This crate is published to crates.io only because cargo requires every
4//! dependency of a published crate to be published; `dora-node-api` and
5//! `dora-cli` depend on it. It is not covered by dora's 1.0 stability
6//! guarantee and may change in any release, including a patch.
7//!
8//! Depend on it directly at your own risk. See the "Stability scope at 1.0"
9//! section of `docs/api-rust.md`.
10//!
11use eyre::{Context, bail, eyre};
12use std::{
13    env::consts::{DLL_PREFIX, DLL_SUFFIX},
14    path::Path,
15};
16
17pub use dora_message::{config, uhlc};
18
19#[cfg(feature = "build")]
20pub mod build;
21pub mod descriptor;
22pub mod manifest;
23pub mod topics;
24pub mod types;
25
26/// Adjusts a shared library path by adding the platform-specific prefix and suffix.
27///
28/// Takes a base path (without platform-specific prefix and extension) and returns
29/// a path with the appropriate shared library naming conventions using
30/// [`DLL_PREFIX`] and [`DLL_SUFFIX`].
31///
32/// # Errors
33///
34/// Returns an error if the path has no file name, contains invalid UTF-8,
35/// already starts with `lib`, or already has an extension.
36///
37/// # Example
38///
39/// ```
40/// use std::path::Path;
41/// use dora_core::adjust_shared_library_path;
42///
43/// let adjusted = adjust_shared_library_path(Path::new("mylib")).unwrap();
44/// let expected = format!("{}mylib{}", std::env::consts::DLL_PREFIX, std::env::consts::DLL_SUFFIX);
45/// assert_eq!(adjusted.file_name().unwrap().to_str().unwrap(), expected);
46/// ```
47pub fn adjust_shared_library_path(path: &Path) -> Result<std::path::PathBuf, eyre::ErrReport> {
48    let file_name = path
49        .file_name()
50        .ok_or_else(|| eyre!("shared library path has no file name"))?
51        .to_str()
52        .ok_or_else(|| eyre!("shared library file name is not valid UTF8"))?;
53    if file_name.starts_with("lib") {
54        bail!("Shared library file name must not start with `lib`, prefix is added automatically");
55    }
56    if path.extension().is_some() {
57        bail!("Shared library file name must have no extension, it is added automatically");
58    }
59
60    let library_filename = format!("{DLL_PREFIX}{file_name}{DLL_SUFFIX}");
61
62    let path = path.with_file_name(library_filename);
63    Ok(path)
64}
65
66/// Locate a Python 3 interpreter, in order of preference:
67///
68/// 1. If `uv` is available, use `uv python find` to get the Python path.
69/// 2. Otherwise, try `python` and check it is not Python 2.
70/// 3. Fall back to `python3` if `python` is Python 2 (or absent).
71///
72/// Returns an error only if none of the above yields a usable Python 3.
73pub fn get_python_path() -> Result<std::path::PathBuf, eyre::ErrReport> {
74    // First, try using uv if available
75    if let Ok(uv_path) = get_uv_path() {
76        let output = std::process::Command::new(&uv_path)
77            .args(["python", "find"])
78            .output();
79
80        if let Ok(output) = output
81            && output.status.success()
82        {
83            let python_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
84            if !python_path.is_empty() {
85                let path = std::path::PathBuf::from(&python_path);
86                if path.exists() {
87                    return Ok(path);
88                }
89            }
90        }
91    }
92
93    // Fall back to finding python directly
94    if let Ok(python) = which::which("python") {
95        // Check if it's Python 2
96        if !is_python2(&python) {
97            return Ok(python);
98        }
99    }
100
101    // Fall back to python3
102    which::which("python3").context(
103        "failed to find a valid Python 3 installation. Make sure that python3 is available.",
104    )
105}
106
107fn is_python2(python_path: &std::path::Path) -> bool {
108    let output = std::process::Command::new(python_path)
109        .args(["--version"])
110        .output();
111
112    match output {
113        Ok(output) => {
114            // Python 2 prints version to stderr, Python 3 to stdout
115            let version = if output.stdout.is_empty() {
116                String::from_utf8_lossy(&output.stderr)
117            } else {
118                String::from_utf8_lossy(&output.stdout)
119            };
120            version.starts_with("Python 2")
121        }
122        Err(_) => false,
123    }
124}
125
126/// Locate the `uv` binary on `PATH`, erroring with an install hint if it is
127/// not found.
128pub fn get_uv_path() -> Result<std::path::PathBuf, eyre::ErrReport> {
129    which::which("uv")
130        .context("failed to find `uv`. Make sure to install it using: https://docs.astral.sh/uv/getting-started/installation/")
131}