1use crate::errors::BridgeError;
14use crate::utils::{resolve_python_path, resolve_site_package_path};
15use once_cell::sync::OnceCell;
16use pyo3::prelude::*;
17use pyo3::types::PyModule;
18use r2x_config::Config;
19use r2x_logger as logger;
20use std::env;
21use std::fs;
22use std::path::{Path, PathBuf};
23use std::process::Command;
24
25pub struct Bridge {
27 _marker: (),
29}
30
31static BRIDGE_INSTANCE: OnceCell<Result<Bridge, BridgeError>> = OnceCell::new();
33
34impl Bridge {
35 pub fn get() -> Result<&'static Bridge, BridgeError> {
37 match BRIDGE_INSTANCE.get_or_init(Bridge::initialize) {
38 Ok(bridge) => Ok(bridge),
39 Err(e) => Err(BridgeError::Initialization(format!("{}", e))),
40 }
41 }
42
43 pub fn is_python_available() -> bool {
45 let config = match Config::load() {
46 Ok(c) => c,
47 Err(_) => return false,
48 };
49
50 let venv_path = PathBuf::from(config.get_venv_path());
52 venv_path.join("pyvenv.cfg").exists()
53 }
54
55 fn initialize() -> Result<Bridge, BridgeError> {
63 let start_time = std::time::Instant::now();
64
65 let mut config = Config::load()
66 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
67
68 let venv_path = PathBuf::from(config.get_venv_path());
70
71 if !venv_path.exists() {
72 Self::create_venv(&config, &venv_path)?;
74 }
75
76 let python_home = resolve_python_home(&venv_path)?;
78 env::set_var("PYTHONHOME", &python_home);
79 logger::debug(&format!("Set PYTHONHOME={}", python_home.display()));
80
81 let site_packages = resolve_site_package_path(&venv_path)?;
83
84 Self::configure_python_path(&site_packages);
86
87 check_python_library_available()?;
89
90 logger::debug("Initializing PyO3...");
92 let pyo3_start = std::time::Instant::now();
93 pyo3::Python::initialize();
94 logger::debug(&format!(
95 "pyo3::Python::initialize took: {:?}",
96 pyo3_start.elapsed()
97 ));
98
99 pyo3::Python::attach(|py| {
101 let sys = PyModule::import(py, "sys")
102 .map_err(|e| BridgeError::Python(format!("Failed to import sys module: {}", e)))?;
103 sys.setattr("dont_write_bytecode", false).map_err(|e| {
104 BridgeError::Python(format!("Failed to enable bytecode generation: {}", e))
105 })?;
106 Ok::<(), BridgeError>(())
107 })?;
108 logger::debug("Enabled Python bytecode generation");
109
110 pyo3::Python::attach(|py| {
112 let site = PyModule::import(py, "site")
113 .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
114 site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
115 .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
116 Ok::<(), BridgeError>(())
117 })?;
118
119 let cache_path = config.ensure_cache_path().map_err(|e| {
121 BridgeError::Initialization(format!("Failed to ensure cache path: {}", e))
122 })?;
123 Self::configure_python_cache(&cache_path)?;
124
125 if let Err(e) = Self::configure_python_logging() {
127 logger::warn(&format!("Python logging configuration failed: {}", e));
128 }
129
130 logger::debug(&format!(
131 "Total bridge initialization took: {:?}",
132 start_time.elapsed()
133 ));
134
135 Ok(Bridge { _marker: () })
136 }
137
138 fn create_venv(config: &Config, venv_path: &PathBuf) -> Result<(), BridgeError> {
142 logger::step(&format!(
143 "Creating Python virtual environment at: {}",
144 venv_path.display()
145 ));
146
147 let python_version = get_compiled_python_version();
148
149 if let Some(ref uv_path) = config.uv_path {
151 let output = Command::new(uv_path)
152 .arg("venv")
153 .arg(venv_path)
154 .arg("--python")
155 .arg(&python_version)
156 .output()?;
157
158 if output.status.success() {
159 logger::success("Virtual environment created successfully");
160 return Ok(());
161 }
162
163 let stderr = String::from_utf8_lossy(&output.stderr);
164 logger::debug(&format!("uv venv failed: {}", stderr));
165 }
166
167 let python_cmd = format!("python{}", python_version);
169 let output = Command::new(&python_cmd)
170 .args(["-m", "venv"])
171 .arg(venv_path)
172 .output();
173
174 if let Ok(output) = output {
175 if output.status.success() {
176 logger::success("Virtual environment created successfully");
177 return Ok(());
178 }
179 }
180
181 let output = Command::new("python3")
183 .args(["-m", "venv"])
184 .arg(venv_path)
185 .output()?;
186
187 if !output.status.success() {
188 let stderr = String::from_utf8_lossy(&output.stderr);
189 return Err(BridgeError::Initialization(format!(
190 "Failed to create virtual environment: {}",
191 stderr
192 )));
193 }
194
195 logger::success("Virtual environment created successfully");
196 Ok(())
197 }
198
199 fn configure_python_path(site_packages: &Path) {
201 let mut paths = vec![site_packages.to_path_buf()];
202 if let Some(existing) = env::var_os("PYTHONPATH") {
203 if !existing.is_empty() {
204 paths.extend(env::split_paths(&existing));
205 }
206 }
207 if let Ok(joined) = env::join_paths(paths) {
208 env::set_var("PYTHONPATH", &joined);
209 logger::debug(&format!(
210 "Updated PYTHONPATH to include {}",
211 site_packages.display()
212 ));
213 }
214 }
215
216 fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
218 std::fs::create_dir_all(cache_path).map_err(|e| {
219 BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
220 })?;
221 env::set_var("R2X_CACHE_PATH", cache_path);
222
223 let cache_path_escaped = cache_path.replace('\\', "\\\\");
224 pyo3::Python::attach(|py| {
225 let patch_code = format!(
226 r#"from pathlib import Path
227_R2X_CACHE_PATH = Path(r"{cache}")
228
229def _r2x_cache_path_override():
230 return _R2X_CACHE_PATH
231"#,
232 cache = cache_path_escaped
233 );
234
235 let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
236 BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
237 })?;
238 let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
239 let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
240 let patch_module = PyModule::from_code(
241 py,
242 code_cstr.as_c_str(),
243 filename.as_c_str(),
244 module_name.as_c_str(),
245 )
246 .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
247
248 let override_fn = patch_module
249 .getattr("_r2x_cache_path_override")
250 .map_err(|e| {
251 BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
252 })?;
253
254 let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
255 BridgeError::Python(format!(
256 "Failed to import r2x_core.utils.file_operations: {}",
257 e
258 ))
259 })?;
260
261 file_ops
262 .setattr("get_r2x_cache_path", override_fn)
263 .map_err(|e| {
264 BridgeError::Python(format!("Failed to override cache path: {}", e))
265 })?;
266
267 Ok::<(), BridgeError>(())
268 })?;
269
270 Ok(())
271 }
272
273 fn configure_python_logging() -> Result<(), BridgeError> {
275 let log_python = logger::get_log_python();
276 if !log_python {
277 return Ok(());
278 }
279
280 let verbosity = logger::get_verbosity();
281 logger::debug(&format!(
282 "Configuring Python logging with verbosity={}",
283 verbosity
284 ));
285
286 pyo3::Python::attach(|py| {
287 let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
288 logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
289 BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
290 })?;
291 let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
292 logger::warn(&format!("Failed to get setup_logging function: {}", e));
293 BridgeError::Python(format!("setup_logging not found: {}", e))
294 })?;
295 setup_logging.call1((verbosity,))?;
296
297 let loguru = PyModule::import(py, "loguru")?;
298 let logger_obj = loguru.getattr("logger")?;
299 logger_obj.call_method1("enable", ("r2x_core",))?;
300 logger_obj.call_method1("enable", ("r2x_reeds",))?;
301 logger_obj.call_method1("enable", ("r2x_plexos",))?;
302 logger_obj.call_method1("enable", ("r2x_sienna",))?;
303
304 Ok::<(), BridgeError>(())
305 })
306 }
307
308 pub fn reconfigure_logging_for_plugin(_plugin_name: &str) -> Result<(), BridgeError> {
310 Self::configure_python_logging()
311 }
312}
313
314fn resolve_python_home(venv_path: &Path) -> Result<PathBuf, BridgeError> {
326 let pyvenv_cfg = venv_path.join("pyvenv.cfg");
327
328 if !pyvenv_cfg.exists() {
329 return Err(BridgeError::Initialization(format!(
330 "pyvenv.cfg not found in venv: {}",
331 venv_path.display()
332 )));
333 }
334
335 let content = fs::read_to_string(&pyvenv_cfg)
336 .map_err(|e| BridgeError::Initialization(format!("Failed to read pyvenv.cfg: {}", e)))?;
337
338 for line in content.lines() {
339 let line = line.trim();
340 if line.starts_with("home") {
341 if let Some((_key, value)) = line.split_once('=') {
342 let home_bin = PathBuf::from(value.trim());
343 if let Some(parent) = home_bin.parent() {
345 logger::debug(&format!(
346 "Resolved PYTHONHOME from pyvenv.cfg: {}",
347 parent.display()
348 ));
349 return Ok(parent.to_path_buf());
350 }
351 return Ok(home_bin);
353 }
354 }
355 }
356
357 Err(BridgeError::Initialization(format!(
358 "Could not find 'home' in pyvenv.cfg: {}",
359 pyvenv_cfg.display()
360 )))
361}
362
363fn get_compiled_python_version() -> String {
368 "3.12".to_string()
372}
373
374fn check_python_library_available() -> Result<(), BridgeError> {
379 #[cfg(target_os = "macos")]
380 {
381 let python_version = get_compiled_python_version();
383 let lib_name = format!("libpython{}.dylib", python_version);
384
385 let search_paths = [
386 "/opt/homebrew/lib",
388 "/usr/local/lib",
389 "/Library/Frameworks/Python.framework/Versions/Current/lib",
391 ];
392
393 if let Ok(paths) = env::var("DYLD_LIBRARY_PATH") {
395 for path in paths.split(':') {
396 let lib_path = PathBuf::from(path).join(&lib_name);
397 if lib_path.exists() {
398 logger::debug(&format!("Found Python library at: {}", lib_path.display()));
399 return Ok(());
400 }
401 }
402 }
403
404 for search_path in &search_paths {
406 let lib_path = PathBuf::from(search_path).join(&lib_name);
407 if lib_path.exists() {
408 logger::debug(&format!("Found Python library at: {}", lib_path.display()));
409 return Ok(());
410 }
411 }
412
413 if let Ok(output) = Command::new("uv")
415 .args(["python", "find", &python_version])
416 .output()
417 {
418 if output.status.success() {
419 let python_path = String::from_utf8_lossy(&output.stdout);
420 let python_path = python_path.trim();
421 if let Some(parent) = PathBuf::from(python_path).parent() {
422 let lib_dir = parent.parent().map(|p| p.join("lib")).unwrap_or_default();
424 let lib_path = lib_dir.join(&lib_name);
425 if lib_path.exists() {
426 logger::debug(&format!(
427 "Found Python library via uv: {}",
428 lib_path.display()
429 ));
430 if let Some(existing) = env::var_os("DYLD_LIBRARY_PATH") {
432 let mut paths = env::split_paths(&existing).collect::<Vec<_>>();
433 paths.insert(0, lib_dir.clone());
434 if let Ok(new_path) = env::join_paths(&paths) {
435 env::set_var("DYLD_LIBRARY_PATH", new_path);
436 }
437 } else {
438 env::set_var("DYLD_LIBRARY_PATH", &lib_dir);
439 }
440 logger::debug(&format!(
441 "Set DYLD_LIBRARY_PATH to include: {}",
442 lib_dir.display()
443 ));
444 return Ok(());
445 }
446 }
447 }
448 }
449
450 logger::debug(&format!(
454 "Python library {} not found in standard locations, relying on rpath",
455 lib_name
456 ));
457 Ok(())
458 }
459
460 #[cfg(target_os = "linux")]
461 {
462 let python_version = get_compiled_python_version();
464 let lib_names = [
465 format!("libpython{}.so", python_version),
466 format!("libpython{}.so.1.0", python_version),
467 ];
468
469 let search_paths = [
470 "/usr/lib",
471 "/usr/lib64",
472 "/usr/local/lib",
473 "/usr/local/lib64",
474 ];
475
476 if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
478 for path in paths.split(':') {
479 for lib_name in &lib_names {
480 let lib_path = PathBuf::from(path).join(lib_name);
481 if lib_path.exists() {
482 logger::debug(&format!("Found Python library at: {}", lib_path.display()));
483 return Ok(());
484 }
485 }
486 }
487 }
488
489 for search_path in &search_paths {
491 for lib_name in &lib_names {
492 let lib_path = PathBuf::from(search_path).join(lib_name);
493 if lib_path.exists() {
494 logger::debug(&format!("Found Python library at: {}", lib_path.display()));
495 return Ok(());
496 }
497 }
498 }
499
500 if let Ok(output) = Command::new("uv")
502 .args(["python", "find", &python_version])
503 .output()
504 {
505 if output.status.success() {
506 let python_path = String::from_utf8_lossy(&output.stdout);
507 let python_path = python_path.trim();
508 if let Some(parent) = PathBuf::from(python_path).parent() {
509 let lib_dir = parent.parent().map(|p| p.join("lib")).unwrap_or_default();
511 for lib_name in &lib_names {
512 let lib_path = lib_dir.join(lib_name);
513 if lib_path.exists() {
514 logger::debug(&format!(
515 "Found Python library via uv: {}",
516 lib_path.display()
517 ));
518 if let Some(existing) = env::var_os("LD_LIBRARY_PATH") {
520 let mut paths = env::split_paths(&existing).collect::<Vec<_>>();
521 paths.insert(0, lib_dir.clone());
522 if let Ok(new_path) = env::join_paths(&paths) {
523 env::set_var("LD_LIBRARY_PATH", new_path);
524 }
525 } else {
526 env::set_var("LD_LIBRARY_PATH", &lib_dir);
527 }
528 logger::debug(&format!(
529 "Set LD_LIBRARY_PATH to include: {}",
530 lib_dir.display()
531 ));
532 return Ok(());
533 }
534 }
535 }
536 }
537 }
538
539 logger::debug("Python library not found in standard locations, relying on rpath");
542 Ok(())
543 }
544
545 #[cfg(target_os = "windows")]
546 {
547 if let Err(e) = setup_windows_dll_path() {
549 logger::debug(&format!("Windows DLL path setup note: {}", e));
550 }
551 Ok(())
552 }
553
554 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
555 {
556 Ok(())
558 }
559}
560
561#[cfg(target_os = "windows")]
563fn setup_windows_dll_path() -> Result<(), BridgeError> {
564 use std::process::Command;
565
566 let python_version = get_compiled_python_version();
567 let dll_name = format!("python{}.dll", python_version.replace(".", ""));
568
569 let output = Command::new("uv")
571 .args(["python", "find", &python_version])
572 .output();
573
574 if let Ok(output) = output {
575 if output.status.success() {
576 let python_path = String::from_utf8_lossy(&output.stdout);
577 let python_path = python_path.trim();
578 if let Some(parent) = PathBuf::from(python_path).parent() {
579 let dll_path = parent.join(&dll_name);
581 if dll_path.exists() {
582 if let Ok(current_path) = env::var("PATH") {
584 let new_path = format!("{};{}", parent.display(), current_path);
585 env::set_var("PATH", &new_path);
586 logger::debug(&format!(
587 "Added {} to PATH for Python DLL discovery",
588 parent.display()
589 ));
590 return Ok(());
591 }
592 }
593 }
594 }
595 }
596
597 if let Ok(output) = Command::new("where").arg("python").output() {
599 if output.status.success() {
600 let python_path = String::from_utf8_lossy(&output.stdout);
601 if let Some(first_line) = python_path.lines().next() {
602 if let Some(parent) = PathBuf::from(first_line.trim()).parent() {
603 let dll_path = parent.join(&dll_name);
604 if dll_path.exists() {
605 logger::debug(&format!("Found Python DLL at: {}", dll_path.display()));
606 return Ok(());
607 }
608 }
609 }
610 }
611 }
612
613 Err(BridgeError::PythonLibraryNotFound(format!(
614 "Could not find {}.\n\n\
615 This binary requires Python {} to be installed.\n\n\
616 To fix this on Windows:\n\
617 1. Install Python via uv: uv python install {}\n\
618 2. Or download from https://www.python.org/downloads/\n\
619 3. Ensure Python is in your PATH\n\n\
620 If you installed Python via uv, try running:\n\
621 uv python find {}",
622 dll_name, python_version, python_version, python_version
623 )))
624}
625
626pub fn configure_python_venv() -> Result<PythonEnvCompat, BridgeError> {
628 let config = Config::load()
629 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
630
631 let venv_path = PathBuf::from(config.get_venv_path());
632
633 let interpreter = resolve_python_path(&venv_path)?;
634 let python_home = resolve_python_home(&venv_path).ok();
635
636 Ok(PythonEnvCompat {
637 interpreter,
638 python_home,
639 })
640}
641
642#[derive(Debug, Clone)]
644pub struct PythonEnvCompat {
645 pub interpreter: PathBuf,
646 pub python_home: Option<PathBuf>,
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn test_bridge_struct() {
655 let _bridge = Bridge { _marker: () };
657 }
658
659 #[test]
660 fn test_get_compiled_python_version() {
661 let version = get_compiled_python_version();
662 assert!(version.starts_with("3."));
663 }
664}