@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
33import { existsSync } from 'fs' ;
44import { join } from 'path' ;
55import { getUvMirrorEnv } from './uv-env' ;
6+ import { logger } from './logger' ;
67
78/**
89 * Get the path to the bundled uv binary
@@ -14,31 +15,60 @@ 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+ const { execSync } = require ( 'child_process' ) as typeof import ( 'child_process' ) ;
54+ try {
55+ const cmd = process . platform === 'win32' ? 'where.exe uv' : 'which uv' ;
56+ execSync ( cmd , { stdio : 'ignore' , timeout : 5000 } ) ;
57+ return true ;
58+ } catch {
59+ return false ;
60+ }
61+ }
62+
63+ /**
64+ * Check if uv is available (either bundled or in system PATH)
65+ */
66+ export async function checkUvInstalled ( ) : Promise < boolean > {
67+ const { bin, source } = resolveUvBin ( ) ;
68+ if ( source === 'bundled' || source === 'bundled-fallback' ) {
69+ return existsSync ( bin ) ;
70+ }
71+ return findUvInPathSync ( ) ;
4272}
4373
4474/**
@@ -51,22 +81,14 @@ export async function installUv(): Promise<void> {
5181 const bin = getBundledUvPath ( ) ;
5282 throw new Error ( `uv not found in system PATH and bundled binary missing at ${ bin } ` ) ;
5383 }
54- console . log ( 'uv is available and ready to use' ) ;
84+ logger . info ( 'uv is available and ready to use' ) ;
5585}
5686
5787/**
5888 * Check if a managed Python 3.12 is ready and accessible
5989 */
6090export 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 ( ) ;
91+ const { bin : uvBin } = resolveUvBin ( ) ;
7092
7193 return new Promise < boolean > ( ( resolve ) => {
7294 try {
@@ -82,70 +104,119 @@ export async function isPythonReady(): Promise<boolean> {
82104}
83105
84106/**
85- * Use bundled uv to install a managed Python version (default 3.12)
86- * Automatically picks the best available uv binary
107+ * Run ` uv python install 3.12` once with the given environment.
108+ * Returns on success, throws with captured stderr on failure.
87109 */
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 ( ) ;
110+ async function runPythonInstall (
111+ uvBin : string ,
112+ env : Record < string , string | undefined > ,
113+ label : string ,
114+ ) : Promise < void > {
115+ return new Promise < void > ( ( resolve , reject ) => {
116+ const stderrChunks : string [ ] = [ ] ;
117+ const stdoutChunks : string [ ] = [ ] ;
101118
102- await new Promise < void > ( ( resolve , reject ) => {
103119 const child = spawn ( uvBin , [ 'python' , 'install' , '3.12' ] , {
104120 shell : process . platform === 'win32' ,
105- env : {
106- ...process . env ,
107- ...uvEnv ,
108- } ,
121+ env,
109122 } ) ;
110123
111124 child . stdout ?. on ( 'data' , ( data ) => {
112- console . log ( `python setup stdout: ${ data } ` ) ;
125+ const line = data . toString ( ) . trim ( ) ;
126+ if ( line ) {
127+ stdoutChunks . push ( line ) ;
128+ logger . debug ( `[python-setup:${ label } ] stdout: ${ line } ` ) ;
129+ }
113130 } ) ;
114131
115132 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 ( ) } ` ) ;
133+ const line = data . toString ( ) . trim ( ) ;
134+ if ( line ) {
135+ stderrChunks . push ( line ) ;
136+ logger . info ( `[python-setup:${ label } ] stderr: ${ line } ` ) ;
137+ }
118138 } ) ;
119139
120140 child . on ( 'close' , ( code ) => {
121- if ( code === 0 ) resolve ( ) ;
122- else reject ( new Error ( `Python installation failed with code ${ code } ` ) ) ;
141+ if ( code === 0 ) {
142+ resolve ( ) ;
143+ } else {
144+ const stderr = stderrChunks . join ( '\n' ) ;
145+ const stdout = stdoutChunks . join ( '\n' ) ;
146+ const detail = stderr || stdout || '(no output captured)' ;
147+ reject ( new Error (
148+ `Python installation failed with code ${ code } [${ label } ]\n` +
149+ ` uv binary: ${ uvBin } \n` +
150+ ` platform: ${ process . platform } /${ process . arch } \n` +
151+ ` output: ${ detail } `
152+ ) ) ;
153+ }
123154 } ) ;
124155
125- child . on ( 'error' , ( err ) => reject ( err ) ) ;
156+ child . on ( 'error' , ( err ) => {
157+ reject ( new Error (
158+ `Python installation spawn error [${ label } ]: ${ err . message } \n` +
159+ ` uv binary: ${ uvBin } \n` +
160+ ` platform: ${ process . platform } /${ process . arch } `
161+ ) ) ;
162+ } ) ;
126163 } ) ;
164+ }
165+
166+ /**
167+ * Use bundled uv to install a managed Python version (default 3.12).
168+ *
169+ * Tries with mirror env first (for CN region), then retries without mirror
170+ * if the first attempt fails, to rule out mirror-specific issues.
171+ */
172+ export async function setupManagedPython ( ) : Promise < void > {
173+ const { bin : uvBin , source } = resolveUvBin ( ) ;
174+ const uvEnv = await getUvMirrorEnv ( ) ;
175+ const hasMirror = Object . keys ( uvEnv ) . length > 0 ;
176+
177+ logger . info (
178+ `Setting up managed Python 3.12 ` +
179+ `(uv=${ uvBin } , source=${ source } , arch=${ process . arch } , mirror=${ hasMirror } )`
180+ ) ;
181+
182+ const baseEnv : Record < string , string | undefined > = { ...process . env } ;
183+
184+ // Attempt 1: with mirror (if applicable)
185+ try {
186+ await runPythonInstall ( uvBin , { ...baseEnv , ...uvEnv } , hasMirror ? 'mirror' : 'default' ) ;
187+ } catch ( firstError ) {
188+ logger . warn ( 'Python install attempt 1 failed:' , firstError ) ;
189+
190+ if ( hasMirror ) {
191+ // Attempt 2: retry without mirror to rule out mirror issues
192+ logger . info ( 'Retrying Python install without mirror...' ) ;
193+ try {
194+ await runPythonInstall ( uvBin , baseEnv , 'no-mirror' ) ;
195+ } catch ( secondError ) {
196+ logger . error ( 'Python install attempt 2 (no mirror) also failed:' , secondError ) ;
197+ throw secondError ;
198+ }
199+ } else {
200+ throw firstError ;
201+ }
202+ }
127203
128- // After installation, find and print where the Python executable is
204+ // After installation, verify and log the Python path
129205 try {
130206 const findPath = await new Promise < string > ( ( resolve ) => {
131207 const child = spawn ( uvBin , [ 'python' , 'find' , '3.12' ] , {
132208 shell : process . platform === 'win32' ,
133- env : {
134- ...process . env ,
135- ...uvEnv ,
136- } ,
209+ env : { ...process . env , ...uvEnv } ,
137210 } ) ;
138211 let output = '' ;
139212 child . stdout ?. on ( 'data' , ( data ) => { output += data ; } ) ;
140213 child . on ( 'close' , ( ) => resolve ( output . trim ( ) ) ) ;
141214 } ) ;
142215
143216 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.
217+ logger . info ( `Managed Python 3.12 installed at: ${ findPath } ` ) ;
147218 }
148219 } catch ( err ) {
149- console . warn ( 'Could not determine Python path:' , err ) ;
220+ logger . warn ( 'Could not determine Python path after install :' , err ) ;
150221 }
151222}
0 commit comments