1use 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
26pub 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
35pub fn reset_binary_cache() {
37 let mut state = STATE.lock().unwrap();
38 state.override_path = None;
39 state.resolved = None;
40}
41
42fn repo_root() -> Option<&'static Path> {
48 Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2)
49}
50
51fn 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
82fn 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 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
112pub 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 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 if let Ok(found) = resolve_binary() {
185 assert_ne!(found, "/definitely/not/here/bsdkrun");
186 }
187 reset_binary_cache();
188 }
189}