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_npm() -> Result<PathBuf> {
30 let candidate = if cfg!(windows) { "npm.cmd" } else { "npm" };
32 let output = Command::new(candidate)
33 .arg("--version")
34 .output()
35 .map_err(|_| {
36 anyhow!(
37 "npm not found on PATH. Install Node.js LTS (includes npm) from \
38 https://nodejs.org/ or via your package manager (`brew install node`, \
39 `apt install nodejs npm`, `winget install OpenJS.NodeJS`)."
40 )
41 })?;
42
43 if !output.status.success() {
44 return Err(anyhow!(
45 "npm found on PATH but `npm --version` exited with status {:?}. \
46 Verify your Node.js install: {}",
47 output.status.code(),
48 "https://nodejs.org/"
49 ));
50 }
51
52 Ok(PathBuf::from(candidate))
53}
54
55pub fn detect_python(explicit: Option<&str>) -> Result<PathBuf> {
59 if let Some(path) = explicit {
60 let candidate = PathBuf::from(path);
61 return probe(&candidate).ok_or_else(|| missing_python_error(Some(path)));
62 }
63
64 let candidates: &[&str] = if cfg!(windows) {
65 &["py", "python3", "python"]
66 } else {
67 &["python3", "python"]
68 };
69
70 for name in candidates {
71 if let Some(resolved) = probe(Path::new(name)) {
72 return Ok(resolved);
73 }
74 }
75
76 Err(missing_python_error(None))
77}
78
79fn probe(candidate: &Path) -> Option<PathBuf> {
82 let output = Command::new(candidate)
83 .args([
84 "-c",
85 "import sys; print(sys.executable); print(sys.version_info[0]); print(sys.version_info[1])",
86 ])
87 .output()
88 .ok()?;
89
90 if !output.status.success() {
91 return None;
92 }
93
94 let stdout = String::from_utf8_lossy(&output.stdout);
95 let mut lines = stdout.lines();
96 let exe = lines.next()?.trim();
97 let major: u32 = lines.next()?.trim().parse().ok()?;
98 let minor: u32 = lines.next()?.trim().parse().ok()?;
99
100 if exe.is_empty() {
101 return None;
102 }
103 if major < MIN_MAJOR || (major == MIN_MAJOR && minor < MIN_MINOR) {
104 return None;
105 }
106
107 Some(PathBuf::from(exe))
108}
109
110fn missing_python_error(tried: Option<&str>) -> anyhow::Error {
111 let prefix = match tried {
112 Some(p) => format!("Python interpreter `{p}` is unusable"),
113 None => format!("No usable Python ≥{MIN_MAJOR}.{MIN_MINOR} found on PATH"),
114 };
115 anyhow!(
116 "{prefix}.\n\n\
117 Construct requires Python {MIN_MAJOR}.{MIN_MINOR}+ to run its MCP sidecars.\n\
118 Install it from {DOWNLOAD_URL} and re-run `construct install --sidecars-only`.\n\n\
119 On Windows, avoid the Microsoft Store Python stub — install from python.org directly."
120 )
121}