(pythonPath: string)
| 21 | * @throws Error if the path doesn't exist or no Python executable is found |
| 22 | */ |
| 23 | export async function resolvePythonExecutable(pythonPath: string): Promise<string> { |
| 24 | if (isBareSystemPython(pythonPath)) { |
| 25 | return pythonPath |
| 26 | } |
| 27 | |
| 28 | let fileStat: Awaited<ReturnType<typeof stat>> |
| 29 | try { |
| 30 | fileStat = await stat(pythonPath) |
| 31 | } catch (err) { |
| 32 | const error = err as NodeJS.ErrnoException |
| 33 | if (error.code === 'ENOENT') { |
| 34 | throw new Error(`Python path not found: ${pythonPath}`) |
| 35 | } |
| 36 | throw new Error(`Failed to access Python path: ${pythonPath} (${error.code}: ${error.message})`) |
| 37 | } |
| 38 | |
| 39 | // Case 1: Direct path to Python executable |
| 40 | if (fileStat.isFile()) { |
| 41 | const name = basename(pythonPath).toLowerCase() |
| 42 | if (name.startsWith('python')) { |
| 43 | return pythonPath |
| 44 | } |
| 45 | throw new Error( |
| 46 | `Path is a file but doesn't appear to be a Python executable: ${pythonPath}\n` + |
| 47 | 'Expected a file named python, python3, python.exe, or similar.' |
| 48 | ) |
| 49 | } |
| 50 | |
| 51 | if (!fileStat.isDirectory()) { |
| 52 | throw new Error(`Python path is neither a file nor a directory: ${pythonPath}`) |
| 53 | } |
| 54 | |
| 55 | // Case 2: Directory containing python directly (bin/ or Scripts/ folder) |
| 56 | const directPython = await findPythonInDirectory(pythonPath, PYTHON_EXECUTABLES) |
| 57 | if (directPython) { |
| 58 | return directPython |
| 59 | } |
| 60 | |
| 61 | // Case 3: Venv root directory (look in bin/ or Scripts/) |
| 62 | const binDir = join(pythonPath, VENV_BIN_DIR) |
| 63 | const binDirStat = await stat(binDir).catch(() => null) |
| 64 | |
| 65 | if (binDirStat?.isDirectory()) { |
| 66 | const venvPython = await findPythonInDirectory(binDir, PYTHON_EXECUTABLES) |
| 67 | if (venvPython) { |
| 68 | return venvPython |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // No Python found - provide helpful error message |
| 73 | const searchedPaths = [ |
| 74 | ...PYTHON_EXECUTABLES.map(c => join(pythonPath, c)), |
| 75 | ...PYTHON_EXECUTABLES.map(c => join(binDir, c)), |
| 76 | ] |
| 77 | |
| 78 | throw new Error( |
| 79 | `No Python executable found at: ${pythonPath}\n\n` + |
| 80 | `Searched for:\n${searchedPaths.map(p => ` - ${p}`).join('\n')}\n\n` + |
no test coverage detected