r2x_python/
initialization.rs

1//! Python environment initialization and setup
2//!
3//! This module handles all Python interpreter initialization, virtual environment
4//! configuration, and environment setup required before the bridge can be used.
5
6use 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    /// Get or initialize the bridge singleton
22    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    /// Initialize Python interpreter and configure environment
30    ///
31    /// This performs:
32    /// - Configure venv/python environment
33    /// - Add venv site-packages to sys.path
34    /// - Ensure r2x-core is installed
35    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        // Enable Python bytecode generation for faster subsequent imports
59        // This overrides PYTHONDONTWRITEBYTECODE if set in the environment
60        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        // Add site-packages from venv to sys.path so imports work as expected
71        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        // Detect and store the compiled Python version in config if not already set
95        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        // r2x_core is now installed during venv creation, so no need to check here
105
106        // Configure Python loguru to write to the same log file as Rust
107        // Python logs always go to file, --log-python flag controls console output
108        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    /// Configure Python loguru logging to integrate with Rust logger
122    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        // Format to match Rust logger: [YYYY-MM-DD HH:MM:SS] [PYTHON] LEVEL message
133        let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
134
135        // Check if Python logs should be shown on console
136        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            // Explicitly enable logging for r2x modules
160            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
172/// Helper: get python3.X directory inside venv lib/
173/// Detect the Python version from the embedded interpreter and store it in config
174///
175/// This function:
176/// 1. Gets the Python version from sys.version_info (the compiled/embedded version)
177/// 2. Compares it with what's stored in config
178/// 3. If missing or mismatched, updates config to the actual version
179/// 4. Logs warnings if there's a mismatch (indicates config was manually edited)
180fn 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    // Get Python version from sys.version_info (the actual compiled version)
185    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    // Check if config version matches detected version
210    if let Some(ref config_version) = config.python_version {
211        if config_version == &version_str {
212            // Versions match, nothing to do
213            return Ok(());
214        } else {
215            // Mismatch detected - config was likely manually edited
216            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        // First time detection
223        logger::debug("First time detecting Python version for this binary");
224    }
225
226    // Store/update the actual compiled version in config
227    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
291/// Configure the Python virtual environment before PyO3 initialization
292pub 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    // Create venv if it doesn't exist
307    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        // Use the Python version from config, or default to 3.12
318        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}