@@ -75,14 +75,11 @@ private static string GetSaveLocation()
7575 }
7676 else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
7777 {
78- string path = "/usr/local/bin" ;
79- return ! Directory . Exists ( path ) || ! IsDirectoryWritable ( path )
80- ? Path . Combine (
81- Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ,
82- "Applications" ,
83- RootFolder
84- )
85- : Path . Combine ( path , RootFolder ) ;
78+ // Use Application Support for a stable, user-writable location
79+ return Path . Combine (
80+ Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) ,
81+ "UnityMCP"
82+ ) ;
8683 }
8784 throw new Exception ( "Unsupported operating system." ) ;
8885 }
@@ -213,5 +210,189 @@ private static void CopyDirectoryRecursive(string sourceDir, string destinationD
213210 CopyDirectoryRecursive ( dirPath , destSubDir ) ;
214211 }
215212 }
213+
214+ public static bool RepairPythonEnvironment ( )
215+ {
216+ try
217+ {
218+ string serverSrc = GetServerPath ( ) ;
219+ bool hasServer = File . Exists ( Path . Combine ( serverSrc , "server.py" ) ) ;
220+ if ( ! hasServer )
221+ {
222+ // In dev mode or if not installed yet, try the embedded/dev source
223+ if ( TryGetEmbeddedServerSource ( out string embeddedSrc ) && File . Exists ( Path . Combine ( embeddedSrc , "server.py" ) ) )
224+ {
225+ serverSrc = embeddedSrc ;
226+ hasServer = true ;
227+ }
228+ else
229+ {
230+ // Attempt to install then retry
231+ EnsureServerInstalled ( ) ;
232+ serverSrc = GetServerPath ( ) ;
233+ hasServer = File . Exists ( Path . Combine ( serverSrc , "server.py" ) ) ;
234+ }
235+ }
236+
237+ if ( ! hasServer )
238+ {
239+ Debug . LogWarning ( "RepairPythonEnvironment: server.py not found; ensure server is installed first." ) ;
240+ return false ;
241+ }
242+
243+ // Remove stale venv and pinned version file if present
244+ string venvPath = Path . Combine ( serverSrc , ".venv" ) ;
245+ if ( Directory . Exists ( venvPath ) )
246+ {
247+ try { Directory . Delete ( venvPath , recursive : true ) ; } catch ( Exception ex ) { Debug . LogWarning ( $ "Failed to delete .venv: { ex . Message } ") ; }
248+ }
249+ string pyPin = Path . Combine ( serverSrc , ".python-version" ) ;
250+ if ( File . Exists ( pyPin ) )
251+ {
252+ try { File . Delete ( pyPin ) ; } catch ( Exception ex ) { Debug . LogWarning ( $ "Failed to delete .python-version: { ex . Message } ") ; }
253+ }
254+
255+ string uvPath = FindUvPath ( ) ;
256+ if ( uvPath == null )
257+ {
258+ Debug . LogError ( "UV not found. Please install uv (https://docs.astral.sh/uv/)." ) ;
259+ return false ;
260+ }
261+
262+ var psi = new System . Diagnostics . ProcessStartInfo
263+ {
264+ FileName = uvPath ,
265+ Arguments = "sync" ,
266+ WorkingDirectory = serverSrc ,
267+ UseShellExecute = false ,
268+ RedirectStandardOutput = true ,
269+ RedirectStandardError = true ,
270+ CreateNoWindow = true
271+ } ;
272+
273+ using var p = System . Diagnostics . Process . Start ( psi ) ;
274+ string stdout = p . StandardOutput . ReadToEnd ( ) ;
275+ string stderr = p . StandardError . ReadToEnd ( ) ;
276+ p . WaitForExit ( 60000 ) ;
277+
278+ if ( p . ExitCode != 0 )
279+ {
280+ Debug . LogError ( $ "uv sync failed: { stderr } \n { stdout } ") ;
281+ return false ;
282+ }
283+
284+ Debug . Log ( "Unity MCP: Python environment repaired successfully." ) ;
285+ return true ;
286+ }
287+ catch ( Exception ex )
288+ {
289+ Debug . LogError ( $ "RepairPythonEnvironment failed: { ex . Message } ") ;
290+ return false ;
291+ }
292+ }
293+
294+ private static string FindUvPath ( )
295+ {
296+ // Allow user override via EditorPrefs
297+ try
298+ {
299+ string overridePath = EditorPrefs . GetString ( "UnityMCP.UvPath" , string . Empty ) ;
300+ if ( ! string . IsNullOrEmpty ( overridePath ) && File . Exists ( overridePath ) )
301+ {
302+ if ( ValidateUvBinary ( overridePath ) ) return overridePath ;
303+ }
304+ }
305+ catch { }
306+
307+ string home = Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty ;
308+ string [ ] candidates =
309+ {
310+ "/opt/homebrew/bin/uv" ,
311+ "/usr/local/bin/uv" ,
312+ "/usr/bin/uv" ,
313+ "/opt/local/bin/uv" ,
314+ Path . Combine ( home , ".local" , "bin" , "uv" ) ,
315+ "/opt/homebrew/opt/uv/bin/uv" ,
316+ // Framework Python installs
317+ "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv" ,
318+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv" ,
319+ // Fallback to PATH resolution by name
320+ "uv"
321+ } ;
322+ foreach ( string c in candidates )
323+ {
324+ try
325+ {
326+ if ( ValidateUvBinary ( c ) ) return c ;
327+ }
328+ catch { /* ignore */ }
329+ }
330+
331+ // Try which uv (explicit path)
332+ try
333+ {
334+ var whichPsi = new System . Diagnostics . ProcessStartInfo
335+ {
336+ FileName = "/usr/bin/which" ,
337+ Arguments = "uv" ,
338+ UseShellExecute = false ,
339+ RedirectStandardOutput = true ,
340+ RedirectStandardError = true ,
341+ CreateNoWindow = true
342+ } ;
343+ using var wp = System . Diagnostics . Process . Start ( whichPsi ) ;
344+ string output = wp . StandardOutput . ReadToEnd ( ) . Trim ( ) ;
345+ wp . WaitForExit ( 3000 ) ;
346+ if ( wp . ExitCode == 0 && ! string . IsNullOrEmpty ( output ) && File . Exists ( output ) )
347+ {
348+ if ( ValidateUvBinary ( output ) ) return output ;
349+ }
350+ }
351+ catch { }
352+
353+ // Manual PATH scan
354+ try
355+ {
356+ string pathEnv = Environment . GetEnvironmentVariable ( "PATH" ) ?? string . Empty ;
357+ string [ ] parts = pathEnv . Split ( Path . PathSeparator ) ;
358+ foreach ( string part in parts )
359+ {
360+ try
361+ {
362+ string candidate = Path . Combine ( part , "uv" ) ;
363+ if ( File . Exists ( candidate ) && ValidateUvBinary ( candidate ) ) return candidate ;
364+ }
365+ catch { }
366+ }
367+ }
368+ catch { }
369+
370+ return null ;
371+ }
372+
373+ private static bool ValidateUvBinary ( string uvPath )
374+ {
375+ try
376+ {
377+ var psi = new System . Diagnostics . ProcessStartInfo
378+ {
379+ FileName = uvPath ,
380+ Arguments = "--version" ,
381+ UseShellExecute = false ,
382+ RedirectStandardOutput = true ,
383+ RedirectStandardError = true ,
384+ CreateNoWindow = true
385+ } ;
386+ using var p = System . Diagnostics . Process . Start ( psi ) ;
387+ if ( ! p . WaitForExit ( 5000 ) ) { try { p . Kill ( ) ; } catch { } return false ; }
388+ if ( p . ExitCode == 0 )
389+ {
390+ string output = p . StandardOutput . ReadToEnd ( ) . Trim ( ) ;
391+ return output . StartsWith ( "uv " ) ;
392+ }
393+ }
394+ catch { }
395+ return false ;
396+ }
216397 }
217398}
0 commit comments