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::path::PathBuf;
14use std::process::Command;
15
16pub struct Bridge {}
17
18static BRIDGE_INSTANCE: OnceCell<Result<Bridge, BridgeError>> = OnceCell::new();
19
20impl Bridge {
21 pub fn get() -> Result<&'static Bridge, BridgeError> {
23 match BRIDGE_INSTANCE.get_or_init(Bridge::initialize) {
24 Ok(bridge) => Ok(bridge),
25 Err(e) => Err(BridgeError::Initialization(format!("{}", e))),
26 }
27 }
28
29 fn initialize() -> Result<Bridge, BridgeError> {
36 let start_time = std::time::Instant::now();
37
38 let python_path = configure_python_venv()?;
39
40 let mut config = Config::load()
41 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
42 let cache_path = config.ensure_cache_path().map_err(|e| {
43 BridgeError::Initialization(format!("Failed to ensure cache path: {}", e))
44 })?;
45
46 logger::debug(&format!(
47 "Initializing Python bridge with: {}",
48 python_path.display()
49 ));
50
51 let pyo3_start = std::time::Instant::now();
52 pyo3::Python::initialize();
53 logger::debug(&format!(
54 "pyo3::Python::initialize took: {:?}",
55 pyo3_start.elapsed()
56 ));
57
58 pyo3::Python::attach(|py| {
61 let sys = PyModule::import(py, "sys")
62 .map_err(|e| BridgeError::Python(format!("Failed to import sys module: {}", e)))?;
63 sys.setattr("dont_write_bytecode", false).map_err(|e| {
64 BridgeError::Python(format!("Failed to enable bytecode generation: {}", e))
65 })?;
66 Ok::<(), BridgeError>(())
67 })?;
68 logger::debug("Enabled Python bytecode generation");
69
70 let venv_path = PathBuf::from(config.get_venv_path());
72 let site_packages = resolve_site_package_path(&venv_path)?;
73
74 logger::debug(&format!(
75 "site_packages: {}, exists: {}",
76 site_packages.display(),
77 site_packages.exists()
78 ));
79
80 pyo3::Python::attach(|py| {
81 let site = PyModule::import(py, "site")
82 .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
83 site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
84 .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
85 Ok::<(), BridgeError>(())
86 })?;
87
88 let sitedir_start = std::time::Instant::now();
89 logger::debug(&format!(
90 "Site packages setup completed in: {:?}",
91 sitedir_start.elapsed()
92 ));
93
94 let version_start = std::time::Instant::now();
96 detect_and_store_python_version()?;
97 logger::debug(&format!(
98 "Python version detection took: {:?}",
99 version_start.elapsed()
100 ));
101
102 configure_python_cache(&cache_path)?;
103
104 logger::debug("Starting Python logging configuration...");
109 if let Err(e) = Self::configure_python_logging() {
110 logger::warn(&format!("Python logging configuration failed: {}", e));
111 }
112 logger::debug("Python logging configuration completed");
113
114 logger::debug(&format!(
115 "Total bridge initialization took: {:?}",
116 start_time.elapsed()
117 ));
118 Ok(Bridge {})
119 }
120
121 fn configure_python_logging() -> Result<(), BridgeError> {
123 let log_file = logger::get_log_path_string();
124 let verbosity = logger::get_verbosity();
125 let log_level = match verbosity {
126 0 => "WARNING",
127 1 => "INFO",
128 2 => "DEBUG",
129 _ => "TRACE",
130 };
131
132 let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
134
135 let enable_console = logger::get_log_python();
137
138 logger::debug(&format!(
139 "Configuring Python logging with level={}, file={}, enable_console={}",
140 log_level, log_file, enable_console
141 ));
142
143 pyo3::Python::attach(|py| {
144 let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
145 logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
146 BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
147 })?;
148 let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
149 logger::warn(&format!("Failed to get setup_logging function: {}", e));
150 BridgeError::Python(format!("setup_logging not found: {}", e))
151 })?;
152 let kwargs = pyo3::types::PyDict::new(py);
153 kwargs.set_item("level", log_level)?;
154 kwargs.set_item("log_file", &log_file)?;
155 kwargs.set_item("fmt", fmt)?;
156 kwargs.set_item("enable_console_log", enable_console)?;
157 setup_logging.call((), Some(&kwargs))?;
158
159 let loguru = PyModule::import(py, "loguru")?;
161 let logger = loguru.getattr("logger")?;
162 logger.call_method1("enable", ("r2x_core",))?;
163 logger.call_method1("enable", ("r2x_reeds",))?;
164 logger.call_method1("enable", ("r2x_plexos",))?;
165 logger.call_method1("enable", ("r2x_sienna",))?;
166
167 Ok::<(), BridgeError>(())
168 })
169 }
170}
171
172fn detect_and_store_python_version() -> Result<(), BridgeError> {
181 let mut config = Config::load()
182 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
183
184 let version_str = pyo3::Python::attach(|py| {
186 let sys = PyModule::import(py, "sys")
187 .map_err(|e| BridgeError::Python(format!("Failed to import sys: {}", e)))?;
188 let version_info = sys
189 .getattr("version_info")
190 .map_err(|e| BridgeError::Python(format!("Failed to get version_info: {}", e)))?;
191
192 let major = version_info
193 .getattr("major")
194 .map_err(|e| BridgeError::Python(format!("Failed to get major: {}", e)))?
195 .extract::<i32>()
196 .map_err(|e| BridgeError::Python(format!("Failed to extract major: {}", e)))?;
197
198 let minor = version_info
199 .getattr("minor")
200 .map_err(|e| BridgeError::Python(format!("Failed to get minor: {}", e)))?
201 .extract::<i32>()
202 .map_err(|e| BridgeError::Python(format!("Failed to extract minor: {}", e)))?;
203
204 Ok::<String, BridgeError>(format!("{}.{}", major, minor))
205 })?;
206
207 logger::debug(&format!("Detected Python version: {}", version_str));
208
209 if let Some(ref config_version) = config.python_version {
211 if config_version == &version_str {
212 return Ok(());
214 } else {
215 logger::warn(&format!(
217 "Python version mismatch: binary was compiled with {}, but config shows {}. Updating config to match compiled version.",
218 version_str, config_version
219 ));
220 }
221 } else {
222 logger::debug("First time detecting Python version for this binary");
224 }
225
226 config.python_version = Some(version_str.clone());
228 config
229 .save()
230 .map_err(|e| BridgeError::Initialization(format!("Failed to save config: {}", e)))?;
231
232 logger::info(&format!("Python version {} stored in config", version_str));
233
234 Ok(())
235}
236
237fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
238 std::fs::create_dir_all(cache_path).map_err(|e| {
239 BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
240 })?;
241 std::env::set_var("R2X_CACHE_PATH", cache_path);
242
243 let cache_path_escaped = cache_path.replace('\\', "\\\\");
244 pyo3::Python::attach(|py| {
245 let patch_code = format!(
246 r#"from pathlib import Path
247_R2X_CACHE_PATH = Path(r"{cache}")
248
249def _r2x_cache_path_override():
250 return _R2X_CACHE_PATH
251"#,
252 cache = cache_path_escaped
253 );
254
255 let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
256 BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
257 })?;
258 let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
259 let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
260 let patch_module = PyModule::from_code(
261 py,
262 code_cstr.as_c_str(),
263 filename.as_c_str(),
264 module_name.as_c_str(),
265 )
266 .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
267
268 let override_fn = patch_module
269 .getattr("_r2x_cache_path_override")
270 .map_err(|e| {
271 BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
272 })?;
273
274 let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
275 BridgeError::Python(format!(
276 "Failed to import r2x_core.utils.file_operations: {}",
277 e
278 ))
279 })?;
280
281 file_ops
282 .setattr("get_r2x_cache_path", override_fn)
283 .map_err(|e| BridgeError::Python(format!("Failed to override cache path: {}", e)))?;
284
285 Ok::<(), BridgeError>(())
286 })?;
287
288 Ok(())
289}
290
291pub fn configure_python_venv() -> Result<PathBuf, BridgeError> {
293 let mut config = Config::load()
294 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
295
296 let venv_path = PathBuf::from(config.get_venv_path());
297
298 let python_path_result = resolve_python_path(&venv_path);
299
300 if python_path_result.is_err() {
301 logger::debug("Could not resolve Python path");
302 }
303
304 let python_path = python_path_result.unwrap_or_else(|_| PathBuf::new());
305
306 if !venv_path.exists() || !python_path.exists() {
308 logger::step(&format!(
309 "Creating Python virtual environment at: {}",
310 venv_path.display()
311 ));
312
313 let uv_path = config
314 .ensure_uv_path()
315 .map_err(|e| BridgeError::Initialization(format!("Failed to ensure uv: {}", e)))?;
316
317 let python_version = config.python_version.as_deref().unwrap_or("3.12");
319
320 let output = Command::new(&uv_path)
321 .arg("venv")
322 .arg(&venv_path)
323 .arg("--python")
324 .arg(python_version)
325 .output()?;
326
327 logger::capture_output(&format!("uv venv --python {}", python_version), &output);
328
329 if !output.status.success() {
330 return Err(BridgeError::Initialization(
331 "Failed to create Python virtual environment".to_string(),
332 ));
333 }
334 }
335
336 Ok(python_path)
337}