mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-31 21:39:28 +00:00
fix: Windows venv initialization issues
This commit is contained in:
parent
ba73b6b9b6
commit
bdda1b4e87
7 changed files with 222 additions and 131 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
29
scripts/compile-babel.js
Normal file
29
scripts/compile-babel.js
Normal file
|
|
@ -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',
|
||||
});
|
||||
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue