Merge branch 'fix/uv_installation_error' into main
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -126,6 +126,9 @@ nsis:
|
|||||||
|
|
||||||
# Linux Configuration
|
# Linux Configuration
|
||||||
linux:
|
linux:
|
||||||
|
extraResources:
|
||||||
|
- from: resources/bin/linux-${arch}
|
||||||
|
to: bin
|
||||||
icon: resources/icons
|
icon: resources/icons
|
||||||
target:
|
target:
|
||||||
- target: AppImage
|
- target: AppImage
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { spawn } from 'child_process';
|
import { execSync, spawn } from 'child_process';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getUvMirrorEnv } from './uv-env';
|
import { getUvMirrorEnv } from './uv-env';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the bundled uv binary
|
* Get the path to the bundled uv binary
|
||||||
@@ -14,31 +15,59 @@ function getBundledUvPath(): string {
|
|||||||
const binName = platform === 'win32' ? 'uv.exe' : 'uv';
|
const binName = platform === 'win32' ? 'uv.exe' : 'uv';
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
// In production, we flattened the structure to 'bin/'
|
|
||||||
return join(process.resourcesPath, 'bin', binName);
|
return join(process.resourcesPath, 'bin', binName);
|
||||||
} else {
|
} else {
|
||||||
// In dev, resources are at project root/resources/bin/<platform>-<arch>
|
|
||||||
return join(process.cwd(), 'resources', 'bin', target, binName);
|
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 {
|
||||||
|
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<boolean> {
|
export async function checkUvInstalled(): Promise<boolean> {
|
||||||
// 1. Check system PATH first
|
const { bin, source } = resolveUvBin();
|
||||||
const inPath = await new Promise<boolean>((resolve) => {
|
if (source === 'bundled' || source === 'bundled-fallback') {
|
||||||
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
|
return existsSync(bin);
|
||||||
const child = spawn(cmd, ['uv']);
|
}
|
||||||
child.on('close', (code) => resolve(code === 0));
|
return findUvInPathSync();
|
||||||
child.on('error', () => resolve(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (inPath) return true;
|
|
||||||
|
|
||||||
// 2. Check bundled path
|
|
||||||
const bin = getBundledUvPath();
|
|
||||||
return existsSync(bin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,22 +80,14 @@ export async function installUv(): Promise<void> {
|
|||||||
const bin = getBundledUvPath();
|
const bin = getBundledUvPath();
|
||||||
throw new Error(`uv not found in system PATH and bundled binary missing at ${bin}`);
|
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
|
* Check if a managed Python 3.12 is ready and accessible
|
||||||
*/
|
*/
|
||||||
export async function isPythonReady(): Promise<boolean> {
|
export async function isPythonReady(): Promise<boolean> {
|
||||||
// Use 'uv' if in PATH, otherwise use full bundled path
|
const { bin: uvBin } = resolveUvBin();
|
||||||
const inPath = await new Promise<boolean>((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();
|
|
||||||
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
try {
|
try {
|
||||||
@@ -82,58 +103,109 @@ export async function isPythonReady(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use bundled uv to install a managed Python version (default 3.12)
|
* Run `uv python install 3.12` once with the given environment.
|
||||||
* Automatically picks the best available uv binary
|
* Returns on success, throws with captured stderr on failure.
|
||||||
*/
|
*/
|
||||||
export async function setupManagedPython(): Promise<void> {
|
async function runPythonInstall(
|
||||||
// Use 'uv' if in PATH, otherwise use full bundled path
|
uvBin: string,
|
||||||
const inPath = await new Promise<boolean>((resolve) => {
|
env: Record<string, string | undefined>,
|
||||||
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
|
label: string,
|
||||||
const child = spawn(cmd, ['uv']);
|
): Promise<void> {
|
||||||
child.on('close', (code) => resolve(code === 0));
|
return new Promise<void>((resolve, reject) => {
|
||||||
child.on('error', () => resolve(false));
|
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<void>((resolve, reject) => {
|
|
||||||
const child = spawn(uvBin, ['python', 'install', '3.12'], {
|
const child = spawn(uvBin, ['python', 'install', '3.12'], {
|
||||||
shell: process.platform === 'win32',
|
shell: process.platform === 'win32',
|
||||||
env: {
|
env,
|
||||||
...process.env,
|
|
||||||
...uvEnv,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
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) => {
|
child.stderr?.on('data', (data) => {
|
||||||
// uv prints progress to stderr, so we log it as info
|
const line = data.toString().trim();
|
||||||
console.log(`python setup info: ${data.toString().trim()}`);
|
if (line) {
|
||||||
|
stderrChunks.push(line);
|
||||||
|
logger.info(`[python-setup:${label}] stderr: ${line}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
if (code === 0) resolve();
|
if (code === 0) {
|
||||||
else reject(new Error(`Python installation failed with code ${code}`));
|
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<void> {
|
||||||
|
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<string, string | undefined> = { ...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 {
|
try {
|
||||||
const findPath = await new Promise<string>((resolve) => {
|
const findPath = await new Promise<string>((resolve) => {
|
||||||
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
||||||
shell: process.platform === 'win32',
|
shell: process.platform === 'win32',
|
||||||
env: {
|
env: { ...process.env, ...uvEnv },
|
||||||
...process.env,
|
|
||||||
...uvEnv,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
let output = '';
|
let output = '';
|
||||||
child.stdout?.on('data', (data) => { output += data; });
|
child.stdout?.on('data', (data) => { output += data; });
|
||||||
@@ -141,11 +213,9 @@ export async function setupManagedPython(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (findPath) {
|
if (findPath) {
|
||||||
console.log(`✅ Managed Python 3.12 path: ${findPath}`);
|
logger.info(`Managed Python 3.12 installed at: ${findPath}`);
|
||||||
// Note: uv stores environments in a central cache,
|
|
||||||
// Individual skills will create their own venvs in ~/.cache/uv or similar.
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Could not determine Python path:', err);
|
logger.warn('Could not determine Python path after install:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user