66 * the root directory of this source tree.
77 */
88
9- import * as proc from '../proc' ;
10- import { callInstallerScript } from './get-pioarduino' ;
9+ import * as proc from '../proc.js ' ;
10+ import { callInstallerScript } from './get-pioarduino.js ' ;
1111import fs from 'fs' ;
1212import path from 'path' ;
1313import { promisify } from 'util' ;
@@ -208,16 +208,14 @@ async function getUVCommand() {
208208}
209209
210210/**
211- * Install Python using UV package manager
212- * Creates a virtual environment using `uv venv` with Python 3.13
213- * This is simpler and more reliable than installing Python separately
214- * @param {string } destinationDir - Target installation directory (venv path)
215- * @param {string } pythonVersion - Python version to install (default: "3.13")
216- * @returns {Promise<string> } Path to installed Python venv directory
217- * @throws {Error } If UV installation or venv creation fails
211+ * Ensure Python is available via UV
212+ * UV will automatically download and manage Python if needed
213+ * @param {string } pythonVersion - Python version to ensure (default: "3.13")
214+ * @returns {Promise<string> } Path to UV-managed Python executable
215+ * @throws {Error } If UV installation or Python download fails
218216 */
219- async function installPythonWithUV ( destinationDir , pythonVersion = '3.13' ) {
220- log ( 'info' , `Creating Python ${ pythonVersion } venv using UV` ) ;
217+ async function ensurePythonWithUV ( pythonVersion = '3.13' ) {
218+ log ( 'info' , `Ensuring Python ${ pythonVersion } is available via UV` ) ;
221219
222220 // Ensure UV is available, install if necessary
223221 if ( ! ( await isUVAvailable ( ) ) ) {
@@ -228,118 +226,121 @@ async function installPythonWithUV(destinationDir, pythonVersion = '3.13') {
228226 const uvCommand = await getUVCommand ( ) ;
229227 log ( 'info' , `Using UV command: ${ uvCommand } ` ) ;
230228
231- // Clean up any existing installation to avoid conflicts
232229 try {
233- await fs . promises . rm ( destinationDir , { recursive : true , force : true } ) ;
234- } catch ( err ) {
235- // Ignore cleanup errors (directory might not exist)
236- }
237-
238- try {
239- // Create venv directly using uv venv command with absolute path
240- const absolutePath = path . resolve ( destinationDir ) ;
230+ // First check if Python is already installed
231+ try {
232+ const existingPath = await getUVPythonPath ( pythonVersion ) ;
233+ log ( 'info' , `Python ${ pythonVersion } already available at: ${ existingPath } ` ) ;
234+ return existingPath ;
235+ } catch {
236+ // Python not found, need to install
237+ log ( 'info' , `Python ${ pythonVersion } not found, installing...` ) ;
238+ }
241239
242- // Use --python-preference managed to allow UV to download Python if not found on system
243- await execFile (
240+ // Use 'uv python install' to ensure Python is available
241+ // UV will download and manage Python automatically
242+ const installResult = await execFile (
244243 uvCommand ,
245- [
246- 'venv' ,
247- absolutePath ,
248- '--python' ,
249- pythonVersion ,
250- '--python-preference' ,
251- 'managed' ,
252- ] ,
244+ [ 'python' , 'install' , pythonVersion ] ,
253245 {
254- timeout : 300000 , // 5 minutes timeout for download and installation
246+ timeout : 300000 , // 5 minutes timeout for download
255247 } ,
256248 ) ;
257249
258- // Verify that Python executable was successfully created
259- await ensurePythonExeExists ( destinationDir ) ;
250+ log ( 'info' , `UV Python install output: ${ installResult . stdout } ` ) ;
260251
261- log ( 'info' , `Python ${ pythonVersion } venv created successfully: ${ destinationDir } ` ) ;
262- return destinationDir ;
252+ // Get the path to the UV-managed Python
253+ const pythonPath = await getUVPythonPath ( pythonVersion ) ;
254+ log ( 'info' , `UV-managed Python ${ pythonVersion } installed at: ${ pythonPath } ` ) ;
255+ return pythonPath ;
263256 } catch ( err ) {
264- throw new Error ( `UV venv creation failed: ${ err . message } ` ) ;
257+ throw new Error ( `UV Python installation failed: ${ err . message } ` ) ;
265258 }
266259}
267260
268261/**
269- * Verify that Python executable exists in the venv directory
270- * Checks the standard venv bin/Scripts directory for Python executable
271- * @param {string } pythonDir - Directory containing Python venv
272- * @returns {Promise<boolean> } True if executable exists and is accessible
273- * @throws {Error } If no Python executable found in expected locations
262+ * Get the path to UV-managed Python executable
263+ * @param {string } pythonVersion - Python version (default: "3.13")
264+ * @returns {Promise<string> } Path to UV-managed Python executable
265+ * @throws {Error } If Python is not found
274266 */
275- async function ensurePythonExeExists ( pythonDir ) {
276- // Standard venv structure: bin/ on Unix, Scripts/ on Windows
277- const binDir = proc . IS_WINDOWS
278- ? path . join ( pythonDir , 'Scripts' )
279- : path . join ( pythonDir , 'bin' ) ;
280- const executables = proc . IS_WINDOWS ? [ 'python.exe' ] : [ 'python3' , 'python' ] ;
281-
282- for ( const exeName of executables ) {
267+ async function getUVPythonPath ( pythonVersion = '3.13' ) {
268+ const uvCommand = await getUVCommand ( ) ;
269+
270+ try {
271+ const result = await execFile ( uvCommand , [ 'python' , 'find' , pythonVersion ] , {
272+ timeout : 10000 ,
273+ } ) ;
274+
275+ let pythonPath = result . stdout . trim ( ) ;
276+ if ( ! pythonPath ) {
277+ throw new Error ( 'UV did not return a Python path' ) ;
278+ }
279+
280+ // Normalize path for the current platform
281+ pythonPath = path . normalize ( pythonPath ) ;
282+
283+ // Verify the executable exists
283284 try {
284- await fs . promises . access ( path . join ( binDir , exeName ) ) ;
285- return true ;
286- } catch ( err ) {
287- // Continue trying other executables
285+ await fs . promises . access ( pythonPath , fs . constants . X_OK ) ;
286+ } catch ( accessErr ) {
287+ // On Windows, try adding .exe if not present
288+ if ( proc . IS_WINDOWS && ! pythonPath . endsWith ( '.exe' ) ) {
289+ const pythonPathWithExe = pythonPath + '.exe' ;
290+ try {
291+ await fs . promises . access ( pythonPathWithExe , fs . constants . X_OK ) ;
292+ pythonPath = pythonPathWithExe ;
293+ } catch {
294+ throw new Error ( `Python executable not accessible at: ${ pythonPath } ` ) ;
295+ }
296+ } else {
297+ throw new Error ( `Python executable not accessible at: ${ pythonPath } ` ) ;
298+ }
288299 }
289- }
290300
291- throw new Error ( 'Python executable does not exist after venv creation!' ) ;
301+ log ( 'info' , `Verified UV-managed Python at: ${ pythonPath } ` ) ;
302+ return pythonPath ;
303+ } catch ( err ) {
304+ throw new Error ( `Could not find UV-managed Python: ${ err . message } ` ) ;
305+ }
292306}
293307
294308/**
295- * Main entry point for installing Python distribution using UV
296- * This replaces the legacy complex installation logic with a simple UV-based approach
297- * @param {string } destinationDir - Target installation directory
298- * @param {object } options - Optional configuration (kept for API compatibility)
299- * @returns {Promise<string> } Path to installed Python directory
309+ * Main entry point for ensuring Python is available via UV
310+ * UV will download and manage Python automatically, no venv needed
311+ * @returns {Promise<string> } Path to UV-managed Python executable
300312 * @throws {Error } If Python installation fails for any reason
301313 */
302- export async function installPortablePython ( destinationDir ) {
303- log ( 'info' , 'Starting Python 3.13 installation ' ) ;
314+ export async function installPortablePython ( ) {
315+ log ( 'info' , 'Ensuring Python 3.13 is available via UV ' ) ;
304316
305- // UV-based installation is now the only supported method
306317 try {
307- return await installPythonWithUV ( destinationDir , '3.13' ) ;
318+ // Ensure Python is available via UV (will download if needed)
319+ const pythonPath = await ensurePythonWithUV ( '3.13' ) ;
320+ log ( 'info' , `Python available at: ${ pythonPath } ` ) ;
321+ return pythonPath ;
308322 } catch ( uvError ) {
309- log ( 'error' , `UV installation failed: ${ uvError . message } ` ) ;
323+ log ( 'error' , `UV Python setup failed: ${ uvError . message } ` ) ;
310324 throw new Error (
311325 `Python installation failed: ${ uvError . message } . Please ensure UV can be installed and internet connection is available.` ,
312326 ) ;
313327 }
314328}
315329
316330/**
317- * Locate Python executable in a venv directory
318- * Uses standard venv structure (bin/ on Unix, Scripts/ on Windows)
319- * @param {string } pythonDir - Python venv directory to search
320- * @returns {Promise<string> } Full path to Python executable
321- * @throws {Error } If no executable found in the venv
331+ * Get the path to UV-managed Python executable
332+ * @param {string } pythonVersion - Python version (default: "3.13")
333+ * @returns {Promise<string> } Path to UV-managed Python executable
322334 */
323- function getPythonExecutablePath ( pythonDir ) {
324- // Standard venv structure
325- const binDir = proc . IS_WINDOWS
326- ? path . join ( pythonDir , 'Scripts' )
327- : path . join ( pythonDir , 'bin' ) ;
328- const executables = proc . IS_WINDOWS ? [ 'python.exe' ] : [ 'python3' , 'python' ] ;
329-
330- for ( const exeName of executables ) {
331- const fullPath = path . join ( binDir , exeName ) ;
332- try {
333- fs . accessSync ( fullPath , fs . constants . X_OK ) ;
334- log ( 'info' , `Found Python executable: ${ fullPath } ` ) ;
335- return fullPath ;
336- } catch ( err ) {
337- // Continue searching through all executables
338- }
339- }
340-
341- throw new Error ( `Could not find Python executable in venv ${ pythonDir } ` ) ;
335+ async function getPythonExecutablePath ( pythonVersion = '3.13' ) {
336+ return await getUVPythonPath ( pythonVersion ) ;
342337}
343338
344339// Export utility functions for external use
345- export { isPythonVersionCompatible , isUVAvailable , installUV , getPythonExecutablePath } ;
340+ export {
341+ isPythonVersionCompatible ,
342+ isUVAvailable ,
343+ installUV ,
344+ getPythonExecutablePath ,
345+ getUVCommand ,
346+ } ;
0 commit comments