187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import { spawn } from 'child_process';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
/**
|
||
* Python路径检测工具
|
||
* 支持在Windows、macOS、Linux等多个系统上检测Python
|
||
*/
|
||
|
||
let cachedPythonPath: string | null = null;
|
||
|
||
/**
|
||
* 获取系统中可用的Python可执行文件路径
|
||
* 优先级:
|
||
* 1. 环境变量 PYTHON_PATH
|
||
* 2. 环境变量 FUNASR_PYTHON_PATH
|
||
* 3. Windows特定位置
|
||
* 4. 系统PATH中的python3/python
|
||
*/
|
||
export function getPythonPath(): string {
|
||
// 如果已缓存,直接返回
|
||
if (cachedPythonPath) {
|
||
return cachedPythonPath;
|
||
}
|
||
|
||
// 1. 检查环境变量
|
||
if (process.env.PYTHON_PATH && fs.existsSync(process.env.PYTHON_PATH)) {
|
||
cachedPythonPath = process.env.PYTHON_PATH;
|
||
return cachedPythonPath;
|
||
}
|
||
|
||
if (process.env.FUNASR_PYTHON_PATH && fs.existsSync(process.env.FUNASR_PYTHON_PATH)) {
|
||
cachedPythonPath = process.env.FUNASR_PYTHON_PATH;
|
||
return cachedPythonPath;
|
||
}
|
||
|
||
// 2. Windows特定位置
|
||
if (process.platform === 'win32') {
|
||
const windowsPythonPaths = [
|
||
// 微软应用商店安装
|
||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python312', 'python.exe'),
|
||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python311', 'python.exe'),
|
||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python310', 'python.exe'),
|
||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python39', 'python.exe'),
|
||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python38', 'python.exe'),
|
||
// 直接安装
|
||
'C:\\Python312\\python.exe',
|
||
'C:\\Python311\\python.exe',
|
||
'C:\\Python310\\python.exe',
|
||
'C:\\Python39\\python.exe',
|
||
'C:\\Python38\\python.exe',
|
||
// Anaconda/Miniconda
|
||
path.join(process.env.USERPROFILE || '', 'anaconda3', 'python.exe'),
|
||
path.join(process.env.USERPROFILE || '', 'miniconda3', 'python.exe'),
|
||
// WSL
|
||
path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'python.exe'),
|
||
];
|
||
|
||
for (const pythonPath of windowsPythonPaths) {
|
||
if (fs.existsSync(pythonPath)) {
|
||
cachedPythonPath = pythonPath;
|
||
return cachedPythonPath;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. macOS/Linux 特定位置
|
||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||
const unixPythonPaths = [
|
||
'/usr/bin/python3',
|
||
'/usr/bin/python',
|
||
'/usr/local/bin/python3',
|
||
'/usr/local/bin/python',
|
||
'/opt/homebrew/bin/python3', // M1/M2 Mac
|
||
path.join(process.env.HOME || '', '.pyenv', 'versions', 'default', 'bin', 'python'),
|
||
];
|
||
|
||
for (const pythonPath of unixPythonPaths) {
|
||
if (fs.existsSync(pythonPath)) {
|
||
cachedPythonPath = pythonPath;
|
||
return cachedPythonPath;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 默认尝试 python 或 python3
|
||
// 这会依赖系统 PATH,Windows上可能不工作
|
||
if (process.platform === 'win32') {
|
||
cachedPythonPath = 'python';
|
||
} else {
|
||
cachedPythonPath = 'python3';
|
||
}
|
||
|
||
return cachedPythonPath;
|
||
}
|
||
|
||
/**
|
||
* 验证Python可执行文件是否可用
|
||
*/
|
||
export function isValidPythonPath(pythonPath: string): boolean {
|
||
return fs.existsSync(pythonPath);
|
||
}
|
||
|
||
/**
|
||
* 检查Python是否安装了特定的库
|
||
*/
|
||
export async function checkPythonModule(moduleName: string): Promise<boolean> {
|
||
return new Promise((resolve) => {
|
||
const pythonPath = getPythonPath();
|
||
|
||
const process = spawn(pythonPath, ['-m', 'pip', 'show', moduleName], {
|
||
stdio: ['pipe', 'pipe', 'pipe'],
|
||
shell: true
|
||
});
|
||
|
||
let stdout = '';
|
||
|
||
process.stdout.on('data', (data) => {
|
||
stdout += data.toString();
|
||
});
|
||
|
||
process.on('close', (code) => {
|
||
resolve(code === 0 && stdout.includes(`Name: ${moduleName}`));
|
||
});
|
||
|
||
process.on('error', () => {
|
||
resolve(false);
|
||
});
|
||
|
||
// 设置超时
|
||
setTimeout(() => {
|
||
process.kill();
|
||
resolve(false);
|
||
}, 5000);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取Python版本信息
|
||
*/
|
||
export async function getPythonVersion(): Promise<string | null> {
|
||
return new Promise((resolve) => {
|
||
const pythonPath = getPythonPath();
|
||
|
||
const process = spawn(pythonPath, ['--version'], {
|
||
stdio: ['pipe', 'pipe', 'pipe'],
|
||
shell: true
|
||
});
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
|
||
process.stdout.on('data', (data) => {
|
||
stdout += data.toString();
|
||
});
|
||
|
||
process.stderr.on('data', (data) => {
|
||
stderr += data.toString();
|
||
});
|
||
|
||
process.on('close', (code) => {
|
||
if (code === 0) {
|
||
resolve((stdout || stderr).trim());
|
||
} else {
|
||
resolve(null);
|
||
}
|
||
});
|
||
|
||
process.on('error', () => {
|
||
resolve(null);
|
||
});
|
||
|
||
// 设置超时
|
||
setTimeout(() => {
|
||
process.kill();
|
||
resolve(null);
|
||
}, 5000);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清除缓存(用于测试)
|
||
*/
|
||
export function clearPythonPathCache(): void {
|
||
cachedPythonPath = null;
|
||
}
|