huak/env/
system.rs

1use std::env;
2use std::fs;
3use std::path::PathBuf;
4
5use crate::errors::{HuakError, HuakResult};
6
7const PYTHON_BINARY_TARGETS: [PythonBinary; 3] = [
8    PythonBinary::Python3,
9    PythonBinary::Python,
10    PythonBinary::Python2,
11];
12
13enum PythonBinary {
14    Python,
15    Python3,
16    Python2,
17}
18
19impl PythonBinary {
20    fn as_str(&self) -> &'static str {
21        match &self {
22            PythonBinary::Python => "python",
23            PythonBinary::Python3 => "python3",
24            PythonBinary::Python2 => "python2",
25        }
26    }
27}
28
29/// Returns the full path of the python binary in a sepcific order. Python 2 is depcreated so
30/// python3 is prefered. If there is no python3 some distributions also rename python3 to simply
31/// python. See [PEP394](https://peps.python.org/pep-0394/)
32/// TODO: Refactor to evaluate against each file during the search.
33pub fn find_python_binary_path(
34    from_dir: Option<PathBuf>,
35) -> HuakResult<String> {
36    let paths = match from_dir {
37        Some(path) => vec![path],
38        None => parse_path_var()?,
39    };
40
41    for path in paths {
42        for target in PYTHON_BINARY_TARGETS.iter() {
43            #[cfg(unix)]
44            {
45                if let Ok(Some(python)) = find_binary(target.as_str(), &path) {
46                    return Ok(python);
47                }
48            }
49        }
50    }
51
52    Err(HuakError::PythonNotFound)
53}
54
55/// Gets the PATH environment variable and splits this on ':'.
56fn parse_path_var() -> HuakResult<Vec<PathBuf>> {
57    let path_str = match env::var("PATH") {
58        Ok(path) => path,
59        Err(e) => return Err(HuakError::EnvVarError(e)),
60    };
61
62    Ok(path_str.split(':').map(|dir| dir.into()).collect())
63}
64
65/// Takes a binary name and searches the entire dir, if it finds the binary it will return the path
66/// to the binary by appending the bin name to the dir.
67///
68/// returns on the first hit
69fn find_binary(bin_name: &str, dir: &PathBuf) -> HuakResult<Option<String>> {
70    let read_dir = match fs::read_dir(dir) {
71        Ok(read_dir) => read_dir,
72        Err(e) => return Err(HuakError::IOError(e)),
73    };
74
75    for dir_entry in read_dir.flatten() {
76        if let Ok(file_type) = dir_entry.file_type() {
77            if file_type.is_dir() {
78                if let Ok(bin_path) = find_binary(bin_name, &dir_entry.path()) {
79                    return Ok(bin_path);
80                }
81            } else if let Some(file_name) = dir_entry.file_name().to_str() {
82                if file_name == bin_name {
83                    if let Some(bin_path) = dir_entry.path().to_str() {
84                        return Ok(Some(bin_path.to_string()));
85                    }
86                }
87            }
88        }
89    }
90
91    Ok(None)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    use tempfile::tempdir;
99
100    #[cfg(target_os = "windows")]
101    #[test]
102    fn test_python_search_windows() -> Result<(), std::io::Error> {
103        let directory = tempdir().unwrap();
104
105        fs::write(directory.path().join("python.exe"), "")?;
106
107        let expected_python =
108            String::from(directory.path().join("python.exe").to_str().unwrap());
109
110        assert_eq!(
111            find_binary("python.exe", &directory.into_path()).unwrap(),
112            Some(expected_python)
113        );
114
115        Ok(())
116    }
117
118    #[cfg(target_os = "macos")]
119    #[test]
120    fn test_python_search_macos() -> Result<(), std::io::Error> {
121        let directory = tempdir().unwrap();
122
123        fs::write(directory.path().join("python"), "")?;
124
125        let expected_python =
126            String::from(directory.path().join("python").to_str().unwrap());
127
128        assert_eq!(
129            find_binary("python", &directory.into_path()).unwrap(),
130            Some(expected_python)
131        );
132
133        Ok(())
134    }
135
136    #[cfg(target_os = "linux")]
137    #[test]
138    fn test_python_search_linux() -> Result<(), std::io::Error> {
139        let directory = tempdir().unwrap();
140
141        fs::write(directory.path().join("python3"), "")?;
142
143        let expected_python =
144            String::from(directory.path().join("python3").to_str().unwrap());
145
146        assert_eq!(
147            find_binary("python3", &directory.into_path()).unwrap(),
148            Some(expected_python)
149        );
150
151        Ok(())
152    }
153}