mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
fix(python): improve Python detection and pip command reliability
- Add shared python-utils.ts module for consistent Python detection - Use `python -m pip` instead of direct pip command (fixes "pip not found") - Support CCW_PYTHON env var for custom Python path - Use Windows py launcher to find compatible versions (3.9-3.12) - Warn users when Python version may not be compatible with onnxruntime Fixes issues where users couldn't install ccw-litellm due to: - pip not in PATH - Only pip3 available (not pip) - Python 3.13+ without onnxruntime support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join as pathJoin } from 'path';
|
||||
import { getSystemPython } from '../../utils/python-utils.js';
|
||||
|
||||
// Get current module path for package-relative lookups
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -834,10 +835,13 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Use shared Python detection for consistent cross-platform behavior
|
||||
const pythonCmd = getSystemPython();
|
||||
|
||||
if (!packagePath) {
|
||||
// Try pip install from PyPI as fallback
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', 'ccw-litellm'], { shell: true, timeout: 300000 });
|
||||
const proc = spawn(pythonCmd, ['-m', 'pip', 'install', 'ccw-litellm'], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
@@ -857,7 +861,7 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
|
||||
// Install from local package
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', '-e', packagePath], { shell: true, timeout: 300000 });
|
||||
const proc = spawn(pythonCmd, ['-m', 'pip', 'install', '-e', packagePath], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
@@ -892,8 +896,11 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
|
||||
// Use shared Python detection for consistent cross-platform behavior
|
||||
const pythonCmd = getSystemPython();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['uninstall', '-y', 'ccw-litellm'], { shell: true, timeout: 120000 });
|
||||
const proc = spawn(pythonCmd, ['-m', 'pip', 'uninstall', '-y', 'ccw-litellm'], { shell: true, timeout: 120000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
|
||||
@@ -16,6 +16,7 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getSystemPython } from '../utils/python-utils.js';
|
||||
|
||||
// Get directory of this module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -131,25 +132,7 @@ function clearVenvStatusCache(): void {
|
||||
venvStatusCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect available Python 3 executable
|
||||
* @returns Python executable command
|
||||
*/
|
||||
function getSystemPython(): string {
|
||||
const commands = process.platform === 'win32' ? ['python', 'py', 'python3'] : ['python3', 'python'];
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' });
|
||||
if (version.includes('Python 3')) {
|
||||
return cmd;
|
||||
}
|
||||
} catch {
|
||||
// Try next command
|
||||
}
|
||||
}
|
||||
throw new Error('Python 3 not found. Please install Python 3 and ensure it is in PATH.');
|
||||
}
|
||||
// Python detection functions imported from ../utils/python-utils.js
|
||||
|
||||
/**
|
||||
* Check if CodexLens venv exists and has required packages
|
||||
|
||||
121
ccw/src/utils/python-utils.ts
Normal file
121
ccw/src/utils/python-utils.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Python detection and version compatibility utilities
|
||||
* Shared module for consistent Python discovery across the application
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Parse Python version string to major.minor numbers
|
||||
* @param versionStr - Version string like "Python 3.11.5"
|
||||
* @returns Object with major and minor version numbers, or null if parsing fails
|
||||
*/
|
||||
export function parsePythonVersion(versionStr: string): { major: number; minor: number } | null {
|
||||
const match = versionStr.match(/Python\s+(\d+)\.(\d+)/);
|
||||
if (match) {
|
||||
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Python version is compatible with onnxruntime (3.9-3.12)
|
||||
* @param major - Major version number
|
||||
* @param minor - Minor version number
|
||||
* @returns true if compatible
|
||||
*/
|
||||
export function isPythonVersionCompatible(major: number, minor: number): boolean {
|
||||
// onnxruntime currently supports Python 3.9-3.12
|
||||
return major === 3 && minor >= 9 && minor <= 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect available Python 3 executable
|
||||
* Supports CCW_PYTHON environment variable for custom Python path
|
||||
* On Windows, uses py launcher to find compatible versions
|
||||
* @returns Python executable command
|
||||
*/
|
||||
export function getSystemPython(): string {
|
||||
// Check for user-specified Python via environment variable
|
||||
const customPython = process.env.CCW_PYTHON;
|
||||
if (customPython) {
|
||||
try {
|
||||
const version = execSync(`"${customPython}" --version 2>&1`, { encoding: 'utf8' });
|
||||
if (version.includes('Python 3')) {
|
||||
const parsed = parsePythonVersion(version);
|
||||
if (parsed && !isPythonVersionCompatible(parsed.major, parsed.minor)) {
|
||||
console.warn(`[Python] Warning: CCW_PYTHON points to Python ${parsed.major}.${parsed.minor}, which may not be compatible with onnxruntime (requires 3.9-3.12)`);
|
||||
}
|
||||
return `"${customPython}"`;
|
||||
}
|
||||
} catch {
|
||||
console.warn(`[Python] Warning: CCW_PYTHON="${customPython}" is not a valid Python executable, falling back to system Python`);
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, try py launcher with specific versions first (3.12, 3.11, 3.10, 3.9)
|
||||
if (process.platform === 'win32') {
|
||||
const compatibleVersions = ['3.12', '3.11', '3.10', '3.9'];
|
||||
for (const ver of compatibleVersions) {
|
||||
try {
|
||||
const version = execSync(`py -${ver} --version 2>&1`, { encoding: 'utf8' });
|
||||
if (version.includes(`Python ${ver}`)) {
|
||||
console.log(`[Python] Found compatible Python ${ver} via py launcher`);
|
||||
return `py -${ver}`;
|
||||
}
|
||||
} catch {
|
||||
// Version not installed, try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commands = process.platform === 'win32' ? ['python', 'py', 'python3'] : ['python3', 'python'];
|
||||
let fallbackCmd: string | null = null;
|
||||
let fallbackVersion: { major: number; minor: number } | null = null;
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' });
|
||||
if (version.includes('Python 3')) {
|
||||
const parsed = parsePythonVersion(version);
|
||||
if (parsed) {
|
||||
// Prefer compatible version (3.9-3.12)
|
||||
if (isPythonVersionCompatible(parsed.major, parsed.minor)) {
|
||||
return cmd;
|
||||
}
|
||||
// Keep track of first Python 3 found as fallback
|
||||
if (!fallbackCmd) {
|
||||
fallbackCmd = cmd;
|
||||
fallbackVersion = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Try next command
|
||||
}
|
||||
}
|
||||
|
||||
// If no compatible version found, use fallback with warning
|
||||
if (fallbackCmd && fallbackVersion) {
|
||||
console.warn(`[Python] Warning: Only Python ${fallbackVersion.major}.${fallbackVersion.minor} found, which may not be compatible with onnxruntime (requires 3.9-3.12).`);
|
||||
console.warn('[Python] To use a specific Python version, set CCW_PYTHON environment variable:');
|
||||
console.warn(' Windows: set CCW_PYTHON=C:\\path\\to\\python.exe');
|
||||
console.warn(' Unix: export CCW_PYTHON=/path/to/python3.11');
|
||||
console.warn('[Python] Alternatively, use LiteLLM embedding backend which has no Python version restrictions.');
|
||||
return fallbackCmd;
|
||||
}
|
||||
|
||||
throw new Error('Python 3 not found. Please install Python 3.9-3.12 and ensure it is in PATH, or set CCW_PYTHON environment variable.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Python command for pip operations (uses -m pip for reliability)
|
||||
* @returns Array of command arguments for spawn
|
||||
*/
|
||||
export function getPipCommand(): { pythonCmd: string; pipArgs: string[] } {
|
||||
const pythonCmd = getSystemPython();
|
||||
return {
|
||||
pythonCmd,
|
||||
pipArgs: ['-m', 'pip']
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user