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::*;
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
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        // Find the python3.X directory inside lib/
84        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        // Detect and store the compiled Python version in config if not already set
117        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        // r2x_core is now installed during venv creation, so no need to check here
127
128        // Configure Python loguru to write to the same log file as Rust
129        // Python logs always go to file, --log-python flag controls console output
130        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    /// Configure Python loguru logging to integrate with Rust logger
144    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        // Format to match Rust logger: [YYYY-MM-DD HH:MM:SS] [PYTHON] LEVEL message
155        let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
156
157        // Check if Python logs should be shown on console
158        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            // Explicitly enable logging for r2x modules
182            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
194/// Helper: get python3.X directory inside venv lib/
195/// Detect the Python version from the embedded interpreter and store it in config
196///
197/// This function:
198/// 1. Gets the Python version from sys.version_info (the compiled/embedded version)
199/// 2. Compares it with what's stored in config
200/// 3. If missing or mismatched, updates config to the actual version
201/// 4. Logs warnings if there's a mismatch (indicates config was manually edited)
202fn 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    // Get Python version from sys.version_info (the actual compiled version)
207    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    // Check if config version matches detected version
232    if let Some(ref config_version) = config.python_version {
233        if config_version == &version_str {
234            // Versions match, nothing to do
235            return Ok(());
236        } else {
237            // Mismatch detected - config was likely manually edited
238            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        // First time detection
245        logger::debug("First time detecting Python version for this binary");
246    }
247
248    // Store/update the actual compiled version in config
249    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
313/// Configure the Python virtual environment before PyO3 initialization
314pub 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    // Create venv if it doesn't exist
323    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        // Use the Python version from config, or default to 3.12
334        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}