| export type CommandResult = { |
| ok: boolean |
| stdout: string |
| stderr: string |
| code: number |
| } |
|
|
| export type CommandRunner = ( |
| cmd: string, |
| args: string[], |
| ) => Promise<CommandResult> |
|
|
| type PythonCandidate = { |
| command: string |
| prefixArgs: string[] |
| locator: { |
| command: string |
| args: string[] |
| } | null |
| } |
|
|
| export type PythonRuntimeResolution = { |
| installed: boolean |
| version: string | null |
| path: string | null |
| command: string | null |
| prefixArgs: string[] |
| source: 'custom' | 'system' | 'venv' | null |
| error: string | null |
| } |
|
|
| function getPythonCandidates(platform: NodeJS.Platform): PythonCandidate[] { |
| if (platform === 'win32') { |
| return [ |
| { |
| command: 'python3', |
| prefixArgs: [], |
| locator: { command: 'where', args: ['python3'] }, |
| }, |
| { |
| command: 'python', |
| prefixArgs: [], |
| locator: { command: 'where', args: ['python'] }, |
| }, |
| { |
| command: 'py', |
| prefixArgs: ['-3'], |
| locator: { command: 'where', args: ['py'] }, |
| }, |
| { |
| command: 'py', |
| prefixArgs: [], |
| locator: { command: 'where', args: ['py'] }, |
| }, |
| ] |
| } |
|
|
| return [ |
| { |
| command: 'python3', |
| prefixArgs: [], |
| locator: { command: 'which', args: ['python3'] }, |
| }, |
| ] |
| } |
|
|
| function extractPythonVersion(output: string): string | null { |
| const match = output.match(/Python\s+([0-9][^\s]*)/i) |
| return match?.[1] ?? null |
| } |
|
|
| export function isPythonVersionAtLeast( |
| version: string | null, |
| major: number, |
| minor: number, |
| ): boolean { |
| if (!version) return false |
| const match = version.match(/^(\d+)\.(\d+)/) |
| if (!match) return false |
| const currentMajor = Number(match[1]) |
| const currentMinor = Number(match[2]) |
| return currentMajor > major || (currentMajor === major && currentMinor >= minor) |
| } |
|
|
| function firstOutputLine(output: string): string | null { |
| const line = output |
| .split(/\r?\n/) |
| .map(value => value.trim()) |
| .find(Boolean) |
| return line ?? null |
| } |
|
|
| async function locateCandidatePath( |
| candidate: PythonCandidate, |
| runCommand: CommandRunner, |
| ): Promise<string | null> { |
| if (!candidate.locator) return null |
| const locateResult = await runCommand(candidate.locator.command, candidate.locator.args) |
| if (!locateResult.ok) return null |
| return firstOutputLine(locateResult.stdout) |
| } |
|
|
| export async function detectPythonRuntime( |
| platform: NodeJS.Platform, |
| runCommand: CommandRunner, |
| venvPythonPath?: string, |
| customPythonPath?: string | null, |
| ): Promise<PythonRuntimeResolution> { |
| const normalizedCustomPath = customPythonPath?.trim() |
| if (normalizedCustomPath) { |
| const customResult = await runCommand(normalizedCustomPath, ['--version']) |
| if (customResult.ok) { |
| return { |
| installed: true, |
| version: extractPythonVersion(`${customResult.stdout}\n${customResult.stderr}`), |
| path: normalizedCustomPath, |
| command: normalizedCustomPath, |
| prefixArgs: [], |
| source: 'custom', |
| error: null, |
| } |
| } |
|
|
| return { |
| installed: false, |
| version: null, |
| path: normalizedCustomPath, |
| command: normalizedCustomPath, |
| prefixArgs: [], |
| source: 'custom', |
| error: customResult.stderr || customResult.stdout || `Failed to run ${normalizedCustomPath}`, |
| } |
| } |
|
|
| for (const candidate of getPythonCandidates(platform)) { |
| const versionResult = await runCommand(candidate.command, [...candidate.prefixArgs, '--version']) |
| if (!versionResult.ok) continue |
|
|
| return { |
| installed: true, |
| version: extractPythonVersion(`${versionResult.stdout}\n${versionResult.stderr}`), |
| path: await locateCandidatePath(candidate, runCommand), |
| command: candidate.command, |
| prefixArgs: candidate.prefixArgs, |
| source: 'system', |
| error: null, |
| } |
| } |
|
|
| if (venvPythonPath) { |
| const venvResult = await runCommand(venvPythonPath, ['--version']) |
| if (venvResult.ok) { |
| return { |
| installed: true, |
| version: extractPythonVersion(`${venvResult.stdout}\n${venvResult.stderr}`), |
| path: venvPythonPath, |
| command: venvPythonPath, |
| prefixArgs: [], |
| source: 'venv', |
| error: null, |
| } |
| } |
| } |
|
|
| return { |
| installed: false, |
| version: null, |
| path: null, |
| command: null, |
| prefixArgs: [], |
| source: null, |
| error: null, |
| } |
| } |
|
|