diff --git a/electron/main/init.ts b/electron/main/init.ts index 27cd253c..e2648896 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -31,6 +31,7 @@ import { getPrebuiltVenvPath, getUvEnv, getVenvPath, + getVenvPythonPath, isBinaryExists, killProcessByName, } from './utils/process'; @@ -332,21 +333,25 @@ export async function startBackend( } }; + const pythonPath = getVenvPythonPath(venvPath); + // Dev mode: use uv run (ensures sync); Packaged: use venv's python directly (prebuilt has deps) + const useDirectPython = app.isPackaged; + return new Promise(async (resolve, reject) => { - log.info( - `Spawning backend process: ${uv_path} run uvicorn main:api --port ${port} --loop asyncio` - ); + const spawnCmd = useDirectPython + ? `${pythonPath} -m uvicorn main:api --port ${port} --loop asyncio` + : `${uv_path} run python -m uvicorn main:api --port ${port} --loop asyncio`; + log.info(`Spawning backend process: ${spawnCmd}`); log.info(`Backend working directory: ${backendPath}`); - log.info(`Using venv: ${venvPath}`); try { - const { stdout: uvVersion } = await execAsync(`${uv_path} --version`); - log.info(`UV version check: ${uvVersion.trim()}`); - - const { stdout: pythonTest } = await execAsync( - `${uv_path} run python -c "print('Python OK')"`, - { cwd: backendPath, env: env } - ); + const pythonTestCmd = useDirectPython + ? `"${pythonPath}" -c "print('Python OK')"` + : `"${uv_path}" run python -c "print('Python OK')"`; + const { stdout: pythonTest } = await execAsync(pythonTestCmd, { + cwd: backendPath, + env: env, + }); log.info(`Python test output: ${pythonTest.trim()}`); } catch (testErr: any) { log.warn(`Pre-flight check failed, attempting repair: ${testErr}`); @@ -439,10 +444,13 @@ export async function startBackend( }); // Retry the check - const { stdout: pythonTest } = await execAsync( - `${uv_path} run python -c "print('Python OK')"`, - { cwd: backendPath, env: env } - ); + const retryTestCmd = useDirectPython + ? `"${pythonPath}" -c "print('Python OK')"` + : `"${uv_path}" run python -c "print('Python OK')"`; + const { stdout: pythonTest } = await execAsync(retryTestCmd, { + cwd: backendPath, + env: env, + }); log.info(`Python test output after repair: ${pythonTest.trim()}`); } catch (repairErr) { log.error(`Repair failed: ${repairErr}`); @@ -455,24 +463,45 @@ export async function startBackend( } } - const node_process = spawn( - uv_path, - [ - 'run', - 'uvicorn', - 'main:api', - '--port', - port.toString(), - '--loop', - 'asyncio', - ], - { - cwd: backendPath, - env: env, - detached: process.platform !== 'win32', - stdio: ['ignore', 'ignore', 'pipe'], // stdin=ignore, stdout=ignore, stderr=pipe (Python logs to stderr) - } - ); + const node_process = useDirectPython + ? spawn( + pythonPath, + [ + '-m', + 'uvicorn', + 'main:api', + '--port', + port.toString(), + '--loop', + 'asyncio', + ], + { + cwd: backendPath, + env: env, + detached: process.platform !== 'win32', + stdio: ['ignore', 'ignore', 'pipe'], + } + ) + : spawn( + uv_path, + [ + 'run', + 'python', + '-m', + 'uvicorn', + 'main:api', + '--port', + port.toString(), + '--loop', + 'asyncio', + ], + { + cwd: backendPath, + env: env, + detached: process.platform !== 'win32', + stdio: ['ignore', 'ignore', 'pipe'], + } + ); // NOTE: Do NOT use unref() - we need to maintain the process reference // to properly capture stdout/stderr and manage the process lifecycle diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 21e1b489..dc4cce32 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -28,6 +28,7 @@ import { getTerminalVenvPath, getUvEnv, getVenvPath, + getVenvPythonPath, isBinaryExists, runInstallScript, TERMINAL_BASE_PACKAGES, @@ -753,13 +754,15 @@ export async function installDependencies( `Install Dependencies completed ${message} for version ${version}` ); console.log(`Virtual environment path: ${venvPath}`); - spawn(uv_path, ['run', 'task', 'babel'], { - cwd: backendPath, - env: { - ...process.env, - UV_PROJECT_ENVIRONMENT: venvPath, - }, - }); + const pythonPath = getVenvPythonPath(venvPath); + spawn( + pythonPath, + ['-m', 'babel.messages.frontend', 'compile', '-d', 'lang'], + { + cwd: backendPath, + env: { ...process.env }, + } + ); }, notifyInstallDependenciesPage: (): boolean => { const success = safeMainWindowSend('install-dependencies-start'); diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index a3acc2ca..f19b6401 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -159,7 +159,6 @@ function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean { try { let content = fs.readFileSync(pyvenvCfgPath, 'utf-8'); - // Check if the file contains placeholder that needs to be replaced if (content.includes('{{PREBUILT_PYTHON_DIR}}')) { const prebuiltPythonDir = getPrebuiltPythonDir(); if (!prebuiltPythonDir) { @@ -169,12 +168,26 @@ function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean { return false; } - // Replace placeholder with actual path - // On Windows, path.join returns paths with backslashes, which matches our placeholder format content = content.replace( /\{\{PREBUILT_PYTHON_DIR\}\}/g, prebuiltPythonDir ); + + const homeMatch = content.match(/^home\s*=\s*(.+)$/m); + if (homeMatch) { + const finalHomePath = homeMatch[1].trim(); + log.info(`[VENV] pyvenv.cfg home path set to: ${finalHomePath}`); + + // Verify the path exists + if (!fs.existsSync(finalHomePath)) { + log.warn( + `[VENV] WARNING: home path does not exist: ${finalHomePath}` + ); + } else { + log.info(`[VENV] home path verified successfully`); + } + } + fs.writeFileSync(pyvenvCfgPath, content); log.info( `[VENV] Fixed pyvenv.cfg placeholder with: ${prebuiltPythonDir}` @@ -182,7 +195,6 @@ function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean { return true; } - // No placeholder found, check if path is valid const homeMatch = content.match(/^home\s*=\s*(.+)$/m); if (homeMatch) { const homePath = homeMatch[1].trim(); @@ -233,39 +245,64 @@ function fixVenvScriptShebangs(venvPath: string): boolean { for (const entry of entries) { const filePath = path.join(binDir, entry); - // Skip directories, symlinks, and the python executables themselves try { const stat = fs.lstatSync(filePath); if (stat.isDirectory() || stat.isSymbolicLink()) { continue; } - if (entry.startsWith('python') || entry.startsWith('activate')) { + // Skip .exe files (binary), .dll, .pyd (compiled Python modules) + if ( + entry.endsWith('.exe') || + entry.endsWith('.dll') || + entry.endsWith('.pyd') || + entry.startsWith('python') || + entry.startsWith('activate') + ) { continue; } - } catch (err) { + } catch { continue; } try { const content = fs.readFileSync(filePath, 'utf-8'); - // Check if file contains shebang placeholder - if (content.includes('{{PREBUILT_VENV_PYTHON}}')) { - // Replace placeholder with actual python path - const newContent = content.replace( - /^#!\{\{PREBUILT_VENV_PYTHON\}\}/m, - `#!${pythonExe}` - ); + // Check if file contains any placeholders + const hasVenvPythonPlaceholder = content.includes( + '{{PREBUILT_VENV_PYTHON}}' + ); + const hasPythonDirPlaceholder = content.includes( + '{{PREBUILT_PYTHON_DIR}}' + ); + + if (hasVenvPythonPlaceholder || hasPythonDirPlaceholder) { + let newContent = content; + if (hasVenvPythonPlaceholder) { + newContent = newContent.replace( + /\{\{PREBUILT_VENV_PYTHON\}\}/g, + pythonExe + ); + } + if (hasPythonDirPlaceholder) { + const prebuiltPythonDir = getPrebuiltPythonDir(); + if (prebuiltPythonDir) { + newContent = newContent.replace( + /\{\{PREBUILT_PYTHON_DIR\}\}/g, + prebuiltPythonDir + ); + } + } if (newContent !== content) { fs.writeFileSync(filePath, newContent, 'utf-8'); - fs.chmodSync(filePath, 0o755); + if (process.platform !== 'win32') { + fs.chmodSync(filePath, 0o755); + } fixedCount++; } } - } catch (err) { + } catch { // Silently skip files that can't be processed - continue; } } @@ -288,31 +325,20 @@ export function getPrebuiltVenvPath(): string | null { } const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv'); - if (fs.existsSync(prebuiltVenvPath)) { - const pyvenvCfgPath = path.join(prebuiltVenvPath, 'pyvenv.cfg'); - if (fs.existsSync(pyvenvCfgPath)) { - // Fix placeholder in pyvenv.cfg if needed - fixPyvenvCfgPlaceholder(pyvenvCfgPath); + const pyvenvCfgPath = path.join(prebuiltVenvPath, 'pyvenv.cfg'); - // Fix shebang placeholders in all scripts - fixVenvScriptShebangs(prebuiltVenvPath); + log.info(`[VENV] Checking prebuilt venv at: ${prebuiltVenvPath}`); - // Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python) - const isWindows = process.platform === 'win32'; - const pythonExePath = isWindows - ? path.join(prebuiltVenvPath, 'Scripts', 'python.exe') - : path.join(prebuiltVenvPath, 'bin', 'python'); + if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(pyvenvCfgPath)) { + fixPyvenvCfgPlaceholder(pyvenvCfgPath); + fixVenvScriptShebangs(prebuiltVenvPath); - if (fs.existsSync(pythonExePath)) { - log.info(`Using prebuilt venv: ${prebuiltVenvPath}`); - return prebuiltVenvPath; - } else { - log.warn( - `Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` + - `Falling back to user venv.` - ); - } + const pythonExePath = getVenvPythonPath(prebuiltVenvPath); + if (fs.existsSync(pythonExePath)) { + log.info(`[VENV] Using prebuilt venv: ${prebuiltVenvPath}`); + return prebuiltVenvPath; } + log.warn(`[VENV] Prebuilt venv Python missing at: ${pythonExePath}`); } return null; } @@ -389,65 +415,66 @@ export function getPrebuiltTerminalVenvPath(): string | null { '.packages_installed' ); if (fs.existsSync(pyvenvCfgPath) && fs.existsSync(installedMarker)) { - // Fix placeholder in pyvenv.cfg if needed fixPyvenvCfgPlaceholder(pyvenvCfgPath); - - // Fix shebang placeholders in all scripts fixVenvScriptShebangs(prebuiltTerminalVenvPath); - const isWindows = process.platform === 'win32'; - const pythonExePath = isWindows - ? path.join(prebuiltTerminalVenvPath, 'Scripts', 'python.exe') - : path.join(prebuiltTerminalVenvPath, 'bin', 'python'); + const pythonExePath = getVenvPythonPath(prebuiltTerminalVenvPath); if (fs.existsSync(pythonExePath)) { - log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`); - return prebuiltTerminalVenvPath; - } else { - // Try to fix the missing Python executable by creating a symlink to prebuilt Python - log.warn( - `Prebuilt terminal venv found but Python executable missing at: ${pythonExePath}. ` + - `Attempting to fix...` + log.info( + `[VENV] Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}` ); - - const prebuiltPython = findPythonForTerminalVenv(); - if (prebuiltPython && fs.existsSync(prebuiltPython)) { - try { - const binDir = isWindows - ? path.join(prebuiltTerminalVenvPath, 'Scripts') - : path.join(prebuiltTerminalVenvPath, 'bin'); - - // Ensure bin directory exists - if (!fs.existsSync(binDir)) { - fs.mkdirSync(binDir, { recursive: true }); - } - - // Create symlink to prebuilt Python - if (fs.existsSync(pythonExePath)) { - // Remove existing broken symlink or file - fs.unlinkSync(pythonExePath); - } - - // Calculate relative path for symlink - const relativePath = path.relative(binDir, prebuiltPython); - fs.symlinkSync(relativePath, pythonExePath); - - log.info( - `Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}` - ); - return prebuiltTerminalVenvPath; - } catch (error) { - log.warn(`Failed to fix terminal venv Python symlink: ${error}`); - } - } - - log.warn(`Falling back to user terminal venv.`); + return prebuiltTerminalVenvPath; } + + // Try to fix the missing Python executable by creating a symlink to prebuilt Python + const prebuiltPython = findPythonForTerminalVenv(); + if (prebuiltPython && fs.existsSync(prebuiltPython)) { + try { + const binDir = path.join( + prebuiltTerminalVenvPath, + process.platform === 'win32' ? 'Scripts' : 'bin' + ); + + if (!fs.existsSync(binDir)) { + fs.mkdirSync(binDir, { recursive: true }); + } + + if (fs.existsSync(pythonExePath)) { + fs.unlinkSync(pythonExePath); + } + + const relativePath = path.relative(binDir, prebuiltPython); + fs.symlinkSync(relativePath, pythonExePath); + log.info( + `[VENV] Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}` + ); + return prebuiltTerminalVenvPath; + } catch (error) { + log.warn( + `[VENV] Failed to fix terminal venv Python symlink: ${error}` + ); + } + } + log.warn( + `[VENV] Prebuilt terminal venv Python missing, falling back to user venv` + ); } } return null; } +/** + * Get the Python executable path from a venv directory. + * Use this to directly invoke venv's python, avoiding uv/launcher placeholder issues. + */ +export function getVenvPythonPath(venvPath: string): string { + const isWindows = process.platform === 'win32'; + return isWindows + ? path.join(venvPath, 'Scripts', 'python.exe') + : path.join(venvPath, 'bin', 'python'); +} + export function getVenvPath(version: string): string { // First check for prebuilt venv in packaged app if (app.isPackaged) { @@ -586,7 +613,7 @@ export function getPrebuiltPythonDir(): string | null { 'uv_python' ); if (fs.existsSync(prebuiltPythonDir)) { - log.info(`Using prebuilt Python: ${prebuiltPythonDir}`); + log.info(`[VENV] Using prebuilt Python: ${prebuiltPythonDir}`); return prebuiltPythonDir; } diff --git a/package.json b/package.json index 5c333ea9..209f64ce 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "fix-symlinks": "node scripts/fix-symlinks.js", "clean-symlinks": "node scripts/clean-symlinks.js", "test-signing": "node scripts/test-signing.js", - "compile-babel": "cd backend && uv run pybabel compile -d lang", + "compile-babel": "node scripts/compile-babel.js", "prebuild:deps": "npm run preinstall-deps && npm run compile-babel && npm run fix-venv-paths && npm run fix-symlinks", "prebuild:deps:clean": "npm run prebuild:deps && npm run clean-symlinks", "prebuild:compile": "tsc -p tsconfig.build.json && vite build", diff --git a/scripts/compile-babel.js b/scripts/compile-babel.js new file mode 100644 index 00000000..4f7403dd --- /dev/null +++ b/scripts/compile-babel.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Directly use venv's python.exe (not uv run) to avoid Windows .exe launcher +// placeholder issues - same reason we use direct python for backend/uvicorn. +/* global process */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(__dirname, '..'); +const backendDir = path.join(projectRoot, 'backend'); +const prebuiltVenvDir = path.join(projectRoot, 'resources', 'prebuilt', 'venv'); +const isWindows = process.platform === 'win32'; + +// Prebuild uses resources/prebuilt/venv; dev may use backend/.venv +const venvDir = fs.existsSync(prebuiltVenvDir) + ? prebuiltVenvDir + : path.join(backendDir, '.venv'); +const pythonPath = isWindows + ? path.join(venvDir, 'Scripts', 'python.exe') + : path.join(venvDir, 'bin', 'python'); + +execSync(`"${pythonPath}" -m babel.messages.frontend compile -d lang`, { + cwd: backendDir, + stdio: 'inherit', +}); diff --git a/scripts/fix-symlinks.js b/scripts/fix-symlinks.js index 5a3cecdf..8fe36960 100644 --- a/scripts/fix-symlinks.js +++ b/scripts/fix-symlinks.js @@ -340,7 +340,9 @@ function fixScriptShebangs(venvPath, venvName) { * Main function */ function main() { - console.log('🔧 Fixing Python symlinks and shebangs in venv directories...'); + console.log( + '🔧 Fixing Python symlinks and placeholders in venv directories...' + ); console.log('==========================================================\n'); const venvDirs = [ @@ -384,7 +386,7 @@ function main() { console.log( `✅ Fixed symlinks in ${symlinkSuccessCount}/${totalCount} venv(s)` ); - console.log(`✅ Fixed shebangs in ${totalShebangsFixed} script(s)`); + console.log(`✅ Fixed placeholders in ${totalShebangsFixed} script(s)`); console.log('✅ Venvs are now fully portable!'); } } diff --git a/scripts/preinstall-deps.js b/scripts/preinstall-deps.js index 9885f83a..44394600 100644 --- a/scripts/preinstall-deps.js +++ b/scripts/preinstall-deps.js @@ -912,9 +912,10 @@ async function installPythonDeps(uvPath) { console.log('✅ Python dependencies installed'); console.log('📝 Compiling babel...'); - execSync(`"${uvPath}" run pybabel compile -d lang`, { + + execSync(`"${pythonExePath}" -m babel.messages.frontend compile -d lang`, { cwd: BACKEND_DIR, - env: env, + env: { ...env }, stdio: 'inherit', });