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::env;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18#[cfg(windows)]
19const PYTHON_BIN_DIR_NAME: &str = "Scripts";
20#[cfg(not(windows))]
21const PYTHON_BIN_DIR_NAME: &str = "bin";
22
23/// Get the Python version this binary was compiled against
24/// Returns version string like "3.12"
25/// This requires querying the Python interpreter, so it can only be called
26/// after Python initialization or during runtime checks
27fn get_compiled_python_version() -> Result<String, BridgeError> {
28    // Use the PYO3_PYTHON environment variable set at build time if available
29    // Otherwise, we need to query Python at runtime
30    if let Ok(pyo3_python) = env::var("PYO3_PYTHON") {
31        // Try to extract version from path like "python3.12"
32        if let Some(version) = pyo3_python.split('/').last() {
33            if let Some(ver) = version.strip_prefix("python") {
34                if ver.matches('.').count() >= 1 {
35                    let parts: Vec<&str> = ver.split('.').take(2).collect();
36                    if parts.len() == 2 {
37                        return Ok(format!("{}.{}", parts[0], parts[1]));
38                    }
39                }
40            }
41        }
42    }
43
44    // Fallback: default to 3.12 which is our standard version
45    Ok("3.12".to_string())
46}
47
48/// Ensure the compiled Python version is installed via uv
49/// If not found, automatically install it with user notification
50fn ensure_python_installed(config: &Config) -> Result<(), BridgeError> {
51    let uv_path = config
52        .uv_path
53        .as_ref()
54        .ok_or_else(|| BridgeError::Initialization("UV path not configured".to_string()))?;
55
56    let compiled_version = get_compiled_python_version()?;
57
58    logger::debug(&format!(
59        "Checking if Python {} is installed",
60        compiled_version
61    ));
62
63    // Check if Python is already installed
64    let check_output = Command::new(uv_path)
65        .arg("python")
66        .arg("list")
67        .arg("--only-installed")
68        .arg(&compiled_version)
69        .output()
70        .map_err(|e| {
71            BridgeError::Initialization(format!("Failed to check Python installation: {}", e))
72        })?;
73
74    if check_output.status.success() {
75        let stdout = String::from_utf8_lossy(&check_output.stdout);
76        // Check if the output contains the version (means it's installed)
77        if stdout.contains(&compiled_version) {
78            logger::debug(&format!("Python {} is already installed", compiled_version));
79            return Ok(());
80        }
81    }
82
83    // Python not found, install it
84    logger::step(&format!(
85        "Installing Python {} (required by this binary)...",
86        compiled_version
87    ));
88    logger::info(&format!(
89        "This binary was compiled against Python {}. Installing now.",
90        compiled_version
91    ));
92
93    let install_output = Command::new(uv_path)
94        .arg("python")
95        .arg("install")
96        .arg(&compiled_version)
97        .output()
98        .map_err(|e| {
99            BridgeError::Initialization(format!(
100                "Failed to install Python {}: {}",
101                compiled_version, e
102            ))
103        })?;
104
105    logger::capture_output(
106        &format!("uv python install {}", compiled_version),
107        &install_output,
108    );
109
110    if !install_output.status.success() {
111        let stderr = String::from_utf8_lossy(&install_output.stderr);
112        return Err(BridgeError::Initialization(format!(
113            "Failed to install Python {}.\n\
114            Please ensure uv can access the network.\n\
115            Error: {}",
116            compiled_version, stderr
117        )));
118    }
119
120    logger::success(&format!(
121        "Python {} installed successfully",
122        compiled_version
123    ));
124    Ok(())
125}
126
127pub struct Bridge {}
128
129#[derive(Debug, Clone)]
130pub struct PythonEnvironment {
131    pub interpreter: PathBuf,
132    pub python_home: Option<PathBuf>,
133}
134
135static BRIDGE_INSTANCE: OnceCell<Result<Bridge, BridgeError>> = OnceCell::new();
136
137impl Bridge {
138    /// Get or initialize the bridge singleton
139    pub fn get() -> Result<&'static Bridge, BridgeError> {
140        match BRIDGE_INSTANCE.get_or_init(Bridge::initialize) {
141            Ok(bridge) => Ok(bridge),
142            Err(e) => Err(BridgeError::Initialization(format!("{}", e))),
143        }
144    }
145
146    /// Initialize Python interpreter and configure environment
147    ///
148    /// This performs:
149    /// - Configure venv/python environment
150    /// - Add venv site-packages to sys.path
151    /// - Ensure r2x-core is installed
152    fn initialize() -> Result<Bridge, BridgeError> {
153        let start_time = std::time::Instant::now();
154
155        let python_env = configure_python_venv()?;
156        let python_path = python_env.interpreter.clone();
157
158        let mut config = Config::load()
159            .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
160        let cache_path = config.ensure_cache_path().map_err(|e| {
161            BridgeError::Initialization(format!("Failed to ensure cache path: {}", e))
162        })?;
163        let venv_path = PathBuf::from(config.get_venv_path());
164        let site_packages = resolve_site_package_path(&venv_path)?;
165        if let Some(ref home) = python_env.python_home {
166            configure_embedded_python_env(home, &site_packages);
167        } else {
168            logger::debug("Using default embedded Python search paths");
169        }
170
171        logger::debug(&format!(
172            "Initializing Python bridge with: {}",
173            python_path.display()
174        ));
175
176        let pyo3_start = std::time::Instant::now();
177        pyo3::Python::initialize();
178        logger::debug(&format!(
179            "pyo3::Python::initialize took: {:?}",
180            pyo3_start.elapsed()
181        ));
182
183        // Enable Python bytecode generation for faster subsequent imports
184        // This overrides PYTHONDONTWRITEBYTECODE if set in the environment
185        pyo3::Python::attach(|py| {
186            let sys = PyModule::import(py, "sys")
187                .map_err(|e| BridgeError::Python(format!("Failed to import sys module: {}", e)))?;
188            sys.setattr("dont_write_bytecode", false).map_err(|e| {
189                BridgeError::Python(format!("Failed to enable bytecode generation: {}", e))
190            })?;
191            Ok::<(), BridgeError>(())
192        })?;
193        logger::debug("Enabled Python bytecode generation");
194
195        // Add site-packages from venv to sys.path so imports work as expected
196        logger::debug(&format!(
197            "site_packages: {}, exists: {}",
198            site_packages.display(),
199            site_packages.exists()
200        ));
201
202        pyo3::Python::attach(|py| {
203            let site = PyModule::import(py, "site")
204                .map_err(|e| BridgeError::Python(format!("Failed to import site module: {}", e)))?;
205            site.call_method1("addsitedir", (site_packages.to_str().unwrap(),))
206                .map_err(|e| BridgeError::Python(format!("Failed to add site directory: {}", e)))?;
207            Ok::<(), BridgeError>(())
208        })?;
209
210        let sitedir_start = std::time::Instant::now();
211        logger::debug(&format!(
212            "Site packages setup completed in: {:?}",
213            sitedir_start.elapsed()
214        ));
215
216        // Detect and store the compiled Python version in config if not already set
217        let version_start = std::time::Instant::now();
218        detect_and_store_python_version()?;
219        logger::debug(&format!(
220            "Python version detection took: {:?}",
221            version_start.elapsed()
222        ));
223
224        configure_python_cache(&cache_path)?;
225
226        // r2x_core is now installed during venv creation, so no need to check here
227
228        // Configure Python loguru to write to the same log file as Rust
229        // Python logs always go to file, --log-python flag controls console output
230        logger::debug("Starting Python logging configuration...");
231        if let Err(e) = Self::configure_python_logging() {
232            logger::warn(&format!("Python logging configuration failed: {}", e));
233        }
234        logger::debug("Python logging configuration completed");
235
236        logger::debug(&format!(
237            "Total bridge initialization took: {:?}",
238            start_time.elapsed()
239        ));
240        Ok(Bridge {})
241    }
242
243    /// Configure Python loguru logging to integrate with Rust logger
244    fn configure_python_logging() -> Result<(), BridgeError> {
245        let log_file = logger::get_log_path_string();
246        let verbosity = logger::get_verbosity();
247        let log_level = match verbosity {
248            0 => "WARNING",
249            1 => "INFO",
250            2 => "DEBUG",
251            _ => "TRACE",
252        };
253
254        // Format to match Rust logger: [YYYY-MM-DD HH:MM:SS] [PYTHON] LEVEL message
255        let fmt = "[{time:YYYY-MM-DD HH:mm:ss}] [PYTHON] {level: <8} {message}";
256
257        // Check if Python logs should be shown on console
258        let enable_console = logger::get_log_python();
259
260        logger::debug(&format!(
261            "Configuring Python logging with level={}, file={}, enable_console={}",
262            log_level, log_file, enable_console
263        ));
264
265        pyo3::Python::attach(|py| {
266            let logger_module = PyModule::import(py, "r2x_core.logger").map_err(|e| {
267                logger::warn(&format!("Failed to import r2x_core.logger: {}", e));
268                BridgeError::Import("r2x_core.logger".to_string(), format!("{}", e))
269            })?;
270            let setup_logging = logger_module.getattr("setup_logging").map_err(|e| {
271                logger::warn(&format!("Failed to get setup_logging function: {}", e));
272                BridgeError::Python(format!("setup_logging not found: {}", e))
273            })?;
274            let kwargs = pyo3::types::PyDict::new(py);
275            kwargs.set_item("level", log_level)?;
276            kwargs.set_item("log_file", &log_file)?;
277            kwargs.set_item("fmt", fmt)?;
278            kwargs.set_item("enable_console_log", enable_console)?;
279            setup_logging.call((), Some(&kwargs))?;
280
281            // Explicitly enable logging for r2x modules
282            let loguru = PyModule::import(py, "loguru")?;
283            let logger = loguru.getattr("logger")?;
284            logger.call_method1("enable", ("r2x_core",))?;
285            logger.call_method1("enable", ("r2x_reeds",))?;
286            logger.call_method1("enable", ("r2x_plexos",))?;
287            logger.call_method1("enable", ("r2x_sienna",))?;
288
289            Ok::<(), BridgeError>(())
290        })
291    }
292}
293
294/// Helper: get python3.X directory inside venv lib/
295/// Detect the Python version from the embedded interpreter and store it in config
296///
297/// This function:
298/// 1. Gets the Python version from sys.version_info (the actual running version)
299/// 2. Compares it with the compiled version from PyO3
300/// 3. Warns if there's a mismatch (which could cause runtime issues)
301/// 4. Stores the compiled version in config for future use
302fn detect_and_store_python_version() -> Result<(), BridgeError> {
303    let mut config = Config::load()
304        .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
305
306    // Get the compiled version
307    let compiled_version = get_compiled_python_version()?;
308
309    // Get Python version from sys.version_info (the actual running version)
310    let runtime_version = pyo3::Python::attach(|py| {
311        let sys = PyModule::import(py, "sys")
312            .map_err(|e| BridgeError::Python(format!("Failed to import sys: {}", e)))?;
313        let version_info = sys
314            .getattr("version_info")
315            .map_err(|e| BridgeError::Python(format!("Failed to get version_info: {}", e)))?;
316
317        let major = version_info
318            .getattr("major")
319            .map_err(|e| BridgeError::Python(format!("Failed to get major: {}", e)))?
320            .extract::<i32>()
321            .map_err(|e| BridgeError::Python(format!("Failed to extract major: {}", e)))?;
322
323        let minor = version_info
324            .getattr("minor")
325            .map_err(|e| BridgeError::Python(format!("Failed to get minor: {}", e)))?
326            .extract::<i32>()
327            .map_err(|e| BridgeError::Python(format!("Failed to extract minor: {}", e)))?;
328
329        Ok::<String, BridgeError>(format!("{}.{}", major, minor))
330    })?;
331
332    logger::debug(&format!(
333        "Python versions - compiled: {}, runtime: {}",
334        compiled_version, runtime_version
335    ));
336
337    // Verify that runtime matches compiled version
338    if runtime_version != compiled_version {
339        logger::warn(&format!(
340            "Python version mismatch! Binary compiled with {}, but running {}. This may cause undefined behavior.",
341            compiled_version, runtime_version
342        ));
343    }
344
345    // Store/update the compiled version in config
346    if config.python_version.as_deref() != Some(&compiled_version) {
347        config.python_version = Some(compiled_version.clone());
348        config
349            .save()
350            .map_err(|e| BridgeError::Initialization(format!("Failed to save config: {}", e)))?;
351        logger::debug(&format!(
352            "Stored compiled Python version {} in config",
353            compiled_version
354        ));
355    }
356
357    Ok(())
358}
359
360fn configure_python_cache(cache_path: &str) -> Result<(), BridgeError> {
361    std::fs::create_dir_all(cache_path).map_err(|e| {
362        BridgeError::Initialization(format!("Failed to create cache directory: {}", e))
363    })?;
364    std::env::set_var("R2X_CACHE_PATH", cache_path);
365
366    let cache_path_escaped = cache_path.replace('\\', "\\\\");
367    pyo3::Python::attach(|py| {
368        let patch_code = format!(
369            r#"from pathlib import Path
370_R2X_CACHE_PATH = Path(r"{cache}")
371
372def _r2x_cache_path_override():
373    return _R2X_CACHE_PATH
374"#,
375            cache = cache_path_escaped
376        );
377
378        let code_cstr = std::ffi::CString::new(patch_code).map_err(|e| {
379            BridgeError::Python(format!("Failed to prepare cache override script: {}", e))
380        })?;
381        let filename = std::ffi::CString::new("r2x_cache_patch.py").unwrap();
382        let module_name = std::ffi::CString::new("r2x_cache_patch").unwrap();
383        let patch_module = PyModule::from_code(
384            py,
385            code_cstr.as_c_str(),
386            filename.as_c_str(),
387            module_name.as_c_str(),
388        )
389        .map_err(|e| BridgeError::Python(format!("Failed to build cache override: {}", e)))?;
390
391        let override_fn = patch_module
392            .getattr("_r2x_cache_path_override")
393            .map_err(|e| {
394                BridgeError::Python(format!("Failed to obtain cache override function: {}", e))
395            })?;
396
397        let file_ops = PyModule::import(py, "r2x_core.utils.file_operations").map_err(|e| {
398            BridgeError::Python(format!(
399                "Failed to import r2x_core.utils.file_operations: {}",
400                e
401            ))
402        })?;
403
404        file_ops
405            .setattr("get_r2x_cache_path", override_fn)
406            .map_err(|e| BridgeError::Python(format!("Failed to override cache path: {}", e)))?;
407
408        Ok::<(), BridgeError>(())
409    })?;
410
411    Ok(())
412}
413
414/// Configure the Python virtual environment before PyO3 initialization
415pub fn configure_python_venv() -> Result<PythonEnvironment, BridgeError> {
416    let mut config = Config::load()
417        .map_err(|e| BridgeError::Initialization(format!("Failed to load config: {}", e)))?;
418
419    // Get the compiled Python version - this is what we MUST use
420    let compiled_version = get_compiled_python_version()?;
421
422    // Update config to use the compiled version if it's different
423    if config.python_version.as_deref() != Some(&compiled_version) {
424        logger::debug(&format!(
425            "Updating config to use compiled Python version: {}",
426            compiled_version
427        ));
428        config.python_version = Some(compiled_version.clone());
429        config
430            .save()
431            .map_err(|e| BridgeError::Initialization(format!("Failed to save config: {}", e)))?;
432    }
433
434    // Ensure Python is installed before trying to create venv
435    ensure_python_installed(&config)?;
436
437    let venv_path = PathBuf::from(config.get_venv_path());
438
439    let python_path_result = resolve_python_path(&venv_path);
440
441    if python_path_result.is_err() {
442        logger::debug("Could not resolve Python path");
443    }
444
445    let mut python_path = python_path_result.unwrap_or_else(|_| PathBuf::new());
446
447    // Create venv only when it doesn't exist
448    if !venv_path.exists() {
449        logger::step(&format!(
450            "Creating Python virtual environment at: {}",
451            venv_path.display()
452        ));
453
454        let uv_path = config
455            .ensure_uv_path()
456            .map_err(|e| BridgeError::Initialization(format!("Failed to ensure uv: {}", e)))?;
457
458        logger::info(&format!(
459            "Using Python {} (version this binary was compiled with)",
460            compiled_version
461        ));
462
463        let output = Command::new(&uv_path)
464            .arg("venv")
465            .arg(&venv_path)
466            .arg("--python")
467            .arg(&compiled_version)
468            .output()?;
469
470        logger::capture_output(&format!("uv venv --python {}", compiled_version), &output);
471
472        if !output.status.success() {
473            let stderr = String::from_utf8_lossy(&output.stderr);
474            return Err(BridgeError::Initialization(format!(
475                "Failed to create Python virtual environment with Python {}.\n\
476                Error: {}",
477                compiled_version, stderr
478            )));
479        }
480
481        python_path = resolve_python_path(&venv_path).unwrap_or_else(|_| PathBuf::new());
482
483        if python_path.as_os_str().is_empty() || !python_path.exists() {
484            if let Ok(entries) = std::fs::read_dir(venv_path.join(PYTHON_BIN_DIR_NAME)) {
485                let names: Vec<String> = entries
486                    .filter_map(|e| e.ok())
487                    .filter_map(|e| e.file_name().into_string().ok())
488                    .collect();
489                logger::debug(&format!("Venv bin contents after creation: {:?}", names));
490            }
491            return Err(BridgeError::Initialization(
492                "Failed to locate Python executable after creating venv".to_string(),
493            ));
494        }
495    }
496
497    if python_path.as_os_str().is_empty() || !python_path.exists() {
498        logger::warn(
499            "Python binary not found in configured venv; attempting uv-managed Python fallback",
500        );
501        if let Some((fallback, home)) = find_uv_python(&config)? {
502            return Ok(PythonEnvironment {
503                interpreter: fallback,
504                python_home: Some(home),
505            });
506        }
507
508        return Err(BridgeError::Initialization(format!(
509            "Failed to locate a usable Python interpreter.\n\
510            This binary requires Python {}.",
511            compiled_version
512        )));
513    }
514
515    let python_home = resolve_python_home(&venv_path);
516
517    Ok(PythonEnvironment {
518        interpreter: python_path,
519        python_home,
520    })
521}
522
523fn configure_embedded_python_env(python_home: &Path, site_packages: &Path) {
524    let home = python_home.to_string_lossy().to_string();
525    env::set_var("PYTHONHOME", &home);
526    logger::debug(&format!("Set PYTHONHOME={}", home));
527
528    let mut paths = vec![site_packages.to_path_buf()];
529    if let Some(existing) = env::var_os("PYTHONPATH") {
530        if !existing.is_empty() {
531            paths.extend(env::split_paths(&existing));
532        }
533    }
534    if let Ok(joined) = env::join_paths(paths) {
535        env::set_var("PYTHONPATH", &joined);
536        logger::debug(&format!(
537            "Updated PYTHONPATH to include {}",
538            site_packages.display()
539        ));
540    }
541}
542
543fn resolve_python_home(venv_path: &Path) -> Option<PathBuf> {
544    let cfg_path = venv_path.join("pyvenv.cfg");
545    let contents = fs::read_to_string(&cfg_path).ok()?;
546    for line in contents.lines() {
547        let trimmed = line.trim();
548        if trimmed.starts_with("home") {
549            let parts: Vec<_> = trimmed.splitn(2, '=').collect();
550            if parts.len() == 2 {
551                let mut path = PathBuf::from(parts[1].trim());
552                if path.ends_with("bin") || path.ends_with("Scripts") {
553                    path = path.parent().map(PathBuf::from).unwrap_or(path);
554                }
555                logger::debug(&format!(
556                    "Resolved base Python home {} from {}",
557                    path.display(),
558                    cfg_path.display()
559                ));
560                return Some(path);
561            }
562        }
563    }
564    logger::debug(&format!(
565        "Failed to resolve base Python home from {}",
566        cfg_path.display()
567    ));
568    None
569}
570
571/// Find and use uv-managed Python instead of system Python
572/// This avoids conflicts with system Python installations and the Windows Store popup
573fn find_uv_python(config: &Config) -> Result<Option<(PathBuf, PathBuf)>, BridgeError> {
574    let uv_path = config
575        .uv_path
576        .as_ref()
577        .ok_or_else(|| BridgeError::Initialization("UV path not configured".to_string()))?;
578
579    // Use the compiled Python version - this is what the binary requires
580    let compiled_version = get_compiled_python_version()?;
581
582    logger::debug(&format!(
583        "Attempting to use uv-managed Python {} (compiled version)",
584        compiled_version
585    ));
586
587    // Use `uv run python` to get the Python path
588    // This ensures we use uv's managed Python installations
589    let output = Command::new(uv_path)
590        .arg("run")
591        .arg("--python")
592        .arg(&compiled_version)
593        .arg("python")
594        .arg("-c")
595        .arg("import sys; print(sys.executable); print(sys.base_prefix)")
596        .output()
597        .map_err(|e| BridgeError::Initialization(format!("Failed to run uv python: {}", e)))?;
598
599    if !output.status.success() {
600        let stderr = String::from_utf8_lossy(&output.stderr);
601        logger::debug(&format!(
602            "Failed to probe uv-managed Python {} (status {:?}): {}",
603            compiled_version,
604            output.status.code(),
605            stderr
606        ));
607        return Ok(None);
608    }
609
610    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
611    let mut lines: Vec<String> = stdout.lines().map(|s| s.trim().to_string()).collect();
612
613    if lines.len() < 2 {
614        logger::debug("Unexpected output from uv python probe");
615        return Ok(None);
616    }
617
618    let prefix = PathBuf::from(lines.pop().unwrap_or_default());
619    let executable = PathBuf::from(lines.pop().unwrap_or_default());
620
621    if !executable.exists() {
622        logger::debug(&format!(
623            "UV-managed Python executable does not exist: {}",
624            executable.display()
625        ));
626        return Ok(None);
627    }
628
629    logger::info(&format!(
630        "Using uv-managed Python {} at {}",
631        compiled_version,
632        executable.display()
633    ));
634
635    Ok(Some((executable, prefix)))
636}