use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::tscircuit::bun::resolve_bun;
use crate::tscircuit::error::RunnerError;
use crate::tscircuit::{Runner, RunnerKind};
pub fn detect_runner(current_dir: &Path, path_var: Option<&OsStr>) -> Result<Runner, RunnerError> {
if let Some(local_path) = find_local_tsci(current_dir) {
return Ok(Runner::new(RunnerKind::Local(local_path)));
}
if let Some(path_var) = path_var {
if let Some(global_path) = find_binary_in_path("tsci", path_var)
.or_else(|| find_binary_in_path("tscircuit", path_var))
{
return Ok(Runner::new(RunnerKind::Global(global_path)));
}
}
let env_bun = std::env::var_os("FUSIBLE_BUN");
match resolve_bun(env_bun.as_deref(), path_var) {
Some(bun) => Ok(Runner::new(RunnerKind::Bun(bun))),
None => Err(RunnerError::NotFound),
}
}
fn find_local_tsci(dir: &Path) -> Option<PathBuf> {
let dirs = [
dir.join("node_modules").join(".bin"),
dir.join("hardware").join("node_modules").join(".bin"),
];
for bin_dir in dirs {
let candidates = [
bin_dir.join("tsci"),
bin_dir.join("tsci.cmd"),
bin_dir.join("tsci.exe"),
bin_dir.join("tscircuit"),
bin_dir.join("tscircuit.cmd"),
bin_dir.join("tscircuit.exe"),
];
if let Some(found) = candidates.into_iter().find(|c| is_executable(c)) {
return Some(found);
}
}
None
}
pub(crate) fn find_binary_in_path(binary: &str, path_var: &OsStr) -> Option<PathBuf> {
std::env::split_paths(path_var).find_map(|dir| {
let candidate = dir.join(binary);
if is_executable(&candidate) {
return Some(candidate);
}
find_windows_binary(&dir, binary)
})
}
fn find_windows_binary(dir: &Path, binary: &str) -> Option<PathBuf> {
#[cfg(windows)]
{
for ext in &[".cmd", ".exe", ".bat"] {
let candidate_ext = dir.join(format!("{binary}{ext}"));
if is_executable(&candidate_ext) {
return Some(candidate_ext);
}
}
}
#[cfg(not(windows))]
let _ = (dir, binary);
None
}
#[cfg(unix)]
pub(crate) fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
if !path.is_file() {
return false;
}
path.metadata()
.map(|meta| meta.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
pub(crate) fn is_executable(path: &Path) -> bool {
path.is_file()
&& path.extension().is_some_and(|ext| {
["exe", "cmd", "bat", "com"]
.iter()
.any(|known| ext.eq_ignore_ascii_case(known))
})
}
pub(crate) fn resolve_program(name: &str) -> PathBuf {
std::env::var_os("PATH")
.and_then(|path| find_binary_in_path(name, &path))
.unwrap_or_else(|| PathBuf::from(name))
}