1use std::ffi::OsStr;
13use std::path::{Path, PathBuf};
14
15use super::not_found::not_found_error;
16use crate::error::{EngineError, Result};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ProgramKind {
21 Native,
23 CmdShim,
25 PowerShellShim,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Resolved {
32 pub program: PathBuf,
34 pub kind: ProgramKind,
36}
37
38pub fn resolve(program: &OsStr, cwd: &Path) -> Result<Resolved> {
46 let as_path = Path::new(program);
47
48 let has_separator = as_path.components().any(|c| {
49 matches!(
50 c,
51 std::path::Component::RootDir | std::path::Component::ParentDir
52 )
53 }) || program.to_string_lossy().contains(['/', '\\']);
54
55 let path = if has_separator {
56 let candidate = if as_path.is_absolute() {
57 as_path.to_path_buf()
58 } else {
59 cwd.join(as_path)
60 };
61
62 if candidate.is_file() {
63 candidate
64 } else {
65 return Err(EngineError::not_found(format!(
66 "cannot find {}: there is no file at {}.",
67 as_path.display(),
68 candidate.display()
69 )));
70 }
71 } else {
72 which::which(program).map_err(|_| not_found_error(program))?
73 };
74
75 Ok(Resolved {
76 kind: kind_of(&path),
77 program: path,
78 })
79}
80
81fn kind_of(path: &Path) -> ProgramKind {
83 let ext = path
84 .extension()
85 .map(|e| e.to_string_lossy().to_ascii_lowercase())
86 .unwrap_or_default();
87
88 match ext.as_str() {
89 "cmd" | "bat" => ProgramKind::CmdShim,
90 "ps1" => ProgramKind::PowerShellShim,
91 _ => ProgramKind::Native,
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
102 fn exe_and_extensionless_are_native() {
103 assert_eq!(kind_of(Path::new("C:/x/cargo.exe")), ProgramKind::Native);
104 assert_eq!(kind_of(Path::new("/usr/bin/cargo")), ProgramKind::Native);
105 assert_eq!(kind_of(Path::new("C:/x/tool.bin")), ProgramKind::Native);
106 }
107
108 #[test]
109 fn cmd_and_bat_are_shims() {
110 assert_eq!(kind_of(Path::new("C:/x/pnpm.cmd")), ProgramKind::CmdShim);
111 assert_eq!(kind_of(Path::new("C:/x/old.bat")), ProgramKind::CmdShim);
112 }
113
114 #[test]
117 fn extension_matching_is_case_insensitive() {
118 assert_eq!(kind_of(Path::new("C:/x/PNPM.CMD")), ProgramKind::CmdShim);
119 assert_eq!(
120 kind_of(Path::new("C:/x/Run.Ps1")),
121 ProgramKind::PowerShellShim
122 );
123 }
124
125 #[test]
126 fn ps1_needs_powershell() {
127 assert_eq!(
128 kind_of(Path::new("C:/x/x.ps1")),
129 ProgramKind::PowerShellShim
130 );
131 }
132
133 #[test]
138 fn an_explicit_path_is_used_as_is() {
139 let tmp = tempfile::tempdir().unwrap();
140 let p = tmp.path().join("thing");
141 std::fs::write(&p, "").unwrap();
142
143 let r = resolve(p.as_os_str(), tmp.path()).unwrap();
144 assert_eq!(r.program, p);
145 }
146
147 #[test]
151 fn a_relative_program_is_resolved_against_the_given_directory() {
152 let tmp = tempfile::tempdir().unwrap();
153 let work = tmp.path().join("work");
154 std::fs::create_dir_all(&work).unwrap();
155 std::fs::write(work.join("tool"), "").unwrap();
156
157 let r = resolve(OsStr::new("./tool"), &work).unwrap();
158 assert!(r.program.is_file(), "{:?}", r.program);
159 assert_eq!(r.program.file_name().unwrap(), "tool");
160
161 let elsewhere = tmp.path().join("elsewhere");
164 std::fs::create_dir_all(&elsewhere).unwrap();
165 assert!(resolve(OsStr::new("./tool"), &elsewhere).is_err());
166 }
167
168 #[test]
169 fn an_explicit_path_that_does_not_exist_is_an_error() {
170 let tmp = tempfile::tempdir().unwrap();
171 let p = tmp.path().join("nope").join("thing");
172
173 let err = resolve(p.as_os_str(), tmp.path()).unwrap_err();
174 assert!(
175 err.is_not_found(),
176 "a missing program is the setup kind: {err:?}"
177 );
178 assert!(
179 err.to_string().contains("nope"),
180 "the message should name what it looked for: {err}"
181 );
182 }
183
184 #[test]
186 fn resolves_a_real_program_from_path() {
187 let tmp = tempfile::tempdir().unwrap();
188 let r = resolve(OsStr::new("cargo"), tmp.path()).expect("cargo must be on PATH");
189 assert!(r.program.is_absolute(), "{:?}", r.program);
190 }
191
192 #[test]
193 fn a_missing_program_is_reported_as_not_found() {
194 let tmp = tempfile::tempdir().unwrap();
195 let err = resolve(
196 OsStr::new("pmpx-definitely-not-a-real-program-xyz"),
197 tmp.path(),
198 )
199 .unwrap_err();
200 assert!(
201 err.is_not_found(),
202 "a missing program is the setup kind: {err:?}"
203 );
204 }
205
206 #[cfg(windows)]
213 #[test]
214 fn which_does_pathex_resolution() {
215 let tmp = tempfile::tempdir().unwrap();
216 let shim = tmp.path().join("pmpxprobe.cmd");
217 std::fs::write(&shim, "@echo off\r\n").unwrap();
218
219 let found = which::which_in("pmpxprobe", Some(tmp.path()), tmp.path())
220 .expect("which should find the .cmd via PATHEXT");
221
222 assert_eq!(
223 found
224 .file_name()
225 .unwrap()
226 .to_string_lossy()
227 .to_ascii_lowercase(),
228 "pmpxprobe.cmd"
229 );
230 assert_eq!(kind_of(&found), ProgramKind::CmdShim);
231 }
232}