1use std::path::{Path, PathBuf};
9
10pub(crate) fn resolve_on_path(binary: &str) -> Option<PathBuf> {
12 if let Ok(path) = which::which(binary) {
13 return Some(path);
14 }
15 find_on_path_manual(binary)
16}
17
18pub(crate) fn find_on_path_manual(binary: &str) -> Option<PathBuf> {
20 let path_var = std::env::var_os("PATH")?;
21 for dir in std::env::split_paths(&path_var) {
22 if dir.as_os_str().is_empty() {
23 continue;
24 }
25 if let Some(found) = probe_tool_in_dir(&dir, binary) {
26 return Some(found);
27 }
28 }
29 None
30}
31
32fn path_looks_like_tool(path: &Path) -> bool {
33 let Ok(metadata) = std::fs::metadata(path) else {
34 return false;
35 };
36 if !metadata.is_file() {
37 return false;
38 }
39 #[cfg(unix)]
40 {
41 use std::os::unix::fs::PermissionsExt;
42 metadata.permissions().mode() & 0o111 != 0
43 }
44 #[cfg(not(unix))]
45 {
46 let _ = metadata;
47 true
48 }
49}
50
51pub(crate) fn probe_tool_in_dir(dir: &Path, binary: &str) -> Option<PathBuf> {
53 if !dir.is_dir() {
54 return None;
55 }
56
57 let direct = dir.join(binary);
58 if path_looks_like_tool(&direct) {
59 return Some(direct);
60 }
61
62 if cfg!(windows) {
63 for ext in ["exe", "cmd", "bat", "com"] {
64 let candidate = dir.join(format!("{binary}.{ext}"));
65 if path_looks_like_tool(&candidate) {
66 return Some(candidate);
67 }
68 }
69 }
70
71 None
72}
73
74#[cfg(windows)]
76pub(crate) fn well_known_windows_bin_dirs(userprofile: Option<&std::ffi::OsStr>) -> Vec<PathBuf> {
77 let mut dirs: Vec<PathBuf> = Vec::with_capacity(10);
78 dirs.push(PathBuf::from(r"C:\Go\bin"));
79 dirs.push(PathBuf::from(r"C:\Program Files\Go\bin"));
80 dirs.push(PathBuf::from(r"C:\Program Files\nodejs"));
81 if let Some(appdata) = std::env::var_os("APPDATA") {
82 dirs.push(PathBuf::from(appdata).join("npm"));
83 }
84 if let Some(local) = std::env::var_os("LOCALAPPDATA") {
85 let local_path = PathBuf::from(local);
86 dirs.push(local_path.join("pnpm"));
87 dirs.push(local_path.join("Programs").join("Python"));
88 }
89 if let Some(up) = userprofile {
90 let up_path = PathBuf::from(up);
91 dirs.push(up_path.join(r".cargo\bin"));
92 dirs.push(up_path.join(r"go\bin"));
93 dirs.push(up_path.join("scoop").join("shims"));
94 }
95 dirs
96}
97
98#[cfg(not(windows))]
99pub(crate) fn well_known_windows_bin_dirs(_userprofile: Option<&std::ffi::OsStr>) -> Vec<PathBuf> {
100 Vec::new()
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::fs;
107 use std::sync::Mutex;
108
109 static PATH_ENV_LOCK: Mutex<()> = Mutex::new(());
110
111 #[test]
112 fn find_on_path_manual_returns_null_when_path_unset() {
113 let _guard = PATH_ENV_LOCK.lock().unwrap();
114 let saved = std::env::var_os("PATH");
115 std::env::remove_var("PATH");
116 assert!(find_on_path_manual("aft-nonexistent-tool-xyzzy").is_none());
117 if let Some(path) = saved {
118 std::env::set_var("PATH", path);
119 }
120 }
121
122 #[cfg(unix)]
123 #[test]
124 fn find_on_path_manual_finds_executable_in_single_dir() {
125 let _guard = PATH_ENV_LOCK.lock().unwrap();
126 let dir = tempfile::tempdir().unwrap();
127 let bin_path = dir.path().join("opencode-test-bin");
128 fs::write(&bin_path, "#!/bin/sh\necho ok\n").unwrap();
129 let mut perms = fs::metadata(&bin_path).unwrap().permissions();
130 use std::os::unix::fs::PermissionsExt;
131 perms.set_mode(0o755);
132 fs::set_permissions(&bin_path, perms).unwrap();
133
134 let saved = std::env::var_os("PATH");
135 std::env::set_var("PATH", dir.path());
136 let found = find_on_path_manual("opencode-test-bin");
137 if let Some(path) = saved {
138 std::env::set_var("PATH", path);
139 } else {
140 std::env::remove_var("PATH");
141 }
142
143 assert_eq!(found.as_deref(), Some(bin_path.as_path()));
144 }
145
146 #[cfg(unix)]
147 #[test]
148 fn find_on_path_manual_skips_non_executable_file() {
149 let _guard = PATH_ENV_LOCK.lock().unwrap();
150 let dir = tempfile::tempdir().unwrap();
151 let bin_path = dir.path().join("opencode-test-bin");
152 fs::write(&bin_path, "not executable\n").unwrap();
153
154 let saved = std::env::var_os("PATH");
155 std::env::set_var("PATH", dir.path());
156 let found = find_on_path_manual("opencode-test-bin");
157 if let Some(path) = saved {
158 std::env::set_var("PATH", path);
159 } else {
160 std::env::remove_var("PATH");
161 }
162
163 assert!(found.is_none());
164 }
165
166 #[cfg(windows)]
167 #[test]
168 fn probe_tool_in_dir_finds_cmd_shim() {
169 let dir = tempfile::tempdir().unwrap();
170 let cmd_path = dir.path().join("biome.cmd");
171 fs::write(&cmd_path, "@echo off\n").unwrap();
172 assert_eq!(
173 probe_tool_in_dir(dir.path(), "biome").as_deref(),
174 Some(cmd_path.as_path())
175 );
176 }
177}