fix: Windows venv initialization issues

This commit is contained in:
4pmtong 2026-02-05 11:31:29 +08:00
parent ba73b6b9b6
commit bdda1b4e87
7 changed files with 222 additions and 131 deletions

View file

@ -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

View file

@ -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');

View file

@ -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;
}

View file

@ -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
View 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',
});

View file

@ -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!');
}
}

View file

@ -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',
});