1use super::utils::*;
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
73 let lib_dir = venv_path.join(PYTHON_LIB_DIR);
74 logger::debug(&format!(
75 "lib_dir: {}, exists: {}",
76 lib_dir.display(),
77 lib_dir.exists()
78 ));
79 if !lib_dir.exists() {
80 return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
81 }
82
83 use std::fs;
85 let python_version_dir = fs::read_dir(&lib_dir)
86 .map_err(|e| {
87 BridgeError::Initialization(format!("Failed to read lib directory: {}", e))
88 })?
89 .filter_map(|e| e.ok())
90 .find(|e| e.file_name().to_string_lossy().starts_with("python"))
91 .ok_or_else(|| {
92 BridgeError::Initialization("No python3.X directory found in venv/lib".to_string())
93 })?;
94
95 let site_packages = python_version_dir.path().join(SITE_PACKAGES);
96 logger::debug(&format!(
97 "site_packages: {}, exists: {}",
98 site_packages.display(),
99 site_packages.exists()
100 ));
101
102 pyo3::Python::attach(|py| {
103 let site = PyModule::import(py, "site")
104 .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
105 site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
106 .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
107 Ok::<(), BridgeError>(())
108 })?;
109
110 let sitedir_start = std::time::Instant::now();
111 logger::debug(&format!(
112 "Site packages setup completed in: {:?}",
113 sitedir_start.elapsed()
114 ));
115
116 let version_start = std::time::Instant::now();
118 detect_and_store_python_version()?;
119 logger::debug(&format!(
120 "Python version detection took: {:?}",
121 version_start.elapsed()
122 ));
123
124 configure_python_cache(&cache_path)?;
125
126 logger::debug("Starting Python logging configuration...");
131 if let Err(e) = Self::configure_python_logging() {
132 logger::warn(&format!("Python logging configuration failed: {}", e));
133 }
134 logger::debug("Python logging configuration completed");
135
136 logger::debug(&format!(
137 "Total bridge initialization took: {:?}",
138 start_time.elapsed()
139 ));
140 Ok(Bridge {})
141 }
142
143 fn configure_python_logging() -> Result<(), BridgeError> {
145 let log_file = logger::get_log_path_string();
146 let verbosity = logger::get_verbosity();
147 let log_level = match verbosity {
148 0 => "WARNING",
149 1 => "INFO",
150 2 => "DEBUG",
151 _ => "TRACE",
152 };
153
154 let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
156
157 let enable_console = logger::get_log_python();
159
160 logger::debug(&format!(
161 "Configuring Python logging with level={}, file={}, enable_console={}",
162 log_level, log_file, enable_console
163 ));
164
165 pyo3::Python::attach(|py| {
166 let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
167 logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
168 BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
169 })?;
170 let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
171 logger::warn(&format!("Failed to get setup_logging function: {}", e));
172 BridgeError::Python(format!("setup_logging not found: {}", e))
173 })?;
174 let kwargs = pyo3::types::PyDict::new(py);
175 kwargs.set_item("level", log_level)?;
176 kwargs.set_item("log_file", &log_file)?;
177 kwargs.set_item("fmt", fmt)?;
178 kwargs.set_item("enable_console_log", enable_console)?;
179 setup_logging.call((), Some(&kwargs))?;
180
181 let loguru = PyModule::import(py, "loguru")?;
183 let logger = loguru.getattr("logger")?;
184 logger.call_method1("enable", ("r2x_core",))?;
185 logger.call_method1("enable", ("r2x_reeds",))?;
186 logger.call_method1("enable", ("r2x_plexos",))?;
187 logger.call_method1("enable", ("r2x_sienna",))?;
188
189 Ok::<(), BridgeError>(())
190 })
191 }
192}
193
194fn detect_and_store_python_version() -> Result<(), BridgeError> {
203 let mut config = Config::load()
204 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
205
206 let version_str = pyo3::Python::attach(|py| {
208 let sys = PyModule::import(py, "sys")
209 .map_err(|e| BridgeError::Python(format!("Failed to import sys: {}", e)))?;
210 let version_info = sys
211 .getattr("version_info")
212 .map_err(|e| BridgeError::Python(format!("Failed to get version_info: {}", e)))?;
213
214 let major = version_info
215 .getattr("major")
216 .map_err(|e| BridgeError::Python(format!("Failed to get major: {}", e)))?
217 .extract::<i32>()
218 .map_err(|e| BridgeError::Python(format!("Failed to extract major: {}", e)))?;
219
220 let minor = version_info
221 .getattr("minor")
222 .map_err(|e| BridgeError::Python(format!("Failed to get minor: {}", e)))?
223 .extract::<i32>()
224 .map_err(|e| BridgeError::Python(format!("Failed to extract minor: {}", e)))?;
225
226 Ok::<String, BridgeError>(format!("{}.{}", major, minor))
227 })?;
228
229 logger::debug(&format!("Detected Python version: {}", version_str));
230
231 if let Some(ref config_version) = config.python_version {
233 if config_version == &version_str {
234 return Ok(());
236 } else {
237 logger::warn(&format!(
239 "Python version mismatch: binary was compiled with {}, but config shows {}. Updating config to match compiled version.",
240 version_str, config_version
241 ));
242 }
243 } else {
244 logger::debug("First time detecting Python version for this binary");
246 }
247
248 config.python_version = Some(version_str.clone());
250 config
251 .save()
252 .map_err(|e| BridgeError::Initialization(format!("Failed to save config: {}", e)))?;
253
254 logger::info(&format!("Python version {} stored in config", version_str));
255
256 Ok(())
257}
258
259fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
260 std::fs::create_dir_all(cache_path).map_err(|e| {
261 BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
262 })?;
263 std::env::set_var("R2X_CACHE_PATH", cache_path);
264
265 let cache_path_escaped = cache_path.replace('\\', "\\\\");
266 pyo3::Python::attach(|py| {
267 let patch_code = format!(
268 r#"from pathlib import Path
269_R2X_CACHE_PATH = Path(r"{cache}")
270
271def _r2x_cache_path_override():
272 return _R2X_CACHE_PATH
273"#,
274 cache = cache_path_escaped
275 );
276
277 let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
278 BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
279 })?;
280 let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
281 let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
282 let patch_module = PyModule::from_code(
283 py,
284 code_cstr.as_c_str(),
285 filename.as_c_str(),
286 module_name.as_c_str(),
287 )
288 .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
289
290 let override_fn = patch_module
291 .getattr("_r2x_cache_path_override")
292 .map_err(|e| {
293 BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
294 })?;
295
296 let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
297 BridgeError::Python(format!(
298 "Failed to import r2x_core.utils.file_operations: {}",
299 e
300 ))
301 })?;
302
303 file_ops
304 .setattr("get_r2x_cache_path", override_fn)
305 .map_err(|e| BridgeError::Python(format!("Failed to override cache path: {}", e)))?;
306
307 Ok::<(), BridgeError>(())
308 })?;
309
310 Ok(())
311}
312
313pub fn configure_python_venv() -> Result<PathBuf, BridgeError> {
315 let mut config = Config::load()
316 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
317
318 let venv_path = PathBuf::from(config.get_venv_path());
319
320 let python_path = venv_path.join(PYTHON_BIN_DIR).join(PYTHON_EXE);
321
322 if !venv_path.exists() || !python_path.exists() {
324 logger::step(&format!(
325 "Creating Python virtual environment at: {}",
326 venv_path.display()
327 ));
328
329 let uv_path = config
330 .ensure_uv_path()
331 .map_err(|e| BridgeError::Initialization(format!("Failed to ensure uv: {}", e)))?;
332
333 let python_version = config.python_version.as_deref().unwrap_or("3.12");
335
336 let output = Command::new(&uv_path)
337 .arg("venv")
338 .arg(&venv_path)
339 .arg("--python")
340 .arg(python_version)
341 .output()?;
342
343 logger::capture_output(&format!("uv venv --python {}", python_version), &output);
344
345 if !output.status.success() {
346 return Err(BridgeError::Initialization(
347 "Failed to create Python virtual environment".to_string(),
348 ));
349 }
350 }
351
352 Ok(python_path)
353}