|
30 | 30 | import subprocess |
31 | 31 | import sys |
32 | 32 | from collections import OrderedDict |
| 33 | +from collections.abc import Callable |
33 | 34 | from pathlib import Path |
34 | 35 | from typing import Any |
35 | 36 |
|
|
41 | 42 | def _uv_bin(uv_path: str = "") -> list[str]: |
42 | 43 | """Resolve uv executable as a command list (safe for paths with spaces).""" |
43 | 44 | if uv_path: |
| 45 | + # shlex.split treats backslashes as escapes, which breaks Windows paths. |
| 46 | + # Use shlex only for posix-style paths (forward slashes or quoted). |
| 47 | + if "\\" in uv_path and not uv_path.startswith('"'): |
| 48 | + return [uv_path] |
44 | 49 | return shlex.split(uv_path) |
45 | 50 | path = shutil.which("uv") |
46 | 51 | if path: |
@@ -290,23 +295,56 @@ def _flatten_install(target_path: Path) -> None: |
290 | 295 | _cleanup_uv_artifacts(target_path) |
291 | 296 |
|
292 | 297 |
|
293 | | -def install(request: str, target: str, uv_path: str = "", *, flatten: bool = False) -> Path: |
| 298 | +def install( |
| 299 | + request: str, |
| 300 | + target: str, |
| 301 | + uv_path: str = "", |
| 302 | + *, |
| 303 | + flatten: bool = False, |
| 304 | + on_output: Callable[[str], None] | None = None, |
| 305 | +) -> Path: |
294 | 306 | """Install Python via uv python install --install-dir.""" |
295 | 307 | target_path = Path(target).resolve() |
296 | 308 | target_path.mkdir(parents=True, exist_ok=True) |
297 | | - print(f"[INFO] Installing {request} to {target_path} ...") |
| 309 | + if on_output: |
| 310 | + on_output(f"Installing {request} to {target_path} ...") |
298 | 311 | cmd = _uv_bin(uv_path) + [ |
299 | 312 | "python", |
300 | 313 | "install", |
301 | 314 | request, |
302 | 315 | "--install-dir", |
303 | 316 | str(target_path), |
| 317 | + "--no-bin", |
304 | 318 | ] |
305 | | - result = subprocess.run(cmd) # noqa: S603 |
306 | | - if result.returncode != 0: |
307 | | - raise RuntimeError( |
308 | | - f"uv python install failed with return code {result.returncode}" |
309 | | - ) |
| 319 | + if on_output: |
| 320 | + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) # noqa: S603 |
| 321 | + assert proc.stdout is not None |
| 322 | + buf = "" |
| 323 | + while True: |
| 324 | + ch = proc.stdout.read(1) |
| 325 | + if not ch: |
| 326 | + break |
| 327 | + if ch in ("\r", "\n"): |
| 328 | + line = buf.strip() |
| 329 | + if line: |
| 330 | + on_output(line) |
| 331 | + buf = "" |
| 332 | + else: |
| 333 | + buf += ch |
| 334 | + if buf.strip(): |
| 335 | + on_output(buf.strip()) |
| 336 | + proc.wait() |
| 337 | + if proc.returncode != 0: |
| 338 | + raise RuntimeError( |
| 339 | + f"uv python install failed with return code {proc.returncode}" |
| 340 | + ) |
| 341 | + else: |
| 342 | + print(f"[INFO] Installing {request} to {target_path} ...") |
| 343 | + result = subprocess.run(cmd) # noqa: S603 |
| 344 | + if result.returncode != 0: |
| 345 | + raise RuntimeError( |
| 346 | + f"uv python install failed with return code {result.returncode}" |
| 347 | + ) |
310 | 348 |
|
311 | 349 | if flatten: |
312 | 350 | _flatten_install(target_path) |
|
0 commit comments