use std::process::Command;
use std::sync::OnceLock;
pub const PYTHON_CANDIDATES: &[&str] = &["python3", "python", "py -3"];
#[must_use]
pub fn probe_executable(spec: &str) -> bool {
let mut parts = spec.split_whitespace();
let Some(program) = parts.next() else {
return false;
};
let mut cmd = Command::new(program);
for arg in parts {
cmd.arg(arg);
}
cmd.arg("--version");
cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null());
matches!(cmd.status(), Ok(status) if status.success())
}
pub fn resolve_python_interpreter() -> Option<String> {
static CACHE: OnceLock<Option<String>> = OnceLock::new();
CACHE
.get_or_init(|| {
for candidate in PYTHON_CANDIDATES {
if probe_executable(candidate) {
tracing::info!(
target: "tool_dependencies",
candidate = candidate,
"Resolved Python interpreter for code_execution",
);
return Some((*candidate).to_string());
}
}
tracing::warn!(
target: "tool_dependencies",
tried = ?PYTHON_CANDIDATES,
"No Python interpreter found; code_execution tool will not be advertised",
);
None
})
.clone()
}
pub fn resolve_pdftotext() -> Option<String> {
static CACHE: OnceLock<Option<String>> = OnceLock::new();
CACHE
.get_or_init(|| {
if probe_executable("pdftotext") {
Some("pdftotext".to_string())
} else {
None
}
})
.clone()
}
#[must_use]
pub fn split_interpreter_spec(spec: &str) -> (String, Vec<String>) {
let mut parts = spec.split_whitespace();
let program = parts.next().unwrap_or("").to_string();
let args = parts.map(str::to_string).collect();
(program, args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_executable_returns_false_for_unknown_binary() {
assert!(!probe_executable("deepseek-tui-imaginary-binary-xyz123"));
}
#[test]
fn probe_executable_handles_multi_word_specs() {
let _ = probe_executable("py -3");
}
#[test]
fn split_interpreter_spec_strips_args() {
assert_eq!(
split_interpreter_spec("python3"),
("python3".to_string(), Vec::<String>::new())
);
assert_eq!(
split_interpreter_spec("py -3"),
("py".to_string(), vec!["-3".to_string()])
);
assert_eq!(
split_interpreter_spec(" python3 "),
("python3".to_string(), Vec::<String>::new()),
"leading/trailing whitespace must be tolerated"
);
}
#[test]
fn split_interpreter_spec_handles_empty_string() {
assert_eq!(
split_interpreter_spec(""),
(String::new(), Vec::<String>::new())
);
}
#[test]
fn python_resolver_is_cached_across_calls() {
let first = resolve_python_interpreter();
let second = resolve_python_interpreter();
assert_eq!(first, second);
}
#[test]
fn python_resolver_returns_some_on_developer_machines() {
let resolved = resolve_python_interpreter();
if let Some(name) = resolved {
assert!(
!name.is_empty(),
"resolved interpreter name must be non-empty"
);
assert!(
PYTHON_CANDIDATES.contains(&name.as_str()),
"resolved {name:?} is not in PYTHON_CANDIDATES {PYTHON_CANDIDATES:?}"
);
}
}
}