Skip to main content

bsdkrun_sdk/
binary.rs

1//! Locate the `bsdkrun` binary on the host.
2//!
3//! Resolution order (first match wins, then cached):
4//!
5//! 1. an explicit override set via [`set_binary_path`],
6//! 2. the `BSDKRUN_BIN` environment variable,
7//! 3. `bsdkrun` on `PATH`,
8//! 4. in-repo dev builds relative to this crate's source:
9//!    `<repo_root>/target/release/bsdkrun` then `.../target/debug/bsdkrun`.
10
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14use crate::error::{Error, Result};
15
16struct BinState {
17    override_path: Option<String>,
18    resolved: Option<String>,
19}
20
21static STATE: Mutex<BinState> = Mutex::new(BinState {
22    override_path: None,
23    resolved: None,
24});
25
26/// Force the SDK to use a specific `bsdkrun` binary, bypassing discovery.
27///
28/// Handy in tests or when running against a locally built debug binary.
29pub fn set_binary_path(path: impl Into<String>) {
30    let mut state = STATE.lock().unwrap();
31    state.override_path = Some(path.into());
32    state.resolved = None;
33}
34
35/// Reset cached discovery state and any override (mainly for tests).
36pub fn reset_binary_cache() {
37    let mut state = STATE.lock().unwrap();
38    state.override_path = None;
39    state.resolved = None;
40}
41
42/// The repo root when this crate is built from a checkout: the manifest lives
43/// at `<repo>/sdk/rust`, so two levels up. For a crate pulled from a registry
44/// this points into the cargo cache, where the `target/` candidates simply
45/// fail their `exists()` check — same effect as Python's `__file__`-relative
46/// lookup outside a checkout.
47fn repo_root() -> Option<&'static Path> {
48    Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2)
49}
50
51/// A minimal `which`: the first `PATH` entry holding an executable `bsdkrun`.
52fn which(name: &str) -> Option<PathBuf> {
53    let path_var = std::env::var_os("PATH")?;
54    for dir in std::env::split_paths(&path_var) {
55        if dir.as_os_str().is_empty() {
56            continue;
57        }
58        let candidate = dir.join(name);
59        if is_executable_file(&candidate) {
60            return Some(candidate);
61        }
62    }
63    None
64}
65
66fn is_executable_file(path: &Path) -> bool {
67    let Ok(metadata) = path.metadata() else {
68        return false;
69    };
70    if !metadata.is_file() {
71        return false;
72    }
73    #[cfg(unix)]
74    {
75        use std::os::unix::fs::PermissionsExt;
76        metadata.permissions().mode() & 0o111 != 0
77    }
78    #[cfg(not(unix))]
79    true
80}
81
82/// Candidate locations, in priority order.
83fn candidates(override_path: Option<&str>) -> Vec<String> {
84    let mut out = Vec::new();
85    if let Some(explicit) = override_path {
86        out.push(explicit.to_string());
87    }
88    if let Ok(env) = std::env::var("BSDKRUN_BIN") {
89        if !env.is_empty() {
90            out.push(env);
91        }
92    }
93    // A `bsdkrun` already on PATH wins over in-repo builds.
94    if let Some(on_path) = which("bsdkrun") {
95        out.push(on_path.to_string_lossy().into_owned());
96    }
97    if let Some(root) = repo_root() {
98        out.push(
99            root.join("target/release/bsdkrun")
100                .to_string_lossy()
101                .into_owned(),
102        );
103        out.push(
104            root.join("target/debug/bsdkrun")
105                .to_string_lossy()
106                .into_owned(),
107        );
108    }
109    out
110}
111
112/// Resolve (and cache) the path to the `bsdkrun` binary.
113///
114/// Returns [`Error::BinaryNotFound`] if none of the candidate locations exist.
115pub fn resolve_binary() -> Result<String> {
116    let mut state = STATE.lock().unwrap();
117    if let Some(resolved) = &state.resolved {
118        return Ok(resolved.clone());
119    }
120
121    let searched = candidates(state.override_path.as_deref());
122    for candidate in &searched {
123        if Path::new(candidate).exists() {
124            state.resolved = Some(candidate.clone());
125            return Ok(candidate.clone());
126        }
127    }
128    Err(Error::BinaryNotFound { searched })
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    // Discovery reads process-global state (the override cache and env vars),
136    // so the tests here serialize on one lock instead of racing each other.
137    static TEST_LOCK: Mutex<()> = Mutex::new(());
138
139    fn touch_executable(name: &str) -> PathBuf {
140        let path =
141            std::env::temp_dir().join(format!("bsdkrun-sdk-test-{name}-{}", std::process::id()));
142        std::fs::write(&path, "#!/bin/sh\n").unwrap();
143        #[cfg(unix)]
144        {
145            use std::os::unix::fs::PermissionsExt;
146            std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
147        }
148        path
149    }
150
151    #[test]
152    fn explicit_override_wins() {
153        let _guard = TEST_LOCK.lock().unwrap();
154        let fake = touch_executable("override");
155        set_binary_path(fake.to_string_lossy().into_owned());
156        assert_eq!(resolve_binary().unwrap(), fake.to_string_lossy());
157        reset_binary_cache();
158        std::fs::remove_file(&fake).ok();
159    }
160
161    #[test]
162    fn env_var_wins_when_no_override_is_set() {
163        let _guard = TEST_LOCK.lock().unwrap();
164        let fake = touch_executable("env");
165        let saved = std::env::var("BSDKRUN_BIN").ok();
166        reset_binary_cache();
167        std::env::set_var("BSDKRUN_BIN", &fake);
168        assert_eq!(resolve_binary().unwrap(), fake.to_string_lossy());
169        match saved {
170            Some(v) => std::env::set_var("BSDKRUN_BIN", v),
171            None => std::env::remove_var("BSDKRUN_BIN"),
172        }
173        reset_binary_cache();
174        std::fs::remove_file(&fake).ok();
175    }
176
177    #[test]
178    fn missing_override_falls_through_to_later_candidates() {
179        let _guard = TEST_LOCK.lock().unwrap();
180        set_binary_path("/definitely/not/here/bsdkrun");
181        // In this repo a target/release build usually exists, so resolution
182        // may still succeed — the assertion is only that the bogus override
183        // never wins.
184        if let Ok(found) = resolve_binary() {
185            assert_ne!(found, "/definitely/not/here/bsdkrun");
186        }
187        reset_binary_cache();
188    }
189}