use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use super::not_found::not_found_error;
use crate::error::{EngineError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgramKind {
Native,
CmdShim,
PowerShellShim,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub program: PathBuf,
pub kind: ProgramKind,
}
pub fn resolve(program: &OsStr, cwd: &Path) -> Result<Resolved> {
let as_path = Path::new(program);
let has_separator = as_path.components().any(|c| {
matches!(
c,
std::path::Component::RootDir | std::path::Component::ParentDir
)
}) || program.to_string_lossy().contains(['/', '\\']);
let path = if has_separator {
let candidate = if as_path.is_absolute() {
as_path.to_path_buf()
} else {
cwd.join(as_path)
};
if candidate.is_file() {
candidate
} else {
return Err(EngineError::not_found(format!(
"cannot find {}: there is no file at {}.",
as_path.display(),
candidate.display()
)));
}
} else {
which::which(program).map_err(|_| not_found_error(program))?
};
Ok(Resolved {
kind: kind_of(&path),
program: path,
})
}
fn kind_of(path: &Path) -> ProgramKind {
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
match ext.as_str() {
"cmd" | "bat" => ProgramKind::CmdShim,
"ps1" => ProgramKind::PowerShellShim,
_ => ProgramKind::Native,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exe_and_extensionless_are_native() {
assert_eq!(kind_of(Path::new("C:/x/cargo.exe")), ProgramKind::Native);
assert_eq!(kind_of(Path::new("/usr/bin/cargo")), ProgramKind::Native);
assert_eq!(kind_of(Path::new("C:/x/tool.bin")), ProgramKind::Native);
}
#[test]
fn cmd_and_bat_are_shims() {
assert_eq!(kind_of(Path::new("C:/x/pnpm.cmd")), ProgramKind::CmdShim);
assert_eq!(kind_of(Path::new("C:/x/old.bat")), ProgramKind::CmdShim);
}
#[test]
fn extension_matching_is_case_insensitive() {
assert_eq!(kind_of(Path::new("C:/x/PNPM.CMD")), ProgramKind::CmdShim);
assert_eq!(
kind_of(Path::new("C:/x/Run.Ps1")),
ProgramKind::PowerShellShim
);
}
#[test]
fn ps1_needs_powershell() {
assert_eq!(
kind_of(Path::new("C:/x/x.ps1")),
ProgramKind::PowerShellShim
);
}
#[test]
fn an_explicit_path_is_used_as_is() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("thing");
std::fs::write(&p, "").unwrap();
let r = resolve(p.as_os_str(), tmp.path()).unwrap();
assert_eq!(r.program, p);
}
#[test]
fn a_relative_program_is_resolved_against_the_given_directory() {
let tmp = tempfile::tempdir().unwrap();
let work = tmp.path().join("work");
std::fs::create_dir_all(&work).unwrap();
std::fs::write(work.join("tool"), "").unwrap();
let r = resolve(OsStr::new("./tool"), &work).unwrap();
assert!(r.program.is_file(), "{:?}", r.program);
assert_eq!(r.program.file_name().unwrap(), "tool");
let elsewhere = tmp.path().join("elsewhere");
std::fs::create_dir_all(&elsewhere).unwrap();
assert!(resolve(OsStr::new("./tool"), &elsewhere).is_err());
}
#[test]
fn an_explicit_path_that_does_not_exist_is_an_error() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("nope").join("thing");
let err = resolve(p.as_os_str(), tmp.path()).unwrap_err();
assert!(
err.is_not_found(),
"a missing program is the setup kind: {err:?}"
);
assert!(
err.to_string().contains("nope"),
"the message should name what it looked for: {err}"
);
}
#[test]
fn resolves_a_real_program_from_path() {
let tmp = tempfile::tempdir().unwrap();
let r = resolve(OsStr::new("cargo"), tmp.path()).expect("cargo must be on PATH");
assert!(r.program.is_absolute(), "{:?}", r.program);
}
#[test]
fn a_missing_program_is_reported_as_not_found() {
let tmp = tempfile::tempdir().unwrap();
let err = resolve(
OsStr::new("pmpx-definitely-not-a-real-program-xyz"),
tmp.path(),
)
.unwrap_err();
assert!(
err.is_not_found(),
"a missing program is the setup kind: {err:?}"
);
}
#[cfg(windows)]
#[test]
fn which_does_pathex_resolution() {
let tmp = tempfile::tempdir().unwrap();
let shim = tmp.path().join("pmpxprobe.cmd");
std::fs::write(&shim, "@echo off\r\n").unwrap();
let found = which::which_in("pmpxprobe", Some(tmp.path()), tmp.path())
.expect("which should find the .cmd via PATHEXT");
assert_eq!(
found
.file_name()
.unwrap()
.to_string_lossy()
.to_ascii_lowercase(),
"pmpxprobe.cmd"
);
assert_eq!(kind_of(&found), ProgramKind::CmdShim);
}
}