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 logger::debug("Initializing PyO3...");
89 let pyo3_start = std::time::Instant::now();
90 pyo3::Python::initialize();
91 logger::debug(&format!(
92 "pyo3::Python::initialize took: {:?}",
93 pyo3_start.elapsed()
94 ));
95
96 pyo3::Python::attach(|py| {
98 let sys = PyModule::import(py, "sys")
99 .map_err(|e| BridgeError::Python(format!("Failed to import sys module: {}", e)))?;
100 sys.setattr("dont_write_bytecode", false).map_err(|e| {
101 BridgeError::Python(format!("Failed to enable bytecode generation: {}", e))
102 })?;
103 Ok::<(), BridgeError>(())
104 })?;
105 logger::debug("Enabled Python bytecode generation");
106
107 pyo3::Python::attach(|py| {
109 let site = PyModule::import(py, "site")
110 .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
111 site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
112 .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
113 Ok::<(), BridgeError>(())
114 })?;
115
116 let cache_path = config.ensure_cache_path().map_err(|e| {
118 BridgeError::Initialization(format!("Failed to ensure cache path: {}", e))
119 })?;
120 Self::configure_python_cache(&cache_path)?;
121
122 if let Err(e) = Self::configure_python_logging() {
124 logger::warn(&format!("Python logging configuration failed: {}", e));
125 }
126
127 logger::debug(&format!(
128 "Total bridge initialization took: {:?}",
129 start_time.elapsed()
130 ));
131
132 Ok(Bridge { _marker: () })
133 }
134
135 fn create_venv(config: &Config, venv_path: &PathBuf) -> Result<(), BridgeError> {
139 logger::step(&format!(
140 "Creating Python virtual environment at: {}",
141 venv_path.display()
142 ));
143
144 let python_version = get_compiled_python_version();
145
146 if let Some(ref uv_path) = config.uv_path {
148 let output = Command::new(uv_path)
149 .arg("venv")
150 .arg(venv_path)
151 .arg("--python")
152 .arg(&python_version)
153 .output()?;
154
155 if output.status.success() {
156 logger::success("Virtual environment created successfully");
157 return Ok(());
158 }
159
160 let stderr = String::from_utf8_lossy(&output.stderr);
161 logger::debug(&format!("uv venv failed: {}", stderr));
162 }
163
164 let python_cmd = format!("python{}", python_version);
166 let output = Command::new(&python_cmd)
167 .args(["-m", "venv"])
168 .arg(venv_path)
169 .output();
170
171 if let Ok(output) = output {
172 if output.status.success() {
173 logger::success("Virtual environment created successfully");
174 return Ok(());
175 }
176 }
177
178 let output = Command::new("python3")
180 .args(["-m", "venv"])
181 .arg(venv_path)
182 .output()?;
183
184 if !output.status.success() {
185 let stderr = String::from_utf8_lossy(&output.stderr);
186 return Err(BridgeError::Initialization(format!(
187 "Failed to create virtual environment: {}",
188 stderr
189 )));
190 }
191
192 logger::success("Virtual environment created successfully");
193 Ok(())
194 }
195
196 fn configure_python_path(site_packages: &Path) {
198 let mut paths = vec![site_packages.to_path_buf()];
199 if let Some(existing) = env::var_os("PYTHONPATH") {
200 if !existing.is_empty() {
201 paths.extend(env::split_paths(&existing));
202 }
203 }
204 if let Ok(joined) = env::join_paths(paths) {
205 env::set_var("PYTHONPATH", &joined);
206 logger::debug(&format!(
207 "Updated PYTHONPATH to include {}",
208 site_packages.display()
209 ));
210 }
211 }
212
213 fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
215 std::fs::create_dir_all(cache_path).map_err(|e| {
216 BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
217 })?;
218 env::set_var("R2X_CACHE_PATH", cache_path);
219
220 let cache_path_escaped = cache_path.replace('\\', "\\\\");
221 pyo3::Python::attach(|py| {
222 let patch_code = format!(
223 r#"from pathlib import Path
224_R2X_CACHE_PATH = Path(r"{cache}")
225
226def _r2x_cache_path_override():
227 return _R2X_CACHE_PATH
228"#,
229 cache = cache_path_escaped
230 );
231
232 let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
233 BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
234 })?;
235 let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
236 let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
237 let patch_module = PyModule::from_code(
238 py,
239 code_cstr.as_c_str(),
240 filename.as_c_str(),
241 module_name.as_c_str(),
242 )
243 .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
244
245 let override_fn = patch_module
246 .getattr("_r2x_cache_path_override")
247 .map_err(|e| {
248 BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
249 })?;
250
251 let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
252 BridgeError::Python(format!(
253 "Failed to import r2x_core.utils.file_operations: {}",
254 e
255 ))
256 })?;
257
258 file_ops
259 .setattr("get_r2x_cache_path", override_fn)
260 .map_err(|e| {
261 BridgeError::Python(format!("Failed to override cache path: {}", e))
262 })?;
263
264 Ok::<(), BridgeError>(())
265 })?;
266
267 Ok(())
268 }
269
270 fn configure_python_logging() -> Result<(), BridgeError> {
272 let log_python = logger::get_log_python();
273 if !log_python {
274 return Ok(());
275 }
276
277 let verbosity = logger::get_verbosity();
278 logger::debug(&format!(
279 "Configuring Python logging with verbosity={}",
280 verbosity
281 ));
282
283 pyo3::Python::attach(|py| {
284 let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
285 logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
286 BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
287 })?;
288 let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
289 logger::warn(&format!("Failed to get setup_logging function: {}", e));
290 BridgeError::Python(format!("setup_logging not found: {}", e))
291 })?;
292 setup_logging.call1((verbosity,))?;
293
294 let loguru = PyModule::import(py, "loguru")?;
295 let logger_obj = loguru.getattr("logger")?;
296 logger_obj.call_method1("enable", ("r2x_core",))?;
297 logger_obj.call_method1("enable", ("r2x_reeds",))?;
298 logger_obj.call_method1("enable", ("r2x_plexos",))?;
299 logger_obj.call_method1("enable", ("r2x_sienna",))?;
300
301 Ok::<(), BridgeError>(())
302 })
303 }
304
305 pub fn reconfigure_logging_for_plugin(_plugin_name: &str) -> Result<(), BridgeError> {
307 Self::configure_python_logging()
308 }
309}
310
311fn resolve_python_home(venv_path: &Path) -> Result<PathBuf, BridgeError> {
323 let pyvenv_cfg = venv_path.join("pyvenv.cfg");
324
325 if !pyvenv_cfg.exists() {
326 return Err(BridgeError::Initialization(format!(
327 "pyvenv.cfg not found in venv: {}",
328 venv_path.display()
329 )));
330 }
331
332 let content = fs::read_to_string(&pyvenv_cfg)
333 .map_err(|e| BridgeError::Initialization(format!("Failed to read pyvenv.cfg: {}", e)))?;
334
335 for line in content.lines() {
336 let line = line.trim();
337 if line.starts_with("home") {
338 if let Some((_key, value)) = line.split_once('=') {
339 let home_bin = PathBuf::from(value.trim());
340 if let Some(parent) = home_bin.parent() {
342 logger::debug(&format!(
343 "Resolved PYTHONHOME from pyvenv.cfg: {}",
344 parent.display()
345 ));
346 return Ok(parent.to_path_buf());
347 }
348 return Ok(home_bin);
350 }
351 }
352 }
353
354 Err(BridgeError::Initialization(format!(
355 "Could not find 'home' in pyvenv.cfg: {}",
356 pyvenv_cfg.display()
357 )))
358}
359
360fn get_compiled_python_version() -> String {
365 "3.12".to_string()
369}
370
371pub fn configure_python_venv() -> Result<PythonEnvCompat, BridgeError> {
373 let config = Config::load()
374 .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
375
376 let venv_path = PathBuf::from(config.get_venv_path());
377
378 let interpreter = resolve_python_path(&venv_path)?;
379 let python_home = resolve_python_home(&venv_path).ok();
380
381 Ok(PythonEnvCompat {
382 interpreter,
383 python_home,
384 })
385}
386
387#[derive(Debug, Clone)]
389pub struct PythonEnvCompat {
390 pub interpreter: PathBuf,
391 pub python_home: Option<PathBuf>,
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_bridge_struct() {
400 let _bridge = Bridge { _marker: () };
402 }
403
404 #[test]
405 fn test_get_compiled_python_version() {
406 let version = get_compiled_python_version();
407 assert!(version.starts_with("3."));
408 }
409}