diff --git a/electron/main/index.ts b/electron/main/index.ts index 3cf4facb2..ff299100d 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -25,6 +25,17 @@ import { shell, } from 'electron'; import log from 'electron-log'; + +// ==================== EPIPE error prevention ==================== +// During app shutdown, the stdout/stderr pipes may close before all log +// writes complete. This causes uncaught EPIPE errors that crash the app. +// Handle them gracefully by ignoring broken pipe writes. +for (const stream of [process.stdout, process.stderr]) { + stream?.on?.('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EPIPE') return; // Silently ignore broken pipe + }); +} + import FormData from 'form-data'; import fsp from 'fs/promises'; import mime from 'mime'; @@ -199,6 +210,25 @@ log.transports.file.level = 'info'; log.transports.console.format = '[{level}]{text}'; log.transports.file.format = '[{level}]{text}'; +// Catch any uncaught exceptions from broken pipes during shutdown +// to prevent the app from crashing with EPIPE errors +process.on('uncaughtException', (error: NodeJS.ErrnoException) => { + if (error.code === 'EPIPE') { + // Broken pipe during shutdown - safe to ignore. + // This happens when electron-log's console transport tries to write + // after the renderer process or stdout pipe has been closed. + return; + } + // For non-EPIPE errors, log to file (not console to avoid recursion) and re-throw + log.transports.file?.({ + data: [`[UNCAUGHT] ${error.stack || error.message}`], + level: 'error', + date: new Date(), + variables: {}, + } as any); + throw error; +}); + // Disable GPU Acceleration for Windows 7 if (os.release().startsWith('6.1')) app.disableHardwareAcceleration(); @@ -2234,6 +2264,9 @@ app.whenReady().then(async () => { app.on('window-all-closed', () => { log.info('window-all-closed'); + // Disable console transport - all windows are closed so stdout pipe may break + log.transports.console.level = false; + // Clean up WebView manager if (webViewManager) { webViewManager.destroy(); @@ -2271,6 +2304,12 @@ app.on('before-quit', async (event) => { // Prevent default quit to ensure cleanup completes event.preventDefault(); + // Disable console transport BEFORE destroying the window. + // Once the window/renderer is destroyed, stdout pipe may close, + // causing EPIPE errors on any subsequent console.info/log writes. + // File transport remains active for debugging shutdown issues. + log.transports.console.level = false; + try { // NOTE: Profile sync removed - we now use app userData directly for all partitions // No need to sync between different profile directories diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 62ad6a4e5..201c20679 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -406,6 +406,8 @@ class InstallLogs { '--no-dev', '--cache-dir', getCachePath('uv_cache'), + '--no-build-isolation-package', + 'llvmlite', ...extraArgs, ], { diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index 65bf24d51..028e4f1a7 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -631,12 +631,68 @@ export function getUvEnv(version: string): Record { const prebuiltPython = getPrebuiltPythonDir(); const pythonInstallDir = prebuiltPython || getCachePath('uv_python'); - return { + // Ensure PATH includes common tool directories (cmake, brew, etc.) + // Electron GUI apps on macOS don't inherit the full shell PATH + const currentPath = process.env.PATH || ''; + const extraPaths = [ + '/usr/local/bin', + '/opt/homebrew/bin', + '/usr/bin', + '/bin', + '/usr/sbin', + '/sbin', + ]; + + // Add LLVM directories so llvmlite can find the correct version during builds. + // Prefer LLVM 20 (required by llvmlite >=0.46) over older versions. + const llvmPaths = [ + '/usr/local/opt/llvm@20/bin', + '/opt/homebrew/opt/llvm@20/bin', + ]; + + const pathParts = currentPath.split(':'); + // Prepend LLVM paths so they take priority over older LLVM versions + const allExtraPaths = [...llvmPaths, ...extraPaths]; + const missingPaths = allExtraPaths.filter( + (p) => !pathParts.includes(p) && fs.existsSync(p) + ); + const enhancedPath = + missingPaths.length > 0 + ? [...missingPaths, ...pathParts].join(':') + : currentPath; + + // Set LLVM_CONFIG explicitly for llvmlite builds + const env: Record = { UV_PYTHON_INSTALL_DIR: pythonInstallDir, UV_TOOL_DIR: getCachePath('uv_tool'), UV_PROJECT_ENVIRONMENT: getVenvPath(version), UV_HTTP_TIMEOUT: '300', + PATH: enhancedPath, }; + + // Point llvmlite/cmake to the correct LLVM 20 installation. + // llvmlite >=0.46 requires LLVM 20. Without explicit LLVM_DIR, cmake may + // find an older LLVM version (e.g. 14) via system paths and fail the build. + // CMAKE_ARGS with -DLLVM_DIR forces cmake's find_package(LLVM) to use LLVM 20. + // LLVM_CONFIG provides the direct path for llvmlite's build.py. + const llvmPrefixes = [ + '/usr/local/opt/llvm@20', // Intel Mac (Homebrew) + '/opt/homebrew/opt/llvm@20', // Apple Silicon (Homebrew) + ]; + + for (const llvmPrefix of llvmPrefixes) { + const llvmCmakeDir = path.join(llvmPrefix, 'lib', 'cmake', 'llvm'); + if (fs.existsSync(llvmCmakeDir)) { + env.CMAKE_ARGS = `-DLLVM_DIR=${llvmCmakeDir}`; + const llvmConfigPath = path.join(llvmPrefix, 'bin', 'llvm-config'); + if (fs.existsSync(llvmConfigPath)) { + env.LLVM_CONFIG = llvmConfigPath; + } + break; + } + } + + return env; } export async function killProcessByName(name: string): Promise {