From 25476488c6ed270e59bacc4f7240b101db39d2a6 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Wed, 25 Feb 2026 15:34:31 +0800 Subject: [PATCH 1/2] feat(electron): enhance uv binary resolution and logging - Introduced a new function to resolve the best available uv binary, prioritizing the bundled version in production. - Improved logging for uv binary availability checks and Python installation processes. - Refactored the check for uv installation to streamline the process and provide clearer error messages. - Updated the setupManagedPython function to handle installation attempts with and without a mirror environment. --- electron/utils/uv-setup.ts | 193 +++++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 61 deletions(-) diff --git a/electron/utils/uv-setup.ts b/electron/utils/uv-setup.ts index fb349b3f1..e3747afc3 100644 --- a/electron/utils/uv-setup.ts +++ b/electron/utils/uv-setup.ts @@ -3,6 +3,7 @@ import { spawn } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { getUvMirrorEnv } from './uv-env'; +import { logger } from './logger'; /** * Get the path to the bundled uv binary @@ -14,31 +15,60 @@ function getBundledUvPath(): string { const binName = platform === 'win32' ? 'uv.exe' : 'uv'; if (app.isPackaged) { - // In production, we flattened the structure to 'bin/' return join(process.resourcesPath, 'bin', binName); } else { - // In dev, resources are at project root/resources/bin/- return join(process.cwd(), 'resources', 'bin', target, binName); } } /** - * Check if uv is available (either in system PATH or bundled) + * Resolve the best uv binary to use. + * + * In packaged mode we always prefer the bundled binary so we never accidentally + * pick up a system-wide uv that may be a different (possibly broken) version. + * In dev we fall through to the system PATH for convenience. + */ +function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fallback' } { + const bundled = getBundledUvPath(); + + if (app.isPackaged) { + if (existsSync(bundled)) { + return { bin: bundled, source: 'bundled' }; + } + logger.warn(`Bundled uv binary not found at ${bundled}, falling back to system PATH`); + } + + // Dev mode or missing bundled binary — check system PATH + const found = findUvInPathSync(); + if (found) return { bin: 'uv', source: 'path' }; + + if (existsSync(bundled)) { + return { bin: bundled, source: 'bundled-fallback' }; + } + + return { bin: 'uv', source: 'path' }; +} + +function findUvInPathSync(): boolean { + const { execSync } = require('child_process') as typeof import('child_process'); + try { + const cmd = process.platform === 'win32' ? 'where.exe uv' : 'which uv'; + execSync(cmd, { stdio: 'ignore', timeout: 5000 }); + return true; + } catch { + return false; + } +} + +/** + * Check if uv is available (either bundled or in system PATH) */ export async function checkUvInstalled(): Promise { - // 1. Check system PATH first - const inPath = await new Promise((resolve) => { - const cmd = process.platform === 'win32' ? 'where.exe' : 'which'; - const child = spawn(cmd, ['uv']); - child.on('close', (code) => resolve(code === 0)); - child.on('error', () => resolve(false)); - }); - - if (inPath) return true; - - // 2. Check bundled path - const bin = getBundledUvPath(); - return existsSync(bin); + const { bin, source } = resolveUvBin(); + if (source === 'bundled' || source === 'bundled-fallback') { + return existsSync(bin); + } + return findUvInPathSync(); } /** @@ -51,22 +81,14 @@ export async function installUv(): Promise { const bin = getBundledUvPath(); throw new Error(`uv not found in system PATH and bundled binary missing at ${bin}`); } - console.log('uv is available and ready to use'); + logger.info('uv is available and ready to use'); } /** * Check if a managed Python 3.12 is ready and accessible */ export async function isPythonReady(): Promise { - // Use 'uv' if in PATH, otherwise use full bundled path - const inPath = await new Promise((resolve) => { - const cmd = process.platform === 'win32' ? 'where.exe' : 'which'; - const child = spawn(cmd, ['uv']); - child.on('close', (code) => resolve(code === 0)); - child.on('error', () => resolve(false)); - }); - - const uvBin = inPath ? 'uv' : getBundledUvPath(); + const { bin: uvBin } = resolveUvBin(); return new Promise((resolve) => { try { @@ -82,58 +104,109 @@ export async function isPythonReady(): Promise { } /** - * Use bundled uv to install a managed Python version (default 3.12) - * Automatically picks the best available uv binary + * Run `uv python install 3.12` once with the given environment. + * Returns on success, throws with captured stderr on failure. */ -export async function setupManagedPython(): Promise { - // Use 'uv' if in PATH, otherwise use full bundled path - const inPath = await new Promise((resolve) => { - const cmd = process.platform === 'win32' ? 'where.exe' : 'which'; - const child = spawn(cmd, ['uv']); - child.on('close', (code) => resolve(code === 0)); - child.on('error', () => resolve(false)); - }); +async function runPythonInstall( + uvBin: string, + env: Record, + label: string, +): Promise { + return new Promise((resolve, reject) => { + const stderrChunks: string[] = []; + const stdoutChunks: string[] = []; - const uvBin = inPath ? 'uv' : getBundledUvPath(); - - console.log(`Setting up python with: ${uvBin}`); - const uvEnv = await getUvMirrorEnv(); - - await new Promise((resolve, reject) => { const child = spawn(uvBin, ['python', 'install', '3.12'], { shell: process.platform === 'win32', - env: { - ...process.env, - ...uvEnv, - }, + env, }); child.stdout?.on('data', (data) => { - console.log(`python setup stdout: ${data}`); + const line = data.toString().trim(); + if (line) { + stdoutChunks.push(line); + logger.debug(`[python-setup:${label}] stdout: ${line}`); + } }); child.stderr?.on('data', (data) => { - // uv prints progress to stderr, so we log it as info - console.log(`python setup info: ${data.toString().trim()}`); + const line = data.toString().trim(); + if (line) { + stderrChunks.push(line); + logger.info(`[python-setup:${label}] stderr: ${line}`); + } }); child.on('close', (code) => { - if (code === 0) resolve(); - else reject(new Error(`Python installation failed with code ${code}`)); + if (code === 0) { + resolve(); + } else { + 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', (err) => reject(err)); + child.on('error', (err) => { + reject(new Error( + `Python installation spawn error [${label}]: ${err.message}\n` + + ` uv binary: ${uvBin}\n` + + ` platform: ${process.platform}/${process.arch}` + )); + }); }); +} - // After installation, find and print where the Python executable is +/** + * Use bundled uv to install a managed Python version (default 3.12). + * + * Tries with mirror env first (for CN region), then retries without mirror + * if the first attempt fails, to rule out mirror-specific issues. + */ +export async function setupManagedPython(): Promise { + const { bin: uvBin, source } = resolveUvBin(); + const uvEnv = await getUvMirrorEnv(); + const hasMirror = Object.keys(uvEnv).length > 0; + + logger.info( + `Setting up managed Python 3.12 ` + + `(uv=${uvBin}, source=${source}, arch=${process.arch}, mirror=${hasMirror})` + ); + + const baseEnv: Record = { ...process.env }; + + // Attempt 1: with mirror (if applicable) + try { + await runPythonInstall(uvBin, { ...baseEnv, ...uvEnv }, hasMirror ? 'mirror' : 'default'); + } catch (firstError) { + logger.warn('Python install attempt 1 failed:', firstError); + + if (hasMirror) { + // Attempt 2: retry without mirror to rule out mirror issues + logger.info('Retrying Python install without mirror...'); + try { + await runPythonInstall(uvBin, baseEnv, 'no-mirror'); + } catch (secondError) { + logger.error('Python install attempt 2 (no mirror) also failed:', secondError); + throw secondError; + } + } else { + throw firstError; + } + } + + // After installation, verify and log the Python path try { const findPath = await new Promise((resolve) => { const child = spawn(uvBin, ['python', 'find', '3.12'], { shell: process.platform === 'win32', - env: { - ...process.env, - ...uvEnv, - }, + env: { ...process.env, ...uvEnv }, }); let output = ''; child.stdout?.on('data', (data) => { output += data; }); @@ -141,11 +214,9 @@ export async function setupManagedPython(): Promise { }); if (findPath) { - console.log(`✅ Managed Python 3.12 path: ${findPath}`); - // Note: uv stores environments in a central cache, - // Individual skills will create their own venvs in ~/.cache/uv or similar. + logger.info(`Managed Python 3.12 installed at: ${findPath}`); } } catch (err) { - console.warn('Could not determine Python path:', err); + logger.warn('Could not determine Python path after install:', err); } } From 2dfcc4e60020ccac5823a3628f95fac3698f32e4 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Wed, 25 Feb 2026 16:07:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9C=80=E6=96=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=20(#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor Agent Co-authored-by: Haze --- electron-builder.yml | 3 +++ electron/utils/uv-setup.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 09b19e0d5..db570c1b0 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -126,6 +126,9 @@ nsis: # Linux Configuration linux: + extraResources: + - from: resources/bin/linux-${arch} + to: bin icon: resources/icons target: - target: AppImage diff --git a/electron/utils/uv-setup.ts b/electron/utils/uv-setup.ts index e3747afc3..b20cb3fd9 100644 --- a/electron/utils/uv-setup.ts +++ b/electron/utils/uv-setup.ts @@ -1,5 +1,5 @@ import { app } from 'electron'; -import { spawn } from 'child_process'; +import { execSync, spawn } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { getUvMirrorEnv } from './uv-env'; @@ -50,7 +50,6 @@ function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fa } function findUvInPathSync(): boolean { - const { execSync } = require('child_process') as typeof import('child_process'); try { const cmd = process.platform === 'win32' ? 'where.exe uv' : 'which uv'; execSync(cmd, { stdio: 'ignore', timeout: 5000 });