|
5 | 5 | """Runtime installation helpers for platform-specific dependencies.""" |
6 | 6 |
|
7 | 7 | import importlib |
| 8 | +import importlib.machinery |
8 | 9 | import importlib.metadata |
9 | 10 | import importlib.util |
10 | 11 | import os |
11 | 12 | import shlex |
| 13 | +import shutil |
12 | 14 | import subprocess |
13 | 15 | import sys |
14 | 16 | import warnings |
@@ -149,12 +151,41 @@ def get_unsupported_platform_dependency_message( |
149 | 151 | return None |
150 | 152 |
|
151 | 153 |
|
| 154 | +def _pip_available(): |
| 155 | + """Find this interpreter's pip entry point without importing or executing it.""" |
| 156 | + try: |
| 157 | + spec = importlib.util.find_spec("pip") |
| 158 | + if spec is not None and spec.submodule_search_locations is not None: |
| 159 | + # Dotted util.find_spec would import pip. Search its package paths |
| 160 | + # directly instead, including namespace leftovers from uninstalling. |
| 161 | + spec = importlib.machinery.PathFinder.find_spec( |
| 162 | + "pip.__main__", spec.submodule_search_locations |
| 163 | + ) |
| 164 | + return ( |
| 165 | + spec is not None |
| 166 | + and spec.loader is not None |
| 167 | + and spec.submodule_search_locations is None |
| 168 | + ) |
| 169 | + except (ImportError, AttributeError, KeyError, TypeError, ValueError): |
| 170 | + return False |
| 171 | + |
| 172 | + |
152 | 173 | def format_install_command(requirements, executable="pip"): |
153 | | - """Return a shell-safe command for installing pip requirements.""" |
| 174 | + """Return shell-safe manual installation guidance for the target Python.""" |
154 | 175 | if executable == "pip": |
155 | 176 | command = [executable, "install", *requirements] |
156 | 177 | else: |
157 | 178 | command = [executable, "-m", "pip", "install", *requirements] |
| 179 | + if executable == sys.executable and not _pip_available(): |
| 180 | + if shutil.which("uv") is not None: |
| 181 | + command = [ |
| 182 | + "uv", |
| 183 | + "pip", |
| 184 | + "install", |
| 185 | + "--python", |
| 186 | + executable, |
| 187 | + *requirements, |
| 188 | + ] |
158 | 189 | return shlex.join(command) |
159 | 190 |
|
160 | 191 |
|
@@ -268,6 +299,27 @@ def install_missing_platform_dependencies( |
268 | 299 |
|
269 | 300 | def _install_platform_dependencies(detector, python_version, missing, optional): |
270 | 301 | """Install confirmed requirements and check same-process usability.""" |
| 302 | + if not _pip_available(): |
| 303 | + uv_command = shlex.join( |
| 304 | + ["uv", "pip", "install", "--python", sys.executable, *missing] |
| 305 | + ) |
| 306 | + message = ( |
| 307 | + "The current Python environment has no runnable pip entry point, so Blinka cannot " |
| 308 | + "install its platform dependencies automatically. Install them with " |
| 309 | + "your environment's package manager, or install pip into this " |
| 310 | + f"environment and retry. If using uv, run: {uv_command}\n" |
| 311 | + "Then restart Python before using Blinka." |
| 312 | + ) |
| 313 | + if set(missing).issubset(optional): |
| 314 | + warnings.warn( |
| 315 | + message |
| 316 | + + " Blinka startup will continue; optional features may be unavailable.", |
| 317 | + RuntimeWarning, |
| 318 | + stacklevel=2, |
| 319 | + ) |
| 320 | + return False |
| 321 | + raise RuntimeError(message) |
| 322 | + |
271 | 323 | # OS-provided modules can be importable without distribution metadata. |
272 | 324 | # Any loaded module being installed must be restarted, not just known old ones. |
273 | 325 | loaded = [ |
|
0 commit comments