construct/sidecars/
python.rs1use anyhow::{Result, anyhow};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11const MIN_MAJOR: u32 = 3;
12const MIN_MINOR: u32 = 11;
13
14const DOWNLOAD_URL: &str = "https://www.python.org/downloads/";
15
16pub fn default_python_command() -> &'static str {
22 if cfg!(windows) { "python" } else { "python3" }
23}
24
25pub fn detect_python(explicit: Option<&str>) -> Result<PathBuf> {
29 if let Some(path) = explicit {
30 let candidate = PathBuf::from(path);
31 return probe(&candidate).ok_or_else(|| missing_python_error(Some(path)));
32 }
33
34 let candidates: &[&str] = if cfg!(windows) {
35 &["py", "python3", "python"]
36 } else {
37 &["python3", "python"]
38 };
39
40 for name in candidates {
41 if let Some(resolved) = probe(Path::new(name)) {
42 return Ok(resolved);
43 }
44 }
45
46 Err(missing_python_error(None))
47}
48
49fn probe(candidate: &Path) -> Option<PathBuf> {
52 let output = Command::new(candidate)
53 .args([
54 "-c",
55 "import sys; print(sys.executable); print(sys.version_info[0]); print(sys.version_info[1])",
56 ])
57 .output()
58 .ok()?;
59
60 if !output.status.success() {
61 return None;
62 }
63
64 let stdout = String::from_utf8_lossy(&output.stdout);
65 let mut lines = stdout.lines();
66 let exe = lines.next()?.trim();
67 let major: u32 = lines.next()?.trim().parse().ok()?;
68 let minor: u32 = lines.next()?.trim().parse().ok()?;
69
70 if exe.is_empty() {
71 return None;
72 }
73 if major < MIN_MAJOR || (major == MIN_MAJOR && minor < MIN_MINOR) {
74 return None;
75 }
76
77 Some(PathBuf::from(exe))
78}
79
80fn missing_python_error(tried: Option<&str>) -> anyhow::Error {
81 let prefix = match tried {
82 Some(p) => format!("Python interpreter `{p}` is unusable"),
83 None => format!("No usable Python ≥{MIN_MAJOR}.{MIN_MINOR} found on PATH"),
84 };
85 anyhow!(
86 "{prefix}.\n\n\
87 Construct requires Python {MIN_MAJOR}.{MIN_MINOR}+ to run its MCP sidecars.\n\
88 Install it from {DOWNLOAD_URL} and re-run `construct install --sidecars-only`.\n\n\
89 On Windows, avoid the Microsoft Store Python stub — install from python.org directly."
90 )
91}