1use super::errors::BridgeError;
7use r2x_logger as logger;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11#[cfg(windows)]
13pub const PYTHON_LIB_DIR: &str = "Lib";
14#[cfg(unix)]
15pub const PYTHON_LIB_DIR: &str = "lib";
16
17#[cfg(windows)]
19const PYTHON_BIN_DIR: &str = "Scripts";
20#[cfg(unix)]
21const PYTHON_BIN_DIR: &str = "bin";
22
23#[cfg(unix)]
25const PYTHON_EXE_CANDIDATES: &[&str] = &["python3", "python"];
26#[cfg(windows)]
27const PYTHON_EXE_CANDIDATES: &[&str] = &["python.exe", "python3.exe", "python3.12.exe"];
28
29pub fn resolve_site_package_path(venv_path: &Path) -> Result<PathBuf, BridgeError> {
38 logger::debug(&format!(
39 "Resolving site-packages path for venv: {}",
40 venv_path.display()
41 ));
42
43 if !venv_path.is_dir() {
45 logger::debug(&format!(
46 "Venv path does not exist or is not a directory: {}",
47 venv_path.display()
48 ));
49 return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
50 }
51
52 #[cfg(windows)]
53 {
54 let site_packages = venv_path.join(PYTHON_LIB_DIR).join("site-packages");
55 logger::debug(&format!(
56 "Windows: Looking for site-packages at: {}",
57 site_packages.display()
58 ));
59
60 if !site_packages.is_dir() {
62 logger::debug(&format!(
63 "Windows: site-packages directory not found at: {}",
64 site_packages.display()
65 ));
66 return Err(BridgeError::Initialization(format!(
67 "unable to locate package directory: {}",
68 site_packages.display()
69 )));
70 }
71 logger::debug(&format!(
72 "Windows: Successfully resolved site-packages: {}",
73 site_packages.display()
74 ));
75 Ok(site_packages)
76 }
77
78 #[cfg(not(windows))]
79 {
80 let lib_dir = venv_path.join(PYTHON_LIB_DIR);
81 logger::debug(&format!(
82 "Unix: Looking for lib directory at: {}",
83 lib_dir.display()
84 ));
85
86 if !lib_dir.is_dir() {
87 logger::debug(&format!(
88 "Unix: lib directory not found at: {}",
89 lib_dir.display()
90 ));
91 return Err(BridgeError::Initialization(format!(
92 "unable to locate lib directory: {}",
93 lib_dir.display()
94 )));
95 }
96
97 let python_version_dir = fs::read_dir(&lib_dir)
98 .map_err(|e| {
99 logger::debug(&format!("Unix: Failed to read lib directory: {}", e));
100 BridgeError::Initialization(format!("Failed to read lib directory: {}", e))
101 })?
102 .filter_map(|e| e.ok())
103 .find(|e| e.file_name().to_string_lossy().starts_with("python"))
104 .ok_or_else(|| {
105 logger::debug("Unix: No python3.X directory found in venv/lib");
106 BridgeError::Initialization("No python3.X directory found in venv/lib".to_string())
107 })?;
108
109 logger::debug(&format!(
110 "Unix: Found python version directory: {}",
111 python_version_dir.path().display()
112 ));
113
114 let site_packages = python_version_dir.path().join("site-packages");
115 logger::debug(&format!(
116 "Unix: Looking for site-packages at: {}",
117 site_packages.display()
118 ));
119
120 if !site_packages.is_dir() {
121 logger::debug(&format!(
122 "Unix: site-packages directory not found at: {}",
123 site_packages.display()
124 ));
125 return Err(BridgeError::Initialization(format!(
126 "unable to locate package directory: {}",
127 site_packages.display()
128 )));
129 }
130
131 logger::debug(&format!(
132 "Unix: Successfully resolved site-packages: {}",
133 site_packages.display()
134 ));
135 Ok(site_packages)
136 }
137}
138
139pub fn resolve_python_path(venv_path: &Path) -> Result<PathBuf, BridgeError> {
140 if !venv_path.is_dir() {
142 return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
143 }
144
145 let bin_dir = venv_path.join(PYTHON_BIN_DIR);
146 if !bin_dir.is_dir() {
147 return Err(BridgeError::Initialization(format!(
148 "Python bin directory missing: {}",
149 bin_dir.display()
150 )));
151 }
152
153 for exe in PYTHON_EXE_CANDIDATES {
154 let candidate = bin_dir.join(exe);
155 if candidate.is_file() {
156 return Ok(candidate);
157 }
158 }
159
160 if let Ok(entries) = fs::read_dir(&bin_dir) {
161 if let Some(candidate) = entries.filter_map(|e| e.ok()).map(|e| e.path()).find(|p| {
162 p.file_name()
163 .and_then(|n| n.to_str())
164 .map(|name| name.contains("python"))
165 .unwrap_or(false)
166 && p.is_file()
167 }) {
168 return Ok(candidate);
169 }
170 }
171
172 Err(BridgeError::Initialization(format!(
173 "Path to python binary is not valid in {}",
174 venv_path.display()
175 )))
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::fs;
182 use tempfile::TempDir;
183
184 #[allow(dead_code)]
186 fn create_mock_venv_unix(python_version: &str) -> Option<TempDir> {
187 let temp_dir = TempDir::new().ok()?;
188 let venv_path = temp_dir.path();
189
190 let lib_dir = venv_path.join("lib");
192 let python_dir = lib_dir.join(python_version);
193 let site_packages = python_dir.join("site-packages");
194 fs::create_dir_all(&site_packages).ok()?;
195
196 let bin_dir = venv_path.join("bin");
198 fs::create_dir_all(&bin_dir).ok()?;
199 fs::write(bin_dir.join("python3"), "").ok()?;
200
201 Some(temp_dir)
202 }
203
204 #[allow(dead_code)]
206 fn create_mock_venv_windows() -> Option<TempDir> {
207 let temp_dir = TempDir::new().ok()?;
208 let venv_path = temp_dir.path();
209
210 let lib_dir = venv_path.join("Lib");
212 let site_packages = lib_dir.join("site-packages");
213 fs::create_dir_all(&site_packages).ok()?;
214
215 let scripts_dir = venv_path.join("Scripts");
217 fs::create_dir_all(&scripts_dir).ok()?;
218 fs::write(scripts_dir.join("python.exe"), "").ok()?;
219
220 Some(temp_dir)
221 }
222
223 #[test]
224 #[cfg(unix)]
225 fn test_resolve_site_package_path_unix() {
226 let Some(temp_venv) = create_mock_venv_unix("python3.12") else {
227 return;
228 };
229 let venv_path = temp_venv.path().to_path_buf();
230
231 let result = resolve_site_package_path(&venv_path);
232 assert!(result.is_ok());
233 assert!(result.is_ok_and(|sp| sp.ends_with("lib/python3.12/site-packages") && sp.exists()));
234 }
235
236 #[test]
237 #[cfg(unix)]
238 fn test_resolve_site_package_path_unix_different_version() {
239 let Some(temp_venv) = create_mock_venv_unix("python3.11") else {
240 return;
241 };
242 let venv_path = temp_venv.path().to_path_buf();
243
244 let result = resolve_site_package_path(&venv_path);
245 assert!(result.is_ok());
246 assert!(result.is_ok_and(|sp| sp.ends_with("lib/python3.11/site-packages")));
247 }
248
249 #[test]
250 #[cfg(windows)]
251 fn test_resolve_site_package_path_windows() {
252 let Some(temp_venv) = create_mock_venv_windows() else {
253 return;
254 };
255 let venv_path = temp_venv.path().to_path_buf();
256
257 let result = resolve_site_package_path(&venv_path);
258 assert!(result.is_ok());
259 assert!(result.is_ok_and(|sp| sp.ends_with("Lib\\site-packages") && sp.exists()));
260 }
261
262 #[test]
263 fn test_resolve_site_package_path_venv_not_found() {
264 let non_existent_path = PathBuf::from("/tmp/non_existent_venv_12345");
265
266 let result = resolve_site_package_path(&non_existent_path);
267 assert!(result.is_err());
268
269 match result {
270 Err(BridgeError::VenvNotFound(path)) => {
271 assert_eq!(path, non_existent_path);
272 }
273 _ => panic!("Expected VenvNotFound error"),
274 }
275 }
276
277 #[test]
278 #[cfg(unix)]
279 fn test_resolve_site_package_path_missing_python_dir() {
280 let Ok(temp_dir) = TempDir::new() else {
281 return;
282 };
283 let venv_path = temp_dir.path();
284
285 let lib_dir = venv_path.join("lib");
287 if fs::create_dir_all(&lib_dir).is_err() {
288 return;
289 }
290
291 let result = resolve_site_package_path(venv_path);
292 assert!(result.is_err());
293 assert!(result.is_err_and(|e| matches!(e, BridgeError::Initialization(msg) if msg.contains("No python3.X directory found"))));
294 }
295
296 #[test]
297 #[cfg(unix)]
298 fn test_resolve_python_path_unix() {
299 let Some(temp_venv) = create_mock_venv_unix("python3.12") else {
300 return;
301 };
302 let venv_path = temp_venv.path().to_path_buf();
303
304 let result = resolve_python_path(&venv_path);
305 assert!(result.is_ok());
306 assert!(result.is_ok_and(|pp| pp.ends_with("bin/python3")));
307 }
308
309 #[test]
310 #[cfg(windows)]
311 fn test_resolve_python_path_windows() {
312 let Some(temp_venv) = create_mock_venv_windows() else {
313 return;
314 };
315 let venv_path = temp_venv.path().to_path_buf();
316
317 let result = resolve_python_path(&venv_path);
318 assert!(result.is_ok());
319 assert!(result.is_ok_and(|pp| pp.ends_with("Scripts\\python.exe")));
320 }
321
322 #[test]
323 fn test_python_lib_dir_constant() {
324 #[cfg(unix)]
326 assert_eq!(PYTHON_LIB_DIR, "lib");
327
328 #[cfg(windows)]
329 assert_eq!(PYTHON_LIB_DIR, "Lib");
330 }
331
332 #[test]
333 fn test_python_bin_dir_constant() {
334 #[cfg(unix)]
336 assert_eq!(PYTHON_BIN_DIR, "bin");
337
338 #[cfg(windows)]
339 assert_eq!(PYTHON_BIN_DIR, "Scripts");
340 }
341
342 #[test]
343 #[cfg(unix)]
344 fn test_resolve_site_package_path_with_multiple_python_versions() {
345 let Ok(temp_dir) = TempDir::new() else {
346 return;
347 };
348 let venv_path = temp_dir.path();
349
350 let lib_dir = venv_path.join("lib");
352 if fs::create_dir_all(&lib_dir).is_err() {
353 return;
354 }
355
356 let python_311 = lib_dir.join("python3.11");
358 let site_packages_311 = python_311.join("site-packages");
359 if fs::create_dir_all(&site_packages_311).is_err() {
360 return;
361 }
362
363 let python_312 = lib_dir.join("python3.12");
365 let site_packages_312 = python_312.join("site-packages");
366 if fs::create_dir_all(&site_packages_312).is_err() {
367 return;
368 }
369
370 let result = resolve_site_package_path(venv_path);
371 assert!(result.is_ok());
372 assert!(result.is_ok_and(|sp| sp.to_string_lossy().contains("python3.1")
374 && sp.ends_with("site-packages")));
375 }
376}