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 }