@@ -211,11 +211,32 @@ def _is_python_executable_name(path: str) -> bool:
211211 )
212212
213213
214+ def _is_macos_qgis_app_bundle_python (path : str ) -> bool :
215+ """Return True for unsafe Python launchers in QGIS.app/Contents/MacOS."""
216+ if not (platform .system () == "Darwin" or sys .platform == "darwin" ):
217+ return False
218+ parts = os .path .abspath (path ).split (os .sep )
219+ for idx , part in enumerate (parts ):
220+ lower = part .lower ()
221+ if not (lower .startswith ("qgis" ) and lower .endswith (".app" )):
222+ continue
223+ if idx + 2 >= len (parts ):
224+ return False
225+ if parts [idx + 1 ].lower () != "contents" or parts [idx + 2 ].lower () != "macos" :
226+ return False
227+ name = os .path .basename (path ).lower ()
228+ return name .startswith ("qgis" ) or _is_python_executable_name (path )
229+ return False
230+
231+
214232def _python_candidate_matches_runtime (path : str ) -> bool :
215233 """Return True when a candidate is executable and matches QGIS Python."""
216234 if not path or not os .path .isfile (path ) or not _is_python_executable_name (path ):
217235 return False
218236
237+ if _is_macos_qgis_app_bundle_python (path ):
238+ return False
239+
219240 try :
220241 result = subprocess .run ( # nosec B603
221242 [
@@ -503,11 +524,24 @@ def create_venv(venv_dir: str) -> str:
503524 env = _get_clean_env ()
504525 kwargs = _get_subprocess_kwargs ()
505526
506- # Strategy 0: Use uv venv when available (fastest, no pip needed)
527+ python_exe = None
528+ python_lookup_error = ""
529+ try :
530+ python_exe = _find_python_executable ()
531+ except RuntimeError as exc :
532+ python_lookup_error = str (exc )
533+
534+ # Strategy 0: Use uv venv when available (fastest, no pip needed). On
535+ # official macOS QGIS app bundles, require uv-managed Python instead of
536+ # reusing QGIS's app-bundle Python wrapper for venv creation.
537+ uv_error = ""
507538 if uv_exists ():
508539 uv_path = get_uv_path ()
509- python_exe = _find_python_executable ()
510- cmd = [uv_path , "venv" , "--python" , python_exe , venv_dir ]
540+ uv_python = python_exe or f"{ sys .version_info .major } .{ sys .version_info .minor } "
541+ cmd = [uv_path , "venv" ]
542+ if python_exe is None :
543+ cmd .append ("--managed-python" )
544+ cmd += ["--python" , uv_python , venv_dir ]
511545 result = subprocess .run ( # nosec B603
512546 cmd ,
513547 capture_output = True ,
@@ -518,12 +552,17 @@ def create_venv(venv_dir: str) -> str:
518552 )
519553 if result .returncode == 0 and os .path .isfile (python_path ):
520554 return python_path
555+ uv_error = result .stderr or result .stdout or f"exit code { result .returncode } "
521556 # uv venv failed — clean up and fall through to pip strategies
522557 _cleanup_partial_venv (venv_dir )
523558
524559 # Strategy 1: Subprocess with the real Python executable
525- python_exe = _find_python_executable ()
526560 subprocess_error = ""
561+ if python_exe is None :
562+ message = python_lookup_error or "No usable Python executable was found."
563+ if uv_error :
564+ message += f"\n uv venv failed: { uv_error } "
565+ raise RuntimeError (message )
527566
528567 cmd = [python_exe , "-m" , "venv" , venv_dir ]
529568 result = subprocess .run ( # nosec B603
0 commit comments