Skip to content

Commit 1166074

Browse files
cursoragenthazeone
andcommitted
Merge branch 'fix/uv_installation_error' into main
Co-authored-by: Haze <hazeone@users.noreply.github.com>
2 parents 9bdc868 + 2dfcc4e commit 1166074

2 files changed

Lines changed: 134 additions & 61 deletions

File tree

electron-builder.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ nsis:
126126

127127
# Linux Configuration
128128
linux:
129+
extraResources:
130+
- from: resources/bin/linux-${arch}
131+
to: bin
129132
icon: resources/icons
130133
target:
131134
- target: AppImage

electron/utils/uv-setup.ts

Lines changed: 131 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { app } from 'electron';
2-
import { spawn } from 'child_process';
2+
import { execSync, spawn } from 'child_process';
33
import { existsSync } from 'fs';
44
import { join } from 'path';
55
import { getUvMirrorEnv } from './uv-env';
6+
import { logger } from './logger';
67

78
/**
89
* Get the path to the bundled uv binary
@@ -14,31 +15,59 @@ function getBundledUvPath(): string {
1415
const binName = platform === 'win32' ? 'uv.exe' : 'uv';
1516

1617
if (app.isPackaged) {
17-
// In production, we flattened the structure to 'bin/'
1818
return join(process.resourcesPath, 'bin', binName);
1919
} else {
20-
// In dev, resources are at project root/resources/bin/<platform>-<arch>
2120
return join(process.cwd(), 'resources', 'bin', target, binName);
2221
}
2322
}
2423

2524
/**
26-
* Check if uv is available (either in system PATH or bundled)
25+
* Resolve the best uv binary to use.
26+
*
27+
* In packaged mode we always prefer the bundled binary so we never accidentally
28+
* pick up a system-wide uv that may be a different (possibly broken) version.
29+
* In dev we fall through to the system PATH for convenience.
2730
*/
28-
export async function checkUvInstalled(): Promise<boolean> {
29-
// 1. Check system PATH first
30-
const inPath = await new Promise<boolean>((resolve) => {
31-
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
32-
const child = spawn(cmd, ['uv']);
33-
child.on('close', (code) => resolve(code === 0));
34-
child.on('error', () => resolve(false));
35-
});
31+
function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fallback' } {
32+
const bundled = getBundledUvPath();
33+
34+
if (app.isPackaged) {
35+
if (existsSync(bundled)) {
36+
return { bin: bundled, source: 'bundled' };
37+
}
38+
logger.warn(`Bundled uv binary not found at ${bundled}, falling back to system PATH`);
39+
}
40+
41+
// Dev mode or missing bundled binary — check system PATH
42+
const found = findUvInPathSync();
43+
if (found) return { bin: 'uv', source: 'path' };
3644

37-
if (inPath) return true;
45+
if (existsSync(bundled)) {
46+
return { bin: bundled, source: 'bundled-fallback' };
47+
}
3848

39-
// 2. Check bundled path
40-
const bin = getBundledUvPath();
41-
return existsSync(bin);
49+
return { bin: 'uv', source: 'path' };
50+
}
51+
52+
function findUvInPathSync(): boolean {
53+
try {
54+
const cmd = process.platform === 'win32' ? 'where.exe uv' : 'which uv';
55+
execSync(cmd, { stdio: 'ignore', timeout: 5000 });
56+
return true;
57+
} catch {
58+
return false;
59+
}
60+
}
61+
62+
/**
63+
* Check if uv is available (either bundled or in system PATH)
64+
*/
65+
export async function checkUvInstalled(): Promise<boolean> {
66+
const { bin, source } = resolveUvBin();
67+
if (source === 'bundled' || source === 'bundled-fallback') {
68+
return existsSync(bin);
69+
}
70+
return findUvInPathSync();
4271
}
4372

4473
/**
@@ -51,22 +80,14 @@ export async function installUv(): Promise<void> {
5180
const bin = getBundledUvPath();
5281
throw new Error(`uv not found in system PATH and bundled binary missing at ${bin}`);
5382
}
54-
console.log('uv is available and ready to use');
83+
logger.info('uv is available and ready to use');
5584
}
5685

5786
/**
5887
* Check if a managed Python 3.12 is ready and accessible
5988
*/
6089
export async function isPythonReady(): Promise<boolean> {
61-
// Use 'uv' if in PATH, otherwise use full bundled path
62-
const inPath = await new Promise<boolean>((resolve) => {
63-
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
64-
const child = spawn(cmd, ['uv']);
65-
child.on('close', (code) => resolve(code === 0));
66-
child.on('error', () => resolve(false));
67-
});
68-
69-
const uvBin = inPath ? 'uv' : getBundledUvPath();
90+
const { bin: uvBin } = resolveUvBin();
7091

7192
return new Promise<boolean>((resolve) => {
7293
try {
@@ -82,70 +103,119 @@ export async function isPythonReady(): Promise<boolean> {
82103
}
83104

84105
/**
85-
* Use bundled uv to install a managed Python version (default 3.12)
86-
* Automatically picks the best available uv binary
106+
* Run `uv python install 3.12` once with the given environment.
107+
* Returns on success, throws with captured stderr on failure.
87108
*/
88-
export async function setupManagedPython(): Promise<void> {
89-
// Use 'uv' if in PATH, otherwise use full bundled path
90-
const inPath = await new Promise<boolean>((resolve) => {
91-
const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
92-
const child = spawn(cmd, ['uv']);
93-
child.on('close', (code) => resolve(code === 0));
94-
child.on('error', () => resolve(false));
95-
});
96-
97-
const uvBin = inPath ? 'uv' : getBundledUvPath();
98-
99-
console.log(`Setting up python with: ${uvBin}`);
100-
const uvEnv = await getUvMirrorEnv();
109+
async function runPythonInstall(
110+
uvBin: string,
111+
env: Record<string, string | undefined>,
112+
label: string,
113+
): Promise<void> {
114+
return new Promise<void>((resolve, reject) => {
115+
const stderrChunks: string[] = [];
116+
const stdoutChunks: string[] = [];
101117

102-
await new Promise<void>((resolve, reject) => {
103118
const child = spawn(uvBin, ['python', 'install', '3.12'], {
104119
shell: process.platform === 'win32',
105-
env: {
106-
...process.env,
107-
...uvEnv,
108-
},
120+
env,
109121
});
110122

111123
child.stdout?.on('data', (data) => {
112-
console.log(`python setup stdout: ${data}`);
124+
const line = data.toString().trim();
125+
if (line) {
126+
stdoutChunks.push(line);
127+
logger.debug(`[python-setup:${label}] stdout: ${line}`);
128+
}
113129
});
114130

115131
child.stderr?.on('data', (data) => {
116-
// uv prints progress to stderr, so we log it as info
117-
console.log(`python setup info: ${data.toString().trim()}`);
132+
const line = data.toString().trim();
133+
if (line) {
134+
stderrChunks.push(line);
135+
logger.info(`[python-setup:${label}] stderr: ${line}`);
136+
}
118137
});
119138

120139
child.on('close', (code) => {
121-
if (code === 0) resolve();
122-
else reject(new Error(`Python installation failed with code ${code}`));
140+
if (code === 0) {
141+
resolve();
142+
} else {
143+
const stderr = stderrChunks.join('\n');
144+
const stdout = stdoutChunks.join('\n');
145+
const detail = stderr || stdout || '(no output captured)';
146+
reject(new Error(
147+
`Python installation failed with code ${code} [${label}]\n` +
148+
` uv binary: ${uvBin}\n` +
149+
` platform: ${process.platform}/${process.arch}\n` +
150+
` output: ${detail}`
151+
));
152+
}
123153
});
124154

125-
child.on('error', (err) => reject(err));
155+
child.on('error', (err) => {
156+
reject(new Error(
157+
`Python installation spawn error [${label}]: ${err.message}\n` +
158+
` uv binary: ${uvBin}\n` +
159+
` platform: ${process.platform}/${process.arch}`
160+
));
161+
});
126162
});
163+
}
164+
165+
/**
166+
* Use bundled uv to install a managed Python version (default 3.12).
167+
*
168+
* Tries with mirror env first (for CN region), then retries without mirror
169+
* if the first attempt fails, to rule out mirror-specific issues.
170+
*/
171+
export async function setupManagedPython(): Promise<void> {
172+
const { bin: uvBin, source } = resolveUvBin();
173+
const uvEnv = await getUvMirrorEnv();
174+
const hasMirror = Object.keys(uvEnv).length > 0;
175+
176+
logger.info(
177+
`Setting up managed Python 3.12 ` +
178+
`(uv=${uvBin}, source=${source}, arch=${process.arch}, mirror=${hasMirror})`
179+
);
180+
181+
const baseEnv: Record<string, string | undefined> = { ...process.env };
182+
183+
// Attempt 1: with mirror (if applicable)
184+
try {
185+
await runPythonInstall(uvBin, { ...baseEnv, ...uvEnv }, hasMirror ? 'mirror' : 'default');
186+
} catch (firstError) {
187+
logger.warn('Python install attempt 1 failed:', firstError);
188+
189+
if (hasMirror) {
190+
// Attempt 2: retry without mirror to rule out mirror issues
191+
logger.info('Retrying Python install without mirror...');
192+
try {
193+
await runPythonInstall(uvBin, baseEnv, 'no-mirror');
194+
} catch (secondError) {
195+
logger.error('Python install attempt 2 (no mirror) also failed:', secondError);
196+
throw secondError;
197+
}
198+
} else {
199+
throw firstError;
200+
}
201+
}
127202

128-
// After installation, find and print where the Python executable is
203+
// After installation, verify and log the Python path
129204
try {
130205
const findPath = await new Promise<string>((resolve) => {
131206
const child = spawn(uvBin, ['python', 'find', '3.12'], {
132207
shell: process.platform === 'win32',
133-
env: {
134-
...process.env,
135-
...uvEnv,
136-
},
208+
env: { ...process.env, ...uvEnv },
137209
});
138210
let output = '';
139211
child.stdout?.on('data', (data) => { output += data; });
140212
child.on('close', () => resolve(output.trim()));
141213
});
142214

143215
if (findPath) {
144-
console.log(`✅ Managed Python 3.12 path: ${findPath}`);
145-
// Note: uv stores environments in a central cache,
146-
// Individual skills will create their own venvs in ~/.cache/uv or similar.
216+
logger.info(`Managed Python 3.12 installed at: ${findPath}`);
147217
}
148218
} catch (err) {
149-
console.warn('Could not determine Python path:', err);
219+
logger.warn('Could not determine Python path after install:', err);
150220
}
151221
}

0 commit comments

Comments
 (0)