import { execSync, spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { app } from 'electron'; import logManager from '@electron/service/logger'; import { getUvMirrorEnv } from './uv-env'; function getBundledUvPath(): string { const platform = process.platform; const arch = process.arch; const target = `${platform}-${arch}`; const binName = platform === 'win32' ? 'uv.exe' : 'uv'; if (app.isPackaged) { return join(process.resourcesPath, 'bin', binName); } return join(process.cwd(), 'resources', 'bin', target, binName); } function findUvInPathSync(): boolean { try { const command = process.platform === 'win32' ? 'where.exe uv' : 'which uv'; execSync(command, { stdio: 'ignore', timeout: 5000, windowsHide: true }); return true; } catch { return false; } } function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fallback' } { const bundled = getBundledUvPath(); if (app.isPackaged) { if (existsSync(bundled)) { return { bin: bundled, source: 'bundled' }; } logManager.warn(`Bundled uv binary not found at ${bundled}, falling back to system PATH`); } if (findUvInPathSync()) { return { bin: 'uv', source: 'path' }; } if (existsSync(bundled)) { return { bin: bundled, source: 'bundled-fallback' }; } return { bin: 'uv', source: 'path' }; } export async function checkUvInstalled(): Promise { const { bin, source } = resolveUvBin(); if (source === 'bundled' || source === 'bundled-fallback') { return existsSync(bin); } return findUvInPathSync(); } export async function installUv(): Promise { const isAvailable = await checkUvInstalled(); if (!isAvailable) { const bin = getBundledUvPath(); throw new Error(`uv not found in system PATH and bundled binary missing at ${bin}`); } logManager.info('uv is available and ready to use'); } export async function isPythonReady(): Promise { const { bin: uvBin } = resolveUvBin(); return await new Promise((resolve) => { try { const child = spawn(uvBin, ['python', 'find', '3.12'], { windowsHide: true, }); child.on('close', (code) => resolve(code === 0)); child.on('error', () => resolve(false)); } catch { resolve(false); } }); } async function runPythonInstall( uvBin: string, env: Record, label: string, ): Promise { return await new Promise((resolve, reject) => { const stderrChunks: string[] = []; const stdoutChunks: string[] = []; const child = spawn(uvBin, ['python', 'install', '3.12'], { env, windowsHide: true, }); child.stdout?.on('data', (data) => { const line = data.toString().trim(); if (line) { stdoutChunks.push(line); logManager.debug(`[python-setup:${label}] stdout: ${line}`); } }); child.stderr?.on('data', (data) => { const line = data.toString().trim(); if (line) { stderrChunks.push(line); logManager.info(`[python-setup:${label}] stderr: ${line}`); } }); child.on('close', (code) => { if (code === 0) { resolve(); return; } const stderr = stderrChunks.join('\n'); const stdout = stdoutChunks.join('\n'); const detail = stderr || stdout || '(no output captured)'; reject(new Error( `Python installation failed with code ${code} [${label}]\n` + ` uv binary: ${uvBin}\n` + ` platform: ${process.platform}/${process.arch}\n` + ` output: ${detail}`, )); }); child.on('error', (error) => { reject(new Error( `Python installation spawn error [${label}]: ${error.message}\n` + ` uv binary: ${uvBin}\n` + ` platform: ${process.platform}/${process.arch}`, )); }); }); } export async function setupManagedPython(): Promise { const { bin: uvBin, source } = resolveUvBin(); const uvEnv = await getUvMirrorEnv(); const hasMirror = Object.keys(uvEnv).length > 0; logManager.info( `Setting up managed Python 3.12 ` + `(uv=${uvBin}, source=${source}, arch=${process.arch}, mirror=${hasMirror})`, ); const baseEnv: Record = { ...process.env }; try { await runPythonInstall(uvBin, { ...baseEnv, ...uvEnv }, hasMirror ? 'mirror' : 'default'); } catch (firstError) { logManager.warn('Python install attempt 1 failed:', firstError); if (!hasMirror) { throw firstError; } logManager.info('Retrying Python install without mirror...'); try { await runPythonInstall(uvBin, baseEnv, 'no-mirror'); } catch (secondError) { logManager.error('Python install attempt 2 (no mirror) also failed:', secondError); throw secondError; } } try { const findPath = await new Promise((resolve) => { const child = spawn(uvBin, ['python', 'find', '3.12'], { env: { ...process.env, ...uvEnv }, windowsHide: true, }); let output = ''; child.stdout?.on('data', (data) => { output += data; }); child.on('close', () => resolve(output.trim())); child.on('error', () => resolve('')); }); if (findPath) { logManager.info(`Managed Python 3.12 installed at: ${findPath}`); } } catch (error) { logManager.warn('Could not determine Python path after install:', error); } }