Fix Windows install when paths contain spaces or Python 3.14 is on PATH (#5201)

* fix(studio): use py.exe to detect supported Python on Windows

  Description:
  The previous detection looked at `python --version` on PATH and
  hard-failed if the resolved Python wasn't 3.11-3.13. On systems
  where Python 3.14 sits ahead of 3.13 in PATH order, this aborted
  the installer even though a supported interpreter was installed.

  Prefer the py.exe launcher and probe `py -3.13`, `py -3.12`,
  `py -3.11` in turn. Fall back to `python --version` only when py.exe
  is absent, and surface a clearer error when no supported version
  can be found via either path.

* Studio: consolidate Windows studio overlay into single Tauri-gated block

  Replace the in-file sentinel hotfix and the unconditional file-copy
  overlay with a single block gated on $TauriMode. Hash-compare makes
  re-runs no-ops, removing the sentinel-clobbering bug that occurred
  when the second copy path overwrote the marker without re-adding it.

  Non-Tauri --local installs no longer need a copy overlay: the
  editable install above (uv pip install -e $RepoRoot --no-deps) makes
  _PACKAGE_ROOT in unsloth_cli/commands/studio.py resolve to the repo
  source tree via PEP 660 __file__-relative resolution, so
  `unsloth studio setup` finds the local setup.ps1 and
  install_python_stack.py without any file copying.

  Plain PyPI installs invoked from a checked-out repo directory are
  also no longer silently overlaid from cwd.

* fix(studio): work around uv space-in-path truncation on Windows

  uv 0.11.x truncates `-c <path>` and `-r <path>` arguments at the
  first space, breaking installs on Windows when the venv or repo
  sits under a path containing spaces (e.g. C:\Users\First Last\...).

  Pass paths through GetShortPathNameW to convert to 8.3 short form
  before handing them to uv. Plain pip is unaffected and keeps the
  original long path. No-op on Linux/Mac (gated on IS_WINDOWS and
  on the path actually containing a space).

* Refactor Python stack overlay logic in install.ps1

Refactor overlay logic for Python stack installation and improve handling of missing target directories.

* Update Python installation logic in setup.ps1
This commit is contained in:
Etherll 2026-04-28 11:10:47 +03:00 committed by GitHub
commit daf0889804
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 45 deletions

View file

@ -359,6 +359,27 @@ def _ensure_rocm_torch() -> None:
)
def _uv_safe_path(path: object) -> str:
# uv 0.11.x: `-c <path with space>` truncates at the space; use 8.3 short form.
s = str(path)
if not IS_WINDOWS or " " not in s:
return s
try:
import ctypes
from ctypes import wintypes
get_short = ctypes.windll.kernel32.GetShortPathNameW
get_short.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
get_short.restype = wintypes.DWORD
buf = ctypes.create_unicode_buffer(32768)
rc = get_short(s, buf, 32768)
if 0 < rc < 32768 and " " not in buf.value:
return buf.value
except Exception:
pass
return s
def _windows_hidden_subprocess_kwargs() -> dict[str, object]:
"""Return Windows-only subprocess kwargs that suppress console windows."""
if not IS_WINDOWS:
@ -751,14 +772,16 @@ def pip_install_try(
"""Like pip_install but returns False on failure instead of exiting.
For optional installs with a follow-up fallback.
"""
constraint_args: list[str] = []
constraint_args_pip: list[str] = []
constraint_args_uv: list[str] = []
if constrain and CONSTRAINTS.is_file():
constraint_args = ["-c", str(CONSTRAINTS)]
constraint_args_pip = ["-c", str(CONSTRAINTS)]
constraint_args_uv = ["-c", _uv_safe_path(CONSTRAINTS)]
if USE_UV:
cmd = _build_uv_cmd(args) + constraint_args
cmd = _build_uv_cmd(args) + constraint_args_uv
else:
cmd = _build_pip_cmd(args) + constraint_args
cmd = _build_pip_cmd(args) + constraint_args_pip
if VERBOSE:
_step(_LABEL, f"{label}...", _dim)
@ -781,9 +804,11 @@ def pip_install(
constrain: bool = True,
) -> None:
"""Build and run a pip install command (uses uv when available, falls back to pip)."""
constraint_args: list[str] = []
constraint_args_pip: list[str] = []
constraint_args_uv: list[str] = []
if constrain and CONSTRAINTS.is_file():
constraint_args = ["-c", str(CONSTRAINTS)]
constraint_args_pip = ["-c", str(CONSTRAINTS)]
constraint_args_uv = ["-c", _uv_safe_path(CONSTRAINTS)]
actual_req = req
temp_reqs: list[Path] = []
@ -793,13 +818,15 @@ def pip_install(
if actual_req is not None and NO_TORCH and NO_TORCH_SKIP_PACKAGES:
actual_req = _filter_requirements(actual_req, NO_TORCH_SKIP_PACKAGES)
temp_reqs.append(actual_req)
req_args: list[str] = []
req_args_pip: list[str] = []
req_args_uv: list[str] = []
if actual_req is not None:
req_args = ["-r", str(actual_req)]
req_args_pip = ["-r", str(actual_req)]
req_args_uv = ["-r", _uv_safe_path(actual_req)]
try:
if USE_UV:
uv_cmd = _build_uv_cmd(args) + constraint_args + req_args
uv_cmd = _build_uv_cmd(args) + constraint_args_uv + req_args_uv
if VERBOSE:
print(f" {label}...")
result = subprocess.run(
@ -814,7 +841,7 @@ def pip_install(
if result.stdout:
print(result.stdout.decode(errors = "replace"))
pip_cmd = _build_pip_cmd(args) + constraint_args + req_args
pip_cmd = _build_pip_cmd(args) + constraint_args_pip + req_args_pip
run(f"{label} (pip)" if USE_UV else label, pip_cmd)
finally:
for temp_req in temp_reqs: