Skip to main content

parallax_runtime/
discover.rs

1//! Host binary discovery for Python / Node.
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6/// A discovered interpreter/runtime binary.
7#[derive(Clone, Debug)]
8pub struct DiscoveredBinary {
9    /// Absolute or PATH-resolved command.
10    pub path: PathBuf,
11    /// Version string from probing.
12    pub version: Option<String>,
13}
14
15/// Discover a usable Python interpreter.
16///
17/// Tries `python`, `python3`, `py`, then common Windows install locations.
18/// Rejects Windows Store stub aliases that fail to run.
19pub fn discover_python() -> Option<DiscoveredBinary> {
20    let mut candidates: Vec<PathBuf> = vec![
21        PathBuf::from("python"),
22        PathBuf::from("python3"),
23        PathBuf::from("py"),
24    ];
25
26    if let Some(local) = std::env::var_os("LOCALAPPDATA") {
27        let base = PathBuf::from(local).join("Programs").join("Python");
28        if let Ok(entries) = std::fs::read_dir(&base) {
29            for ent in entries.flatten() {
30                let p = ent.path().join("python.exe");
31                if p.is_file() {
32                    candidates.push(p);
33                }
34            }
35        }
36    }
37    // Also check Program Files style paths.
38    for pf in [
39        std::env::var_os("ProgramFiles"),
40        std::env::var_os("ProgramFiles(x86)"),
41    ]
42    .into_iter()
43    .flatten()
44    {
45        let base = PathBuf::from(pf).join("Python");
46        if let Ok(entries) = std::fs::read_dir(&base) {
47            for ent in entries.flatten() {
48                let p = ent.path().join("python.exe");
49                if p.is_file() {
50                    candidates.push(p);
51                }
52            }
53        }
54    }
55
56    for cand in candidates {
57        if let Some(bin) = probe_python(&cand) {
58            return Some(bin);
59        }
60    }
61    None
62}
63
64fn probe_python(cmd: &Path) -> Option<DiscoveredBinary> {
65    let output = Command::new(cmd)
66        .args(["-c", "import sys; print(sys.version.split()[0])"])
67        .output()
68        .ok()?;
69    if !output.status.success() {
70        return None;
71    }
72    let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
73    if version.is_empty() {
74        return None;
75    }
76    // Reject Microsoft Store stub noise.
77    let stderr = String::from_utf8_lossy(&output.stderr);
78    if stderr.contains("Microsoft Store") {
79        return None;
80    }
81    Some(DiscoveredBinary {
82        path: cmd.to_path_buf(),
83        version: Some(version),
84    })
85}
86
87/// Discover Node.js.
88pub fn discover_javascript() -> Option<DiscoveredBinary> {
89    let candidates = [PathBuf::from("node"), PathBuf::from("nodejs")];
90    for cand in candidates {
91        if let Some(bin) = probe_node(&cand) {
92            return Some(bin);
93        }
94    }
95    // Common Windows install path.
96    let pf = std::env::var_os("ProgramFiles").map(PathBuf::from);
97    if let Some(pf) = pf {
98        let p = pf.join("nodejs").join("node.exe");
99        if let Some(bin) = probe_node(&p) {
100            return Some(bin);
101        }
102    }
103    None
104}
105
106fn probe_node(cmd: &Path) -> Option<DiscoveredBinary> {
107    let output = Command::new(cmd).arg("-v").output().ok()?;
108    if !output.status.success() {
109        return None;
110    }
111    let version = String::from_utf8_lossy(&output.stdout)
112        .trim()
113        .trim_start_matches('v')
114        .to_string();
115    if version.is_empty() {
116        return None;
117    }
118    Some(DiscoveredBinary {
119        path: cmd.to_path_buf(),
120        version: Some(version),
121    })
122}