@@ -14,90 +14,62 @@ import path from 'path';
1414import { promisify } from 'util' ;
1515
1616const execFile = promisify ( require ( 'child_process' ) . execFile ) ;
17+ const FALLBACK_PYTHON_VERSION = '3.13' ;
1718
1819/**
1920 * Simple logger for minimal output with timestamp
20- * @param {string } level - Log level ('info', 'warn', 'error')
21- * @param {string } message - Log message to output
2221 */
2322function log ( level , message ) {
2423 const timestamp = new Date ( ) . toISOString ( ) ;
25- // eslint-disable-next-line no-console
2624 console [ level ] ( `[${ timestamp } ] [Python-Installer] ${ message } ` ) ;
2725}
2826
2927/**
30- * Check if Python version meets compatibility requirements
31- * Supports different validation modes for finding existing vs installing new Python
32- * @param {string } pythonVersion - Python version string (e.g., "3.13.1")
33- * @param {boolean } forInstallation - If true, only allows 3.13.x; if false, allows 3.10-3.13
34- * @returns {boolean } True if version is compatible with requirements
28+ * Check if Python version is compatible (3.10-3.13)
3529 */
36- function isPythonVersionCompatible ( pythonVersion , forInstallation = false ) {
30+ function isPythonCompatible ( pythonVersion ) {
3731 const versionParts = pythonVersion . split ( '.' ) ;
3832 const major = parseInt ( versionParts [ 0 ] , 10 ) ;
3933 const minor = parseInt ( versionParts [ 1 ] , 10 ) ;
4034
41- if ( major !== 3 ) {
42- return false ;
43- }
44-
45- if ( forInstallation ) {
46- return minor === 13 ; // Only 3.13.x for new installations
47- } else {
48- return minor >= 10 && minor <= 13 ; // 3.10-3.13 for finding existing installations
49- }
35+ return major === 3 && minor >= 10 && minor <= 13 ;
5036}
5137
5238/**
53- * Search for existing Python executable in system PATH with version validation
54- * Scans through PATH directories to find compatible Python installations.
55- * Accepts Python versions 3.10 through 3.13. Returns first valid installation found.
56- * @returns {Promise<string|null> } Path to first valid Python executable or null if not found
57- * @throws {Error } If distutils module is missing in found Python installation
39+ * Search for existing compatible Python installation (3.10-3.13) in system PATH
5840 */
5941export async function findPythonExecutable ( ) {
6042 const exenames = proc . IS_WINDOWS ? [ 'python.exe' ] : [ 'python3' , 'python' ] ;
6143 const envPath = process . env . PLATFORMIO_PATH || process . env . PATH ;
62- const errors = [ ] ;
6344
6445 log ( 'info' , 'Searching for compatible Python installation (3.10-3.13)' ) ;
6546
66- // Search through all PATH locations for Python executables with early exit on first match
6747 for ( const location of envPath . split ( path . delimiter ) ) {
6848 for ( const exename of exenames ) {
6949 const executable = path . normalize ( path . join ( location , exename ) ) . replace ( / " / g, '' ) ;
50+
7051 try {
71- if (
72- fs . existsSync ( executable ) &&
73- ( await isValidPythonVersion ( executable ) ) &&
74- ( await callInstallerScript ( executable , [ 'check' , 'python' ] ) )
75- ) {
52+ if ( fs . existsSync ( executable ) &&
53+ await isValidPythonVersion ( executable ) &&
54+ await callInstallerScript ( executable , [ 'check' , 'python' ] ) ) {
7655 log ( 'info' , `Found compatible Python: ${ executable } ` ) ;
7756 return executable ;
7857 }
7958 } catch ( err ) {
80- errors . push ( err ) ;
59+ // Continue searching, but log significant errors
60+ if ( err . message && ! err . message . includes ( 'timeout' ) ) {
61+ log ( 'warn' , `Python check failed for ${ executable } : ${ err . message } ` ) ;
62+ }
8163 }
8264 }
8365 }
8466
85- // Handle specific error conditions that should be propagated
86- for ( const err of errors ) {
87- if ( err . toString ( ) . includes ( 'Could not find distutils module' ) ) {
88- throw err ;
89- }
90- }
91-
92- log ( 'info' , 'No compatible system Python found, will install Python 3.13' ) ;
67+ log ( 'info' , 'No compatible Python found, will install Python 3.13 via UV' ) ;
9368 return null ;
9469}
9570
9671/**
97- * Validate Python executable version and basic functionality
98- * This function is used for FINDING existing installations, not for installation validation
99- * @param {string } executable - Full path to Python executable
100- * @returns {Promise<boolean> } True if Python version is acceptable (3.10-3.13)
72+ * Validate Python executable version (3.10-3.13)
10173 */
10274async function isValidPythonVersion ( executable ) {
10375 try {
@@ -113,218 +85,151 @@ async function isValidPythonVersion(executable) {
11385 return false ;
11486 }
11587
116- return isPythonVersionCompatible ( versionMatch [ 1 ] , false ) ; // Allow 3.10-3.13 for finding existing
88+ const version = versionMatch [ 1 ] ;
89+ const isCompatible = isPythonCompatible ( version ) ;
90+
91+ if ( isCompatible ) {
92+ log ( 'info' , `Found Python ${ version } ` ) ;
93+ }
94+
95+ return isCompatible ;
11796 } catch {
11897 return false ;
11998 }
12099}
121100
122101/**
123- * Check if UV (astral-sh/uv) package manager is available on the system
124- * UV is a fast Python package installer and resolver written in Rust
125- * @returns {Promise<boolean> } True if UV is installed and accessible via PATH
102+ * Check if UV package manager is available
126103 */
127104async function isUVAvailable ( ) {
128105 try {
129106 await execFile ( 'uv' , [ '--version' ] , { timeout : 5000 } ) ;
130- log ( 'info' , 'UV is available on system ' ) ;
107+ log ( 'info' , 'UV is available' ) ;
131108 return true ;
132109 } catch {
133- log ( 'info' , 'UV not found on system ' ) ;
110+ log ( 'info' , 'UV not found, will install ' ) ;
134111 return false ;
135112 }
136113}
137114
138115/**
139- * Install UV package manager using official installation scripts
140- * Downloads and runs platform-specific installer from astral.sh
141- * @returns {Promise<void> }
142- * @throws {Error } If UV installation fails
116+ * Install UV package manager
143117 */
144118async function installUV ( ) {
145119 log ( 'info' , 'Installing UV package manager' ) ;
146120
147121 try {
148122 if ( proc . IS_WINDOWS ) {
149- // Windows: Use PowerShell with official installer script
150- await execFile (
151- 'powershell' ,
152- [
153- '-NoProfile' ,
154- '-ExecutionPolicy' ,
155- 'Bypass' ,
156- '-Command' ,
157- 'irm https://astral.sh/uv/install.ps1 | iex' ,
158- ] ,
159- { timeout : 120000 } ,
160- ) ;
123+ await execFile ( 'powershell' , [
124+ '-NoProfile' ,
125+ '-ExecutionPolicy' , 'Bypass' ,
126+ '-Command' ,
127+ 'irm https://astral.sh/uv/install.ps1 | iex'
128+ ] , { timeout : 120000 } ) ;
161129 } else {
162- // Unix/Linux/macOS: Use shell with curl installer
163130 await execFile ( 'sh' , [ '-c' , 'curl -LsSf https://astral.sh/uv/install.sh | sh' ] , {
164- timeout : 120000 ,
131+ timeout : 120000
165132 } ) ;
166133 }
167-
168134 log ( 'info' , 'UV installation completed' ) ;
169135 } catch ( err ) {
170136 throw new Error ( `Failed to install UV: ${ err . message } ` ) ;
171137 }
172138}
173139
174140/**
175- * Install Python using UV package manager
176- * Uses UV to download and install Python from astral-sh/python-build-standalone
177- * Automatically handles platform detection, download, verification, and extraction
178- * @param {string } destinationDir - Target installation directory
179- * @param {string } pythonVersion - Python version to install (default: "3.13")
180- * @returns {Promise<string> } Path to installed Python directory
181- * @throws {Error } If UV installation or Python installation fails
141+ * Install Python 3.13 using UV
182142 */
183- async function installPythonWithUV ( destinationDir , pythonVersion = '3.13' ) {
184- log ( 'info' , `Installing Python ${ pythonVersion } using UV` ) ;
143+ async function installPython313WithUV ( destinationDir ) {
144+ log ( 'info' , `Installing Python ${ FALLBACK_PYTHON_VERSION } using UV` ) ;
185145
186- // Ensure UV is available, install if necessary
146+ // Ensure UV is available
187147 if ( ! ( await isUVAvailable ( ) ) ) {
188148 await installUV ( ) ;
189149 }
190150
191- // Clean up any existing installation to avoid conflicts
151+ // Clean and create destination directory
192152 try {
193153 await fs . promises . rm ( destinationDir , { recursive : true , force : true } ) ;
194- } catch ( err ) {
195- // Ignore cleanup errors ( directory might not exist)
154+ } catch {
155+ // Ignore if directory doesn't exist
196156 }
197-
198- // Create destination directory structure
199157 await fs . promises . mkdir ( destinationDir , { recursive : true } ) ;
200158
201- try {
202- // Configure environment for UV Python installation
203- const env = {
204- ...process . env ,
205- UV_PYTHON_INSTALL_DIR : destinationDir ,
206- UV_CACHE_DIR : path . join ( core . getTmpDir ( ) , 'uv-cache' ) ,
207- } ;
159+ // Install Python via UV
160+ const env = {
161+ ...process . env ,
162+ UV_PYTHON_INSTALL_DIR : destinationDir ,
163+ UV_CACHE_DIR : path . join ( core . getTmpDir ( ) , 'uv-cache' ) ,
164+ } ;
208165
209- // Execute UV Python installation command
210- await execFile ( 'uv' , [ 'python' , 'install' , pythonVersion ] , {
166+ try {
167+ await execFile ( 'uv' , [ 'python' , 'install' , FALLBACK_PYTHON_VERSION ] , {
211168 env,
212- timeout : 300000 , // 5 minutes timeout for download and installation
169+ timeout : 300000 , // 5 minutes
213170 cwd : destinationDir ,
214171 } ) ;
215172
216- // Verify that Python executable was successfully installed
217- await ensurePythonExeExists ( destinationDir , pythonVersion ) ;
218-
219- log ( 'info' , `Python ${ pythonVersion } installation completed: ${ destinationDir } ` ) ;
173+ log ( 'info' , `Python ${ FALLBACK_PYTHON_VERSION } installation completed: ${ destinationDir } ` ) ;
220174 return destinationDir ;
221175 } catch ( err ) {
222176 throw new Error ( `UV Python installation failed: ${ err . message } ` ) ;
223177 }
224178}
225179
226180/**
227- * Verify that Python executable exists in the installed directory
228- * Searches through common installation paths where UV might place Python executables
229- * @param {string } pythonDir - Directory containing Python installation
230- * @param {string } pythonVersion - Python version for path construction (default: "3.13")
231- * @returns {Promise<boolean> } True if executable exists and is accessible
232- * @throws {Error } If no Python executable found in expected locations
181+ * Find Python executable in installed directory
233182 */
234- async function ensurePythonExeExists ( pythonDir , pythonVersion = '3.13' ) {
235- // UV typically installs to subdirectories organized by version
236- const possiblePaths = [
237- pythonDir , // Direct installation in target directory
238- path . join ( pythonDir , 'python' ) ,
239- path . join ( pythonDir , `python-${ pythonVersion } ` ) ,
240- path . join ( pythonDir , pythonVersion ) ,
183+ function getPythonExecutablePath ( pythonDir ) {
184+ const exeName = proc . IS_WINDOWS ? 'python.exe' : 'python3' ;
185+
186+ // Common UV installation paths
187+ const searchPaths = [
188+ path . join ( pythonDir , exeName ) ,
189+ path . join ( pythonDir , 'bin' , exeName ) ,
190+ path . join ( pythonDir , FALLBACK_PYTHON_VERSION , exeName ) ,
191+ path . join ( pythonDir , FALLBACK_PYTHON_VERSION , 'bin' , exeName ) ,
241192 ] ;
242193
243- const executables = proc . IS_WINDOWS ? [ 'python.exe' ] : [ 'python3' , 'python' ] ;
244-
245- for ( const basePath of possiblePaths ) {
246- // Check for executable in root of installation path
247- for ( const exeName of executables ) {
248- try {
249- await fs . promises . access ( path . join ( basePath , exeName ) ) ;
250- return true ;
251- } catch ( err ) {
252- // Continue trying other combinations
253- }
254- }
255-
256- // Check for executable in bin subdirectory (Unix-style layout)
257- const binDir = path . join ( basePath , 'bin' ) ;
258- for ( const exeName of executables ) {
259- try {
260- await fs . promises . access ( path . join ( binDir , exeName ) ) ;
261- return true ;
262- } catch ( err ) {
263- // Continue trying other combinations
264- }
194+ for ( const execPath of searchPaths ) {
195+ try {
196+ fs . accessSync ( execPath , fs . constants . X_OK ) ;
197+ log ( 'info' , `Found Python executable: ${ execPath } ` ) ;
198+ return execPath ;
199+ } catch {
200+ // Continue searching
265201 }
266202 }
267203
268- throw new Error ( 'Python executable does not exist after UV installation!' ) ;
204+ throw new Error ( `Could not find Python executable in ${ pythonDir } ` ) ;
269205}
270206
271207/**
272- * Main entry point for installing Python distribution using UV
273- * This replaces the legacy complex installation logic with a simple UV-based approach
274- * @param {string } destinationDir - Target installation directory
275- * @param {object } options - Optional configuration (kept for API compatibility)
276- * @returns {Promise<string> } Path to installed Python directory
277- * @throws {Error } If Python installation fails for any reason
208+ * Main entry point: find existing compatible Python (3.10-3.13) or install Python 3.13 via UV
278209 */
279210export async function installPortablePython ( destinationDir ) {
280- log ( 'info' , 'Starting Python 3.13 installation ' ) ;
211+ log ( 'info' , 'Starting Python setup (detecting 3.10-3.13, installing 3.13 if needed) ' ) ;
281212
282- // UV-based installation is now the only supported method
283213 try {
284- return await installPythonWithUV ( destinationDir , '3.13' ) ;
285- } catch ( uvError ) {
286- log ( 'error' , `UV installation failed: ${ uvError . message } ` ) ;
214+ // Try to find existing compatible Python first
215+ const existingPython = await findPythonExecutable ( ) ;
216+ if ( existingPython ) {
217+ log ( 'info' , `Using existing compatible Python: ${ existingPython } ` ) ;
218+ return path . dirname ( existingPython ) ;
219+ }
220+
221+ // No compatible Python found, install Python 3.13 via UV
222+ log ( 'info' , 'No compatible Python installation found, installing Python 3.13' ) ;
223+ return await installPython313WithUV ( destinationDir ) ;
224+
225+ } catch ( error ) {
226+ log ( 'error' , `Python setup failed: ${ error . message } ` ) ;
287227 throw new Error (
288- `Python installation failed: ${ uvError . message } . Please ensure UV can be installed and internet connection is available.` ,
228+ `Python installation failed: ${ error . message } . ` +
229+ `Please ensure internet connection is available and system supports UV installation.`
289230 ) ;
290231 }
291232}
292233
293- /**
294- * Locate Python executable in an installed Python directory
295- * Searches through common locations where UV might install Python executables
296- * @param {string } pythonDir - Python installation directory to search
297- * @returns {Promise<string> } Full path to Python executable
298- * @throws {Error } If no executable found in the directory
299- */
300- function getPythonExecutablePath ( pythonDir ) {
301- const executables = proc . IS_WINDOWS ? [ 'python.exe' ] : [ 'python3' , 'python' ] ;
302-
303- // Check common locations where UV might install Python
304- const searchPaths = [
305- pythonDir ,
306- path . join ( pythonDir , 'bin' ) ,
307- path . join ( pythonDir , 'python' ) ,
308- path . join ( pythonDir , 'python-3.13' ) ,
309- path . join ( pythonDir , '3.13' ) ,
310- path . join ( pythonDir , '3.13' , 'bin' ) ,
311- ] ;
312-
313- for ( const searchPath of searchPaths ) {
314- for ( const exeName of executables ) {
315- const fullPath = path . join ( searchPath , exeName ) ;
316- try {
317- fs . accessSync ( fullPath , fs . constants . X_OK ) ;
318- log ( 'info' , `Found Python executable: ${ fullPath } ` ) ;
319- return fullPath ;
320- } catch ( err ) {
321- // Continue searching through all combinations
322- }
323- }
324- }
325-
326- throw new Error ( `Could not find Python executable in ${ pythonDir } ` ) ;
327- }
328-
329- // Export utility functions for external use
330- export { isPythonVersionCompatible , isUVAvailable , installUV , getPythonExecutablePath } ;
234+ // Export utility functions
235+ export { isPythonCompatible , isUVAvailable , installUV , getPythonExecutablePath } ;
0 commit comments