This fixes critical bug where macOS users with default Python 3.9.6 couldn't use Auto-Claude because claude-agent-sdk requires Python 3.10+. Root Cause: - Auto-Claude doesn't bundle Python, relies on system Python - python-detector.ts accepted any Python 3.x without checking minimum version - macOS ships with Python 3.9.6 by default (incompatible) - GitHub Actions runners didn't explicitly set Python version Changes: 1. python-detector.ts: - Added getPythonVersion() to extract version from command - Added validatePythonVersion() to check if >= 3.10.0 - Updated findPythonCommand() to skip Python < 3.10 with clear error messages 2. python-env-manager.ts: - Import and use findPythonCommand() (already has version validation) - Simplified findSystemPython() to use shared validation logic - Updated error message from "Python 3.9+" to "Python 3.10+" with download link 3. .github/workflows/release.yml: - Added Python 3.11 setup to all 4 build jobs (macOS Intel, macOS ARM64, Windows, Linux) - Ensures consistent Python version across all platforms during build Impact: - macOS users with Python 3.9 now see clear error with download link - macOS users with Python 3.10+ work normally - CI/CD builds use consistent Python 3.11 - Prevents "ModuleNotFoundError: dotenv" and dependency install failures Fixes #180, #167 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e3eec68aab
commit
f168bdc3ac
@@ -20,6 +20,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -84,6 +89,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -147,6 +157,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -188,6 +203,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -18,17 +18,18 @@ export function findPythonCommand(): string | null {
|
||||
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
}).toString();
|
||||
|
||||
if (version.includes('Python 3')) {
|
||||
// Validate version meets minimum requirement (Python 3.10+)
|
||||
const validation = validatePythonVersion(cmd);
|
||||
if (validation.valid) {
|
||||
console.log(`[Python] Found valid Python: ${cmd} (${validation.version})`);
|
||||
return cmd;
|
||||
} else {
|
||||
console.warn(`[Python] ${cmd} version too old: ${validation.message}`);
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// Command not found or errored, try next
|
||||
console.warn(`[Python] Command not found or errored: ${cmd}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -37,6 +38,71 @@ export function findPythonCommand(): string | null {
|
||||
return isWindows ? 'python' : 'python3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Python version from a command.
|
||||
*
|
||||
* @param pythonCmd - The Python command to check (e.g., "python3", "py -3")
|
||||
* @returns The version string (e.g., "3.10.5") or null if unable to detect
|
||||
*/
|
||||
function getPythonVersion(pythonCmd: string): string | null {
|
||||
try {
|
||||
const version = execSync(`${pythonCmd} --version`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
}).toString().trim();
|
||||
|
||||
// Extract version number from "Python 3.10.5" format
|
||||
const match = version.match(/Python (\d+\.\d+\.\d+)/);
|
||||
return match ? match[1] : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a Python command meets minimum version requirements.
|
||||
*
|
||||
* @param pythonCmd - The Python command to validate
|
||||
* @returns Validation result with status, version, and message
|
||||
*/
|
||||
function validatePythonVersion(pythonCmd: string): {
|
||||
valid: boolean;
|
||||
version?: string;
|
||||
message: string;
|
||||
} {
|
||||
const MINIMUM_VERSION = '3.10.0';
|
||||
|
||||
const versionStr = getPythonVersion(pythonCmd);
|
||||
if (!versionStr) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Unable to detect Python version'
|
||||
};
|
||||
}
|
||||
|
||||
// Parse version numbers for comparison
|
||||
const [major, minor] = versionStr.split('.').map(Number);
|
||||
const [reqMajor, reqMinor] = MINIMUM_VERSION.split('.').map(Number);
|
||||
|
||||
const meetsRequirement =
|
||||
major > reqMajor || (major === reqMajor && minor >= reqMinor);
|
||||
|
||||
if (!meetsRequirement) {
|
||||
return {
|
||||
valid: false,
|
||||
version: versionStr,
|
||||
message: `Python ${versionStr} is too old. Requires Python ${MINIMUM_VERSION}+ (claude-agent-sdk requirement)`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
version: versionStr,
|
||||
message: `Python ${versionStr} meets requirements`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Python command for the current platform.
|
||||
* This is a synchronous fallback that doesn't test if Python actually exists.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { EventEmitter } from 'events';
|
||||
import { app } from 'electron';
|
||||
import { findPythonCommand } from './python-detector';
|
||||
|
||||
export interface PythonEnvStatus {
|
||||
ready: boolean;
|
||||
@@ -96,61 +97,29 @@ export class PythonEnvManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find system Python3
|
||||
* Find system Python 3.10+
|
||||
* Uses the shared python-detector logic which validates version requirements.
|
||||
*/
|
||||
private findSystemPython(): string | null {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Windows candidates - py launcher is handled specially
|
||||
// Unix candidates - try python3 first, then python
|
||||
const candidates = isWindows
|
||||
? ['python', 'python3']
|
||||
: ['python3', 'python'];
|
||||
|
||||
// On Windows, try the py launcher first (most reliable)
|
||||
if (isWindows) {
|
||||
try {
|
||||
// py -3 runs Python 3, verify it works
|
||||
const version = execSync('py -3 --version', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000
|
||||
}).toString();
|
||||
if (version.includes('Python 3')) {
|
||||
// Get the actual executable path
|
||||
const pythonPath = execSync('py -3 -c "import sys; print(sys.executable)"', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000
|
||||
}).toString().trim();
|
||||
return pythonPath;
|
||||
}
|
||||
} catch {
|
||||
// py launcher not available, continue with other candidates
|
||||
}
|
||||
const pythonCmd = findPythonCommand();
|
||||
if (!pythonCmd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000
|
||||
}).toString();
|
||||
if (version.includes('Python 3')) {
|
||||
// Get the actual path
|
||||
// On Windows, use Python itself to get the path
|
||||
// On Unix, use 'which'
|
||||
const pathCmd = isWindows
|
||||
? `${cmd} -c "import sys; print(sys.executable)"`
|
||||
: `which ${cmd}`;
|
||||
const pythonPath = execSync(pathCmd, { stdio: 'pipe', timeout: 5000 })
|
||||
.toString()
|
||||
.trim();
|
||||
return pythonPath;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// Get the actual executable path from the command
|
||||
// For commands like "py -3", we need to resolve to the actual executable
|
||||
const pythonPath = execSync(`${pythonCmd} -c "import sys; print(sys.executable)"`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000
|
||||
}).toString().trim();
|
||||
|
||||
console.log(`[PythonEnvManager] Found Python at: ${pythonPath}`);
|
||||
return pythonPath;
|
||||
} catch (err) {
|
||||
console.error(`[PythonEnvManager] Failed to get Python path for ${pythonCmd}:`, err);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,7 +130,11 @@ export class PythonEnvManager extends EventEmitter {
|
||||
|
||||
const systemPython = this.findSystemPython();
|
||||
if (!systemPython) {
|
||||
this.emit('error', 'Python 3 not found. Please install Python 3.9+');
|
||||
this.emit(
|
||||
'error',
|
||||
'Python 3.10+ not found. Please install Python 3.10 or higher (required by claude-agent-sdk).\n\n' +
|
||||
'Download: https://www.python.org/downloads/'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user