@@ -34,6 +34,7 @@ pub struct PythonEnvStatus {
3434 pub is_conda : bool ,
3535 pub is_mamba : bool ,
3636 pub conda_env_name : Option < String > ,
37+ pub detection_error : Option < String > ,
3738}
3839
3940/// Global install lock to prevent concurrent installations
@@ -46,23 +47,85 @@ pub fn get_install_lock() -> Arc<Mutex<bool>> {
4647}
4748
4849/// List of python executables to try (in order of preference)
50+ /// Tries the most specific paths first, then falls back to bare commands which
51+ /// use PATH. On Windows also tries the `py` launcher.
4952const PYTHON_CANDIDATES : & [ & str ] = & [
53+ // RustTools YOLO venv (most preferred)
54+ "/home/enols/.rusttools/yolo-env/bin/python" ,
55+ // Bare commands first — rely on PATH
5056 "python3" ,
5157 "python" ,
58+ "py" ,
59+ // Unix-specific paths
5260 "/usr/bin/python3" ,
61+ "/usr/bin/python" ,
5362 "/usr/local/bin/python3" ,
63+ "/usr/local/bin/python" ,
64+ // Windows-specific paths (ProgramFiles / AppData)
65+ "C:\\ Python312\\ python.exe" ,
66+ "C:\\ Python311\\ python.exe" ,
67+ "C:\\ Python310\\ python.exe" ,
68+ "C:\\ Program Files\\ Python312\\ python.exe" ,
69+ "C:\\ Program Files\\ Python311\\ python.exe" ,
70+ "C:\\ Users\\ ${USERNAME}\\ AppData\\ Local\\ Programs\\ Python\\ Python312\\ python.exe" ,
71+ "C:\\ Users\\ ${USERNAME}\\ AppData\\ Local\\ Programs\\ Python\\ Python311\\ python.exe" ,
72+ "C:\\ Users\\ ${USERNAME}\\ AppData\\ Local\\ Programs\\ Python\\ Python310\\ python.exe" ,
73+ // Windows py launcher (latest Python in PATH)
74+ "C:\\ Windows\\ py.exe" ,
5475] ;
5576
56- /// Resolve the actual python path by trying multiple executables directly
77+ /// Resolve the actual python path by trying multiple executables directly.
78+ /// Returns the first python that responds to `--version`, or None.
5779pub fn resolve_python_path ( ) -> Option < String > {
58- for & python in PYTHON_CANDIDATES {
80+ // First, check HOME-based dynamic paths before iterating static candidates
81+ if let Ok ( home) = std:: env:: var ( "HOME" ) {
82+ let home_yolo_python = format ! ( "{}/.rusttools/yolo-env/bin/python" , home) ;
83+ if Command :: new ( & home_yolo_python) . arg ( "--version" ) . output ( ) . ok ( ) ?. status . success ( ) {
84+ return Some ( home_yolo_python) ;
85+ }
86+ let home_hermes_python = format ! ( "{}/.hermes/hermes-agent/venv/bin/python" , home) ;
87+ if Command :: new ( & home_hermes_python) . arg ( "--version" ) . output ( ) . ok ( ) ?. status . success ( ) {
88+ return Some ( home_hermes_python) ;
89+ }
90+ }
91+
92+ // Expand ${USERNAME} env var on Windows
93+ let mut expanded: Vec < & ' static str > = Vec :: with_capacity ( PYTHON_CANDIDATES . len ( ) ) ;
94+ for & candidate in PYTHON_CANDIDATES {
95+ if candidate. contains ( "${USERNAME}" ) {
96+ if let Ok ( username) = std:: env:: var ( "USERNAME" ) {
97+ let expanded_path = candidate. replace ( "${USERNAME}" , & username) ;
98+ expanded. push ( Box :: leak ( expanded_path. into_boxed_str ( ) ) ) ;
99+ }
100+ } else {
101+ expanded. push ( candidate) ;
102+ }
103+ }
104+
105+ for python in & expanded {
59106 if Command :: new ( python) . arg ( "--version" ) . output ( ) . ok ( ) ?. status . success ( ) {
60- return Some ( python. to_string ( ) ) ;
107+ return Some ( ( * python) . to_string ( ) ) ;
61108 }
62109 }
63110 None
64111}
65112
113+ /// Cache for the resolved python path to avoid repeated subprocess spawns
114+ thread_local ! {
115+ static RESOLVED_PYTHON : std:: cell:: OnceCell <String > = std:: cell:: OnceCell :: new( ) ;
116+ }
117+
118+ /// Get the cached resolved python path, or resolve and cache it
119+ pub fn resolved_python ( ) -> Option < String > {
120+ RESOLVED_PYTHON . with ( |cell| cell. get ( ) . cloned ( ) ) . or_else ( || {
121+ let path = resolve_python_path ( ) ;
122+ if let Some ( ref p) = path {
123+ RESOLVED_PYTHON . with ( |cell| { let _ = cell. set ( p. clone ( ) ) ; } ) ;
124+ }
125+ path
126+ } )
127+ }
128+
66129/// Check if conda/mamba environment is active and return env info
67130pub fn check_conda ( ) -> ( bool , bool , Option < String > ) {
68131 // Check MAMBA_DEFAULT_ENV first (mamba takes precedence)
@@ -84,49 +147,43 @@ pub fn check_conda() -> (bool, bool, Option<String>) {
84147
85148/// Check if Python is available and get its version
86149pub fn check_python ( ) -> Option < String > {
87- for & python in PYTHON_CANDIDATES {
88- let output = Command :: new ( python)
89- . arg ( "--version" )
90- . output ( )
91- . ok ( )
92- . filter ( |o| o. status . success ( ) ) ?;
93-
94- let version_str = String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ;
95- let version = version_str. trim ( ) . replace ( "Python " , "" ) ;
96- return Some ( version) ;
97- }
98- None
150+ let python = resolved_python ( ) ?;
151+ let output = Command :: new ( & python)
152+ . arg ( "--version" )
153+ . output ( )
154+ . ok ( )
155+ . filter ( |o| o. status . success ( ) ) ?;
156+ let version_str = String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) ;
157+ let version = version_str. trim ( ) . replace ( "Python " , "" ) ;
158+ Some ( version)
99159}
100160
101161/// Check if PyTorch is available and get its version
102162pub fn check_torch ( ) -> Option < String > {
103- for & python in PYTHON_CANDIDATES {
104- let output = Command :: new ( python)
105- . args ( [ "-c" , "import torch; print(torch.__version__)" ] )
106- . output ( )
107- . ok ( )
108- . filter ( |o| o. status . success ( ) ) ?;
109-
110- let version = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
111- return Some ( version) ;
112- }
113- None
163+ let python = resolved_python ( ) ?;
164+ let output = Command :: new ( & python)
165+ . args ( [ "-c" , "import torch; print(torch.__version__)" ] )
166+ . output ( )
167+ . ok ( )
168+ . filter ( |o| o. status . success ( ) ) ?;
169+ let version = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
170+ Some ( version)
114171}
115172
116173/// Check if CUDA is available via PyTorch
117174pub fn check_cuda ( ) -> bool {
118- for & python in PYTHON_CANDIDATES {
119- let output = Command :: new ( python )
120- . args ( [ "-c" , "import torch; print(torch.cuda.is_available())" ] )
121- . output ( )
122- . ok ( )
123- . filter ( |o| o . status . success ( ) ) ;
124-
125- if let Some ( o ) = output {
126- return String :: from_utf8_lossy ( & o . stdout ) . trim ( ) . contains ( "True" ) ;
127- }
128- }
129- false
175+ let python = match resolved_python ( ) {
176+ Some ( p ) => p ,
177+ None => return false ,
178+ } ;
179+ let output = Command :: new ( & python )
180+ . args ( [ "-c" , "import torch; print(torch.cuda.is_available ())" ] )
181+ . output ( )
182+ . ok ( )
183+ . filter ( |o| o . status . success ( ) ) ;
184+ output . map_or ( false , |o| {
185+ String :: from_utf8_lossy ( & o . stdout ) . trim ( ) . contains ( "True" )
186+ } )
130187}
131188
132189/// Get the full status of the Python environment
@@ -136,6 +193,12 @@ pub fn get_env_status() -> PythonEnvStatus {
136193 let installing = * get_install_lock ( ) . lock ( ) . unwrap ( ) ;
137194 let ( is_conda, is_mamba, conda_env_name) = check_conda ( ) ;
138195
196+ let detection_error = if python_version. is_none ( ) {
197+ Some ( "Python not found or not working. Please install Python 3.8+." . to_string ( ) )
198+ } else {
199+ None
200+ } ;
201+
139202 PythonEnvStatus {
140203 python_available : python_version. is_some ( ) ,
141204 python_version,
@@ -146,6 +209,7 @@ pub fn get_env_status() -> PythonEnvStatus {
146209 is_conda,
147210 is_mamba,
148211 conda_env_name,
212+ detection_error,
149213 }
150214}
151215
@@ -188,13 +252,14 @@ pub fn install_python_deps(
188252 } ) ;
189253 }
190254
191- // Determine pip install command based on environment
192- let python_path = resolve_python_path ( ) . unwrap_or_else ( || "python3" . to_string ( ) ) ;
255+ // Determine pip install command based on resolved python
256+ let python_path = resolved_python ( ) . unwrap_or_else ( || "python3" . to_string ( ) ) ;
193257 let pip_cmd = if check_conda ( ) . 0 || check_conda ( ) . 1 {
194258 // In conda/mamba env, use python -m pip for proper environment targeting
195259 vec ! [ python_path. as_str( ) , "-m" , "pip" ]
196260 } else {
197- vec ! [ "python3" , "-m" , "pip" ]
261+ // Use the resolved python executable for pip
262+ vec ! [ python_path. as_str( ) , "-m" , "pip" ]
198263 } ;
199264
200265 // Install torch with CUDA 12.4 support
0 commit comments