Skip to main content

winx_code_agent/utils/
executable.rs

1//! Portable executable discovery.
2//!
3//! Winx used to probe for programs with `sh -c 'command -v …'`, which assumes a
4//! POSIX shell on `PATH`. Native Windows has no `sh`, and its `bash.exe` may be
5//! the WSL launcher in `System32` rather than a usable shell, so lookups are
6//! done directly against `PATH` (honoring `PATHEXT` on Windows) and the known
7//! Git for Windows install locations.
8
9use std::path::{Path, PathBuf};
10
11/// Locate `program` on `PATH`. A name with a path separator is checked as-is.
12/// On Windows, a bare name is tried with every `PATHEXT` suffix (`.exe`,
13/// `.cmd`, …) as well as verbatim.
14pub fn find_on_path(program: &str) -> Option<PathBuf> {
15    if program.is_empty() {
16        return None;
17    }
18    let direct = Path::new(program);
19    if direct.components().count() > 1 {
20        return is_executable_file(direct).then(|| direct.to_path_buf());
21    }
22    let path = std::env::var_os("PATH")?;
23    std::env::split_paths(&path)
24        .filter(|dir| !dir.as_os_str().is_empty())
25        .flat_map(|dir| candidate_names(program).into_iter().map(move |name| dir.join(name)))
26        .find(|candidate| is_executable_file(candidate))
27}
28
29/// Whether `program` can be found on `PATH`. Best-effort: used for advisory
30/// hints and shell selection, never as a security boundary.
31pub fn is_available(program: &str) -> bool {
32    find_on_path(program).is_some()
33}
34
35/// The `bash` executable Winx should drive. On Unix this is whatever `PATH`
36/// resolves. On Windows the Git for Windows shell is preferred and
37/// `System32\bash.exe` (the WSL launcher, which needs a distribution and does
38/// not honor `PROMPT_COMMAND`) is never selected.
39pub fn bash_executable() -> Option<PathBuf> {
40    #[cfg(windows)]
41    {
42        windows_bash_executable()
43    }
44    #[cfg(not(windows))]
45    {
46        find_on_path("bash")
47    }
48}
49
50#[cfg(windows)]
51fn windows_bash_executable() -> Option<PathBuf> {
52    let mut candidates = Vec::new();
53    for root in ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"] {
54        if let Some(base) = std::env::var_os(root) {
55            candidates.push(PathBuf::from(&base).join("Git").join("bin").join("bash.exe"));
56            candidates
57                .push(PathBuf::from(base).join("Git").join("usr").join("bin").join("bash.exe"));
58        }
59    }
60    if let Some(base) = std::env::var_os("LOCALAPPDATA") {
61        let user_git = PathBuf::from(base).join("Programs").join("Git");
62        candidates.push(user_git.join("bin").join("bash.exe"));
63        candidates.push(user_git.join("usr").join("bin").join("bash.exe"));
64    }
65    candidates
66        .into_iter()
67        .find(|candidate| is_executable_file(candidate))
68        .or_else(|| find_on_path("bash").filter(|found| !is_windows_system_launcher(found)))
69}
70
71/// `bash.exe` under `System32` and the Store alias under
72/// `%LOCALAPPDATA%\Microsoft\WindowsApps` both launch WSL, not a shell.
73#[cfg(windows)]
74fn is_windows_system_launcher(path: &Path) -> bool {
75    let mut launcher_dirs = Vec::new();
76    if let Some(system_root) = std::env::var_os("SystemRoot") {
77        launcher_dirs.push(PathBuf::from(system_root).join("System32"));
78    }
79    if let Some(local) = std::env::var_os("LOCALAPPDATA") {
80        launcher_dirs.push(PathBuf::from(local).join("Microsoft").join("WindowsApps"));
81    }
82    path.ancestors().any(|ancestor| {
83        launcher_dirs.iter().any(|dir| ancestor.as_os_str().eq_ignore_ascii_case(dir.as_os_str()))
84    })
85}
86
87#[cfg(windows)]
88fn candidate_names(program: &str) -> Vec<String> {
89    let has_extension = Path::new(program).extension().is_some();
90    let mut names = vec![program.to_string()];
91    if !has_extension {
92        let pathext =
93            std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
94        names.extend(
95            pathext
96                .split(';')
97                .filter(|extension| !extension.is_empty())
98                .map(|extension| format!("{program}{}", extension.to_ascii_lowercase())),
99        );
100    }
101    names
102}
103
104#[cfg(not(windows))]
105fn candidate_names(program: &str) -> Vec<String> {
106    vec![program.to_string()]
107}
108
109#[cfg(unix)]
110fn is_executable_file(path: &Path) -> bool {
111    use std::os::unix::fs::PermissionsExt as _;
112    std::fs::metadata(path)
113        .is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
114}
115
116#[cfg(not(unix))]
117fn is_executable_file(path: &Path) -> bool {
118    std::fs::metadata(path).is_ok_and(|meta| meta.is_file())
119}
120
121#[cfg(test)]
122mod tests {
123    #![allow(clippy::expect_used)]
124    use super::{find_on_path, is_available};
125
126    #[test]
127    fn empty_and_missing_programs_are_not_found() {
128        assert!(find_on_path("").is_none());
129        assert!(!is_available("winx-definitely-not-a-real-program-9f8e7d"));
130    }
131
132    #[cfg(unix)]
133    #[test]
134    fn resolves_a_standard_unix_tool_and_rejects_non_executables() {
135        assert!(is_available("sh"));
136        let temp = tempfile::tempdir().expect("temp dir");
137        let plain = temp.path().join("not-executable");
138        std::fs::write(&plain, b"data").expect("write");
139        assert!(find_on_path(plain.to_str().expect("utf-8 path")).is_none());
140    }
141}