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
108 #[test]
109 fn find_on_path_manual_returns_null_when_path_unset() {
110 let _guard = crate::test_env::process_env_lock();
111 let saved = std::env::var_os("PATH");
112 std::env::remove_var("PATH");
113 assert!(find_on_path_manual("aft-nonexistent-tool-xyzzy").is_none());
114 if let Some(path) = saved {
115 std::env::set_var("PATH", path);
116 }
117 }
118
119 #[cfg(unix)]
120 #[test]
121 fn find_on_path_manual_finds_executable_in_single_dir() {
122 let _guard = crate::test_env::process_env_lock();
123 let dir = tempfile::tempdir().unwrap();
124 let bin_path = dir.path().join("opencode-test-bin");
125 fs::write(&bin_path, "#!/bin/sh\necho ok\n").unwrap();
126 let mut perms = fs::metadata(&bin_path).unwrap().permissions();
127 use std::os::unix::fs::PermissionsExt;
128 perms.set_mode(0o755);
129 fs::set_permissions(&bin_path, perms).unwrap();
130
131 let saved = std::env::var_os("PATH");
132 std::env::set_var("PATH", dir.path());
133 let found = find_on_path_manual("opencode-test-bin");
134 if let Some(path) = saved {
135 std::env::set_var("PATH", path);
136 } else {
137 std::env::remove_var("PATH");
138 }
139
140 assert_eq!(found.as_deref(), Some(bin_path.as_path()));
141 }
142
143 #[cfg(unix)]
144 #[test]
145 fn find_on_path_manual_skips_non_executable_file() {
146 let _guard = crate::test_env::process_env_lock();
147 let dir = tempfile::tempdir().unwrap();
148 let bin_path = dir.path().join("opencode-test-bin");
149 fs::write(&bin_path, "not executable\n").unwrap();
150
151 let saved = std::env::var_os("PATH");
152 std::env::set_var("PATH", dir.path());
153 let found = find_on_path_manual("opencode-test-bin");
154 if let Some(path) = saved {
155 std::env::set_var("PATH", path);
156 } else {
157 std::env::remove_var("PATH");
158 }
159
160 assert!(found.is_none());
161 }
162
163 #[cfg(windows)]
164 #[test]
165 fn probe_tool_in_dir_finds_cmd_shim() {
166 let dir = tempfile::tempdir().unwrap();
167 let cmd_path = dir.path().join("biome.cmd");
168 fs::write(&cmd_path, "@echo off\n").unwrap();
169 assert_eq!(
170 probe_tool_in_dir(dir.path(), "biome").as_deref(),
171 Some(cmd_path.as_path())
172 );
173 }
174}