diff --git a/config/before-sign.cjs b/config/before-sign.cjs index 968f0676f..347cd35a9 100644 --- a/config/before-sign.cjs +++ b/config/before-sign.cjs @@ -80,93 +80,67 @@ exports.default = async function afterPack(context) { } } - // Find prebuilt Python executable in uv_python directory - function findPrebuiltPython() { - const uvPythonDir = path.join(prebuiltPath, 'uv_python'); - if (!fs.existsSync(uvPythonDir)) { - return null; - } - - // UV stores Python in cpython-* subdirectories - try { - const entries = fs.readdirSync(uvPythonDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name.startsWith('cpython-')) { - const pythonPath = path.join(uvPythonDir, entry.name, 'install', 'bin', 'python'); - if (fs.existsSync(pythonPath)) { - return pythonPath; - } - } - } - } catch (error) { - console.warn(`Warning: Could not search for prebuilt Python: ${error.message}`); - } - return null; - } - - const prebuiltPython = findPrebuiltPython(); - if (prebuiltPython) { - console.log(`Found prebuilt Python: ${prebuiltPython}`); - } - - // Clean and fix Python symlinks in a venv bin directory - function fixPythonSymlinks(binDir, venvName) { - if (!fs.existsSync(binDir)) { - return; - } - + // Clean Python symlinks in venv/bin + const venvBinDir = path.join(prebuiltPath, 'venv', 'bin'); + if (fs.existsSync(venvBinDir)) { const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12']; const bundlePath = path.resolve(appPath); for (const pythonName of pythonNames) { - const pythonSymlink = path.join(binDir, pythonName); + const pythonSymlink = path.join(venvBinDir, pythonName); - try { - const stats = fs.lstatSync(pythonSymlink); - if (stats.isSymbolicLink()) { - const target = fs.readlinkSync(pythonSymlink); - const resolvedPath = path.resolve(path.dirname(pythonSymlink), target); + if (fs.existsSync(pythonSymlink)) { + try { + const stats = fs.lstatSync(pythonSymlink); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(pythonSymlink); + const resolvedPath = path.resolve(path.dirname(pythonSymlink), target); - // If symlink points outside bundle or is broken, remove and recreate it - if (!resolvedPath.startsWith(bundlePath) || !fs.existsSync(resolvedPath)) { - console.log(`Removing invalid ${venvName} ${pythonName} symlink: ${target}`); - fs.unlinkSync(pythonSymlink); - - // Recreate symlink pointing to prebuilt Python (only for main 'python') - if (prebuiltPython && pythonName === 'python') { - const relativePath = path.relative(binDir, prebuiltPython); - fs.symlinkSync(relativePath, pythonSymlink); - console.log(`Created ${venvName} ${pythonName} symlink -> ${relativePath}`); + // If symlink points outside bundle, remove it + if (!resolvedPath.startsWith(bundlePath)) { + console.log(`Removing invalid ${pythonName} symlink: ${target}`); + fs.unlinkSync(pythonSymlink); } } - } - } catch (error) { - // Symlink doesn't exist, create it if this is the main python symlink - if (error.code === 'ENOENT' && prebuiltPython && pythonName === 'python') { - try { - const relativePath = path.relative(binDir, prebuiltPython); - fs.symlinkSync(relativePath, pythonSymlink); - console.log(`Created missing ${venvName} ${pythonName} symlink -> ${relativePath}`); - } catch (createError) { - console.warn(`Warning: Could not create ${venvName} ${pythonName} symlink: ${createError.message}`); - } + } catch (error) { + console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`); } } } } - // Fix Python symlinks in both venv directories - fixPythonSymlinks(path.join(prebuiltPath, 'venv', 'bin'), 'venv'); - fixPythonSymlinks(path.join(prebuiltPath, 'terminal_venv', 'bin'), 'terminal_venv'); + // Clean Python symlinks in terminal_venv/bin (same as venv/bin) + const terminalVenvBinDir = path.join(prebuiltPath, 'terminal_venv', 'bin'); + if (fs.existsSync(terminalVenvBinDir)) { + const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12']; + const bundlePath = path.resolve(appPath); - // Recursively clean other invalid symlinks (skip already-processed venv bin directories) - const processedDirs = new Set([ - path.join(prebuiltPath, 'venv', 'bin'), - path.join(prebuiltPath, 'terminal_venv', 'bin'), - ]); + for (const pythonName of pythonNames) { + const pythonSymlink = path.join(terminalVenvBinDir, pythonName); + if (fs.existsSync(pythonSymlink)) { + try { + const stats = fs.lstatSync(pythonSymlink); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(pythonSymlink); + const resolvedPath = path.resolve(path.dirname(pythonSymlink), target); + + // If symlink points outside bundle, remove it + if (!resolvedPath.startsWith(bundlePath)) { + console.log(`Removing invalid terminal_venv ${pythonName} symlink: ${target}`); + fs.unlinkSync(pythonSymlink); + } + } + } catch (error) { + console.warn(`Warning: Could not process terminal_venv ${pythonName} symlink: ${error.message}`); + } + } + } + } + + // Recursively clean other invalid symlinks function cleanSymlinks(dir, bundleRoot) { - if (!fs.existsSync(dir) || processedDirs.has(dir)) { + if (!fs.existsSync(dir)) { return; } diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 6518730f7..4382a4654 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -11,7 +11,7 @@ import { getTerminalVenvPath, getPrebuiltTerminalVenvPath, getUvEnv, - findPrebuiltPythonExecutable, + getPrebuiltPythonDir, cleanupOldVenvs, isBinaryExists, runInstallScript, @@ -507,6 +507,59 @@ const runInstall = (extraArgs: string[], version: string) => { }); }; +/** + * Find Python executable in prebuilt Python directory + * UV stores Python installations in directories like: cpython-3.10.19+.../install/bin/python + */ +function findPrebuiltPythonExecutable(): string | null { + const prebuiltPythonDir = getPrebuiltPythonDir(); + if (!prebuiltPythonDir) { + return null; + } + + // Look for Python executable in the prebuilt directory + // UV stores Python in subdirectories like: cpython-3.10.19+.../install/bin/python + const possiblePaths: string[] = []; + + // First, try common direct paths + possiblePaths.push( + path.join(prebuiltPythonDir, 'install', 'bin', 'python'), + path.join(prebuiltPythonDir, 'install', 'python.exe'), + path.join(prebuiltPythonDir, 'bin', 'python'), + path.join(prebuiltPythonDir, 'python.exe'), + ); + + // Then, search in subdirectories (UV stores Python in versioned directories) + try { + if (fs.existsSync(prebuiltPythonDir)) { + const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('cpython-')) { + const subDir = path.join(prebuiltPythonDir, entry.name); + possiblePaths.push( + path.join(subDir, 'install', 'bin', 'python'), + path.join(subDir, 'install', 'python.exe'), + path.join(subDir, 'bin', 'python'), + path.join(subDir, 'python.exe'), + ); + } + } + } + } catch (error) { + log.warn('[DEPS INSTALL] Error searching for prebuilt Python:', error); + } + + for (const pythonPath of possiblePaths) { + if (fs.existsSync(pythonPath)) { + log.info(`[DEPS INSTALL] Found prebuilt Python executable: ${pythonPath}`); + return pythonPath; + } + } + + log.info('[DEPS INSTALL] Prebuilt Python directory found but executable not found, will use UV_PYTHON_INSTALL_DIR'); + return null; +} + /** * Install terminal base venv with common packages for terminal tasks. * This is a lightweight venv separate from the backend venv. diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index aa49999ea..6c8329331 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -170,29 +170,39 @@ export function getPrebuiltVenvPath(): string | null { } /** - * Find Python executable in prebuilt Python directory + * Find Python executable in prebuilt Python directory for terminal venv */ -export function findPrebuiltPythonExecutable(): string | null { +function findPythonForTerminalVenv(): string | null { const prebuiltPythonDir = getPrebuiltPythonDir(); if (!prebuiltPythonDir) { return null; } - const isWindows = process.platform === 'win32'; - const pythonName = isWindows ? 'python.exe' : 'python'; - const binPath = isWindows ? '' : path.join('install', 'bin'); + // Look for Python executable in the prebuilt directory + // UV stores Python in subdirectories like: cpython-3.10.19+.../install/bin/python + const possiblePaths: string[] = []; - // UV stores Python in cpython-* subdirectories + // First, try common direct paths + possiblePaths.push( + path.join(prebuiltPythonDir, 'install', 'bin', 'python'), + path.join(prebuiltPythonDir, 'install', 'python.exe'), + path.join(prebuiltPythonDir, 'bin', 'python'), + path.join(prebuiltPythonDir, 'python.exe'), + ); + + // Then, search in subdirectories (UV stores Python in versioned directories) try { - const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name.startsWith('cpython-')) { - const pythonPath = isWindows - ? path.join(prebuiltPythonDir, entry.name, 'install', pythonName) - : path.join(prebuiltPythonDir, entry.name, binPath, pythonName); - if (fs.existsSync(pythonPath)) { - log.info(`[PROCESS] Found prebuilt Python executable: ${pythonPath}`); - return pythonPath; + if (fs.existsSync(prebuiltPythonDir)) { + const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('cpython-')) { + const subDir = path.join(prebuiltPythonDir, entry.name); + possiblePaths.push( + path.join(subDir, 'install', 'bin', 'python'), + path.join(subDir, 'install', 'python.exe'), + path.join(subDir, 'bin', 'python'), + path.join(subDir, 'python.exe'), + ); } } } @@ -200,7 +210,12 @@ export function findPrebuiltPythonExecutable(): string | null { log.warn('[PROCESS] Error searching for prebuilt Python:', error); } - log.info('[PROCESS] Prebuilt Python directory found but executable not found'); + for (const pythonPath of possiblePaths) { + if (fs.existsSync(pythonPath)) { + return pythonPath; + } + } + return null; } @@ -226,13 +241,13 @@ export function getPrebuiltTerminalVenvPath(): string | null { log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`); return prebuiltTerminalVenvPath; } else { - // Try to fix the missing Python executable by creating a symlink to - // prebuilt Python + // 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...` ); - const prebuiltPython = findPrebuiltPythonExecutable(); + + const prebuiltPython = findPythonForTerminalVenv(); if (prebuiltPython && fs.existsSync(prebuiltPython)) { try { const binDir = isWindows diff --git a/test/mocks/environmentMocks.ts b/test/mocks/environmentMocks.ts index 78079709b..51782df53 100644 --- a/test/mocks/environmentMocks.ts +++ b/test/mocks/environmentMocks.ts @@ -426,14 +426,10 @@ export function createProcessUtilsMock() { getBinaryPath: vi.fn(), getCachePath: vi.fn(), getVenvPath: vi.fn(), - getTerminalVenvPath: vi.fn(), - getPrebuiltTerminalVenvPath: vi.fn(), - findPrebuiltPythonExecutable: vi.fn(), getVenvsBaseDir: vi.fn(), cleanupOldVenvs: vi.fn(), isBinaryExists: vi.fn(), getUvEnv: vi.fn(), - TERMINAL_BASE_PACKAGES: ['pandas', 'numpy', 'matplotlib', 'requests', 'openpyxl', 'beautifulsoup4', 'pillow'], mockState: {} as MockEnvironmentState, setup: (mockState: MockEnvironmentState) => { @@ -486,14 +482,6 @@ export function createProcessUtilsMock() { utilsMock.getVenvPath.mockImplementation((version: string) => { return `${mockState.system.homedir}/.eigent/venvs/backend-${version}` }) - - utilsMock.getTerminalVenvPath.mockImplementation((version: string) => { - return `${mockState.system.homedir}/.eigent/venvs/terminal-${version}` - }) - - utilsMock.getPrebuiltTerminalVenvPath.mockReturnValue(null) - - utilsMock.findPrebuiltPythonExecutable.mockReturnValue(null) utilsMock.getVenvsBaseDir.mockReturnValue( `${mockState.system.homedir}/.eigent/venvs`