75 lines
2.9 KiB
JavaScript
75 lines
2.9 KiB
JavaScript
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import { spawnSync } from 'node:child_process'
|
||
|
||
function canRunPython(executable) {
|
||
const result = spawnSync(executable, ['--version'], {
|
||
encoding: 'utf8',
|
||
stdio: 'ignore',
|
||
})
|
||
return !result.error && result.status === 0
|
||
}
|
||
|
||
/**
|
||
* 解析设计构建管线唯一可用的 Python 入口。
|
||
*
|
||
* 顺序必须保持稳定:显式配置 > 项目虚拟环境 > Windows Python Manager >
|
||
* 系统命令。这样既尊重调用方选择,也不会把 WindowsApps 的零字节执行别名
|
||
* 误判为真实解释器。每个候选都必须实际执行 `--version`,文件存在本身不算可用。
|
||
*/
|
||
export function resolvePythonExecutable({
|
||
pipelineDirectory,
|
||
environment = process.env,
|
||
platform = process.platform,
|
||
pathExists = fs.existsSync,
|
||
canRun = canRunPython,
|
||
} = {}) {
|
||
if (!pipelineDirectory) throw new Error('解析 Python 入口时缺少 design-pipeline 目录')
|
||
|
||
const configured = environment.PYTHON?.trim()
|
||
if (configured) {
|
||
if (canRun(configured)) return configured
|
||
throw new Error(`PYTHON 指定的解释器不可用:${configured}`)
|
||
}
|
||
|
||
const pathApi = platform === 'win32' ? path.win32 : path.posix
|
||
const candidates = []
|
||
const virtualEnvironment = platform === 'win32'
|
||
? pathApi.join(pipelineDirectory, '.venv', 'Scripts', 'python.exe')
|
||
: pathApi.join(pipelineDirectory, '.venv', 'bin', 'python')
|
||
if (pathExists(virtualEnvironment)) candidates.push(virtualEnvironment)
|
||
|
||
if (platform === 'win32' && environment.LOCALAPPDATA) {
|
||
const managerPython = pathApi.join(environment.LOCALAPPDATA, 'Python', 'bin', 'python.exe')
|
||
if (pathExists(managerPython)) candidates.push(managerPython)
|
||
}
|
||
|
||
candidates.push(platform === 'win32' ? 'python' : 'python3', 'python')
|
||
for (const candidate of new Set(candidates)) {
|
||
if (canRun(candidate)) return candidate
|
||
}
|
||
|
||
throw new Error(
|
||
'未找到可用的 Python。请设置 PYTHON 为真实解释器路径,或在 design-pipeline/.venv 中安装项目虚拟环境。',
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 通过统一入口执行 Python,保证任何构建和测试都不会向源码目录写入 `.pyc`。
|
||
* 调用方只提供业务参数;`-B`、进程错误和退出码转换由这里集中负责。
|
||
*/
|
||
export function runPythonCommand({ executable, args, cwd, spawn = spawnSync } = {}) {
|
||
if (typeof executable !== 'string' || executable.trim() === '') throw new Error('执行 Python 时缺少解释器')
|
||
if (!Array.isArray(args)) throw new Error('执行 Python 时 args 必须为数组')
|
||
if (typeof cwd !== 'string' || cwd.trim() === '') throw new Error('执行 Python 时缺少工作目录')
|
||
|
||
const result = spawn(executable, ['-B', ...args], {
|
||
cwd,
|
||
encoding: 'utf8',
|
||
stdio: 'inherit',
|
||
})
|
||
if (result.error) throw new Error(`无法启动 Python(${executable}):${result.error.message}`)
|
||
if (result.status !== 0) throw new Error(`Python 命令执行失败,退出码:${result.status ?? 'unknown'}`)
|
||
return result
|
||
}
|