1use super::utils::{resolve_python_path, resolve_site_package_path};
7use crate::errors::BridgeError;
8use once_cell::sync::OnceCell;
9use pyo3::prelude::*;
10use pyo3::types::PyModule;
11use r2x_config::Config;
12use r2x_logger as logger;
13use std::env;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17use which::which;
18
19#[cfg(windows)]
20const PYTHON_BIN_DIR_NAME: &str = "Scripts";
21#[cfg(not(windows))]
22const PYTHON_BIN_DIR_NAME: &str = "bin";
23#[cfg(windows)]
24const SYSTEM_PYTHON_CANDIDATES: &[&str] = &["python.exe", "py.exe"];
25#[cfg(not(windows))]
26const SYSTEM_PYTHON_CANDIDATES: &[&str] = &["python3", "python"];
27const REQUIRED_PYTHON_MAJOR: i32 = 3;
28const REQUIRED_PYTHON_MINOR: i32 = 12;
29
30pub struct Bridge {}
31
32#[derive(Debug, Clone)]
33pub struct PythonEnvironment {
34 pub interpreter: PathBuf,
35 pub python_home: Option<PathBuf>,
36}
37
38static BRIDGE_INSTANCE: OnceCell<Result<Bridge, BridgeError>> = OnceCell::new();
39
40impl Bridge {
41 pub fn get() -> Result<&'static Bridge, BridgeError> {
43 match BRIDGE_INSTANCE.get_or_init(Bridge::initialize) {
44 Ok(bridge) => Ok(bridge),
45 Err(e) => Err(BridgeError::Initialization(format!("{}", e))),
46 }
47 }
48
49 fn initialize() -> Result<Bridge, BridgeError> {
56 let start_time = std::time::Instant::now();
57
58 let python_env = configure_python_venv()?;
59 let python_path = python_env.interpreter.clone();
60
61 let mut config = Config::load()
62 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
63 let cache_path = config.ensure_cache_path().map_err(|e| {
64 BridgeError::Initialization(format!("Failed to ensure cache path: {}", e))
65 })?;
66 let venv_path = PathBuf::from(config.get_venv_path());
67 let site_packages = resolve_site_package_path(&venv_path)?;
68 if let Some(ref home) = python_env.python_home {
69 configure_embedded_python_env(home, &site_packages);
70 } else {
71 logger::debug("Using default embedded Python search paths");
72 }
73
74 logger::debug(&format!(
75 "Initializing Python bridge with: {}",
76 python_path.display()
77 ));
78
79 let pyo3_start = std::time::Instant::now();
80 pyo3::Python::initialize();
81 logger::debug(&format!(
82 "pyo3::Python::initialize took: {:?}",
83 pyo3_start.elapsed()
84 ));
85
86 pyo3::Python::attach(|py| {
89 let sys = PyModule::import(py, "sys")
90 .map_err(|e| BridgeError::Python(format!("Failed to import sys module: {}", e)))?;
91 sys.setattr("dont_write_bytecode", false).map_err(|e| {
92 BridgeError::Python(format!("Failed to enable bytecode generation: {}", e))
93 })?;
94 Ok::<(), BridgeError>(())
95 })?;
96 logger::debug("Enabled Python bytecode generation");
97
98 logger::debug(&format!(
100 "site_packages: {}, exists: {}",
101 site_packages.display(),
102 site_packages.exists()
103 ));
104
105 pyo3::Python::attach(|py| {
106 let site = PyModule::import(py, "site")
107 .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
108 site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
109 .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
110 Ok::<(), BridgeError>(())
111 })?;
112
113 let sitedir_start = std::time::Instant::now();
114 logger::debug(&format!(
115 "Site packages setup completed in: {:?}",
116 sitedir_start.elapsed()
117 ));
118
119 let version_start = std::time::Instant::now();
121 detect_and_store_python_version()?;
122 logger::debug(&format!(
123 "Python version detection took: {:?}",
124 version_start.elapsed()
125 ));
126
127 configure_python_cache(&cache_path)?;
128
129 logger::debug("Starting Python logging configuration...");
134 if let Err(e) = Self::configure_python_logging() {
135 logger::warn(&format!("Python logging configuration failed: {}", e));
136 }
137 logger::debug("Python logging configuration completed");
138
139 logger::debug(&format!(
140 "Total bridge initialization took: {:?}",
141 start_time.elapsed()
142 ));
143 Ok(Bridge {})
144 }
145
146 fn configure_python_logging() -> Result<(), BridgeError> {
148 let log_file = logger::get_log_path_string();
149 let verbosity = logger::get_verbosity();
150 let log_level = match verbosity {
151 0 => "WARNING",
152 1 => "INFO",
153 2 => "DEBUG",
154 _ => "TRACE",
155 };
156
157 let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
159
160 let enable_console = logger::get_log_python();
162
163 logger::debug(&format!(
164 "Configuring Python logging with level={}, file={}, enable_console={}",
165 log_level, log_file, enable_console
166 ));
167
168 pyo3::Python::attach(|py| {
169 let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
170 logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
171 BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
172 })?;
173 let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
174 logger::warn(&format!("Failed to get setup_logging function: {}", e));
175 BridgeError::Python(format!("setup_logging not found: {}", e))
176 })?;
177 let kwargs = pyo3::types::PyDict::new(py);
178 kwargs.set_item("level", log_level)?;
179 kwargs.set_item("log_file", &log_file)?;
180 kwargs.set_item("fmt", fmt)?;
181 kwargs.set_item("enable_console_log", enable_console)?;
182 setup_logging.call((), Some(&kwargs))?;
183
184 let loguru = PyModule::import(py, "loguru")?;
186 let logger = loguru.getattr("logger")?;
187 logger.call_method1("enable", ("r2x_core",))?;
188 logger.call_method1("enable", ("r2x_reeds",))?;
189 logger.call_method1("enable", ("r2x_plexos",))?;
190 logger.call_method1("enable", ("r2x_sienna",))?;
191
192 Ok::<(), BridgeError>(())
193 })
194 }
195}
196
197fn detect_and_store_python_version() -> Result<(), BridgeError> {
206 let mut config = Config::load()
207 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
208
209 let version_str = pyo3::Python::attach(|py| {
211 let sys = PyModule::import(py, "sys")
212 .map_err(|e| BridgeError::Python(format!("Failed to import sys: {}", e)))?;
213 let version_info = sys
214 .getattr("version_info")
215 .map_err(|e| BridgeError::Python(format!("Failed to get version_info: {}", e)))?;
216
217 let major = version_info
218 .getattr("major")
219 .map_err(|e| BridgeError::Python(format!("Failed to get major: {}", e)))?
220 .extract::<i32>()
221 .map_err(|e| BridgeError::Python(format!("Failed to extract major: {}", e)))?;
222
223 let minor = version_info
224 .getattr("minor")
225 .map_err(|e| BridgeError::Python(format!("Failed to get minor: {}", e)))?
226 .extract::<i32>()
227 .map_err(|e| BridgeError::Python(format!("Failed to extract minor: {}", e)))?;
228
229 Ok::<String, BridgeError>(format!("{}.{}", major, minor))
230 })?;
231
232 logger::debug(&format!("Detected Python version: {}", version_str));
233
234 if let Some(ref config_version) = config.python_version {
236 if config_version == &version_str {
237 return Ok(());
239 } else {
240 logger::warn(&format!(
242 "Python version mismatch: binary was compiled with {}, but config shows {}. Updating config to match compiled version.",
243 version_str, config_version
244 ));
245 }
246 } else {
247 logger::debug("First time detecting Python version for this binary");
249 }
250
251 config.python_version = Some(version_str.clone());
253 config
254 .save()
255 .map_err(|e| BridgeError::Initialization(format!("Failed to save config: {}", e)))?;
256
257 logger::info(&format!("Python version {} stored in config", version_str));
258
259 Ok(())
260}
261
262fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
263 std::fs::create_dir_all(cache_path).map_err(|e| {
264 BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
265 })?;
266 std::env::set_var("R2X_CACHE_PATH", cache_path);
267
268 let cache_path_escaped = cache_path.replace('\\', "\\\\");
269 pyo3::Python::attach(|py| {
270 let patch_code = format!(
271 r#"from pathlib import Path
272_R2X_CACHE_PATH = Path(r"{cache}")
273
274def _r2x_cache_path_override():
275 return _R2X_CACHE_PATH
276"#,
277 cache = cache_path_escaped
278 );
279
280 let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
281 BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
282 })?;
283 let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
284 let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
285 let patch_module = PyModule::from_code(
286 py,
287 code_cstr.as_c_str(),
288 filename.as_c_str(),
289 module_name.as_c_str(),
290 )
291 .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
292
293 let override_fn = patch_module
294 .getattr("_r2x_cache_path_override")
295 .map_err(|e| {
296 BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
297 })?;
298
299 let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
300 BridgeError::Python(format!(
301 "Failed to import r2x_core.utils.file_operations: {}",
302 e
303 ))
304 })?;
305
306 file_ops
307 .setattr("get_r2x_cache_path", override_fn)
308 .map_err(|e| BridgeError::Python(format!("Failed to override cache path: {}", e)))?;
309
310 Ok::<(), BridgeError>(())
311 })?;
312
313 Ok(())
314}
315
316pub fn configure_python_venv() -> Result<PythonEnvironment, BridgeError> {
318 let mut config = Config::load()
319 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
320
321 let venv_path = PathBuf::from(config.get_venv_path());
322
323 let python_path_result = resolve_python_path(&venv_path);
324
325 if python_path_result.is_err() {
326 logger::debug("Could not resolve Python path");
327 }
328
329 let mut python_path = python_path_result.unwrap_or_else(|_| PathBuf::new());
330
331 if !venv_path.exists() {
333 logger::step(&format!(
334 "Creating Python virtual environment at: {}",
335 venv_path.display()
336 ));
337
338 let uv_path = config
339 .ensure_uv_path()
340 .map_err(|e| BridgeError::Initialization(format!("Failed to ensure uv: {}", e)))?;
341
342 let python_version = config.python_version.as_deref().unwrap_or("3.12");
344
345 let output = Command::new(&uv_path)
346 .arg("venv")
347 .arg(&venv_path)
348 .arg("--python")
349 .arg(python_version)
350 .output()?;
351
352 logger::capture_output(&format!("uv venv --python {}", python_version), &output);
353
354 if !output.status.success() {
355 return Err(BridgeError::Initialization(
356 "Failed to create Python virtual environment".to_string(),
357 ));
358 }
359
360 python_path = resolve_python_path(&venv_path).unwrap_or_else(|_| PathBuf::new());
361
362 if python_path.as_os_str().is_empty() || !python_path.exists() {
363 if let Ok(entries) = std::fs::read_dir(venv_path.join(PYTHON_BIN_DIR_NAME)) {
364 let names: Vec<String> = entries
365 .filter_map(|e| e.ok())
366 .filter_map(|e| e.file_name().into_string().ok())
367 .collect();
368 logger::debug(&format!("Venv bin contents after creation: {:?}", names));
369 }
370 return Err(BridgeError::Initialization(
371 "Failed to locate Python executable after creating venv".to_string(),
372 ));
373 }
374 }
375
376 if python_path.as_os_str().is_empty() || !python_path.exists() {
377 logger::warn("Python binary not found in configured venv; attempting system fallback");
378 if let Some((fallback, home)) = find_system_python() {
379 return Ok(PythonEnvironment {
380 interpreter: fallback,
381 python_home: Some(home),
382 });
383 }
384
385 return Err(BridgeError::Initialization(
386 "Failed to locate a usable Python interpreter".to_string(),
387 ));
388 }
389
390 let python_home = resolve_python_home(&venv_path);
391
392 Ok(PythonEnvironment {
393 interpreter: python_path,
394 python_home,
395 })
396}
397
398fn configure_embedded_python_env(python_home: &Path, site_packages: &Path) {
399 let home = python_home.to_string_lossy().to_string();
400 env::set_var("PYTHONHOME", &home);
401 logger::debug(&format!("Set PYTHONHOME={}", home));
402
403 let mut paths = vec![site_packages.to_path_buf()];
404 if let Some(existing) = env::var_os("PYTHONPATH") {
405 if !existing.is_empty() {
406 paths.extend(env::split_paths(&existing));
407 }
408 }
409 if let Ok(joined) = env::join_paths(paths) {
410 env::set_var("PYTHONPATH", &joined);
411 logger::debug(&format!(
412 "Updated PYTHONPATH to include {}",
413 site_packages.display()
414 ));
415 }
416}
417
418fn resolve_python_home(venv_path: &Path) -> Option<PathBuf> {
419 let cfg_path = venv_path.join("pyvenv.cfg");
420 let contents = fs::read_to_string(&cfg_path).ok()?;
421 for line in contents.lines() {
422 let trimmed = line.trim();
423 if trimmed.starts_with("home") {
424 let parts: Vec<_> = trimmed.splitn(2, '=').collect();
425 if parts.len() == 2 {
426 let mut path = PathBuf::from(parts[1].trim());
427 if path.ends_with("bin") || path.ends_with("Scripts") {
428 path = path.parent().map(PathBuf::from).unwrap_or(path);
429 }
430 logger::debug(&format!(
431 "Resolved base Python home {} from {}",
432 path.display(),
433 cfg_path.display()
434 ));
435 return Some(path);
436 }
437 }
438 }
439 logger::debug(&format!(
440 "Failed to resolve base Python home from {}",
441 cfg_path.display()
442 ));
443 None
444}
445
446fn detect_python_runtime(
447 python_bin: &Path,
448) -> Option<(PathBuf, i32, i32)> {
449 let output = Command::new(python_bin)
450 .arg("-c")
451 .arg(
452 "import sys\nprint(sys.base_prefix)\nprint(sys.version_info.major)\nprint(sys.version_info.minor)",
453 )
454 .output()
455 .ok()?;
456 if !output.status.success() {
457 logger::debug(&format!(
458 "Failed to probe python runtime (status {:?})",
459 output.status.code()
460 ));
461 return None;
462 }
463 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
464 let mut lines = stdout
465 .lines()
466 .map(|s| s.trim().to_string())
467 .collect::<Vec<_>>();
468 if lines.len() < 3 {
469 return None;
470 }
471 let minor = lines
472 .pop()
473 .and_then(|v| v.parse::<i32>().ok())
474 .unwrap_or(0);
475 let major = lines
476 .pop()
477 .and_then(|v| v.parse::<i32>().ok())
478 .unwrap_or(0);
479 let prefix = PathBuf::from(lines.pop().unwrap_or_default());
480 Some((prefix, major, minor))
481}
482
483fn find_system_python() -> Option<(PathBuf, PathBuf)> {
484 for candidate in SYSTEM_PYTHON_CANDIDATES {
485 if let Ok(path) = which(candidate) {
486 if let Some((home, major, minor)) = detect_python_runtime(&path) {
487 if major == REQUIRED_PYTHON_MAJOR && minor == REQUIRED_PYTHON_MINOR {
488 logger::warn(&format!(
489 "Falling back to system Python at {}",
490 path.display()
491 ));
492 return Some((path, home));
493 } else {
494 logger::debug(&format!(
495 "Skipping system python {} (version {}.{})",
496 path.display(),
497 major,
498 minor
499 ));
500 }
501 }
502 }
503 }
504 None
505}