diff --git a/install.ps1 b/install.ps1 index cf7bb63cd..9abddc9ce 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1177,14 +1177,38 @@ shell.Run cmd, 0, False if ($SkipTorch) { $InitialGpuBranch = "no_torch" } Write-TauriDiag -GpuBranch $InitialGpuBranch -TorchIndexFamily "none" -PythonVersionForDiag $DiagPythonVersion - # ── Install uv if not present ── + # ── Install uv ── Write-TauriLog "STEP" "Installing uv package manager" - if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { - substep "installing uv package manager..." + $UvMinVersion = "0.7.22" + function Test-UvVersionOk { + $cmd = Get-Command uv -ErrorAction SilentlyContinue + if (-not $cmd) { return $false } + try { + $raw = (& uv --version 2>$null | Select-Object -First 1) + } catch { + return $false + } + if ($raw -notmatch 'uv\s+([0-9]+(?:\.[0-9]+)+)') { return $false } + try { + return ([version]$Matches[1] -ge [version]$UvMinVersion) + } catch { + return $false + } + } + + if (-not (Test-UvVersionOk)) { + if (Get-Command uv -ErrorAction SilentlyContinue) { + substep "updating uv package manager..." + } else { + substep "installing uv package manager..." + } if ($script:WingetAvailable) { $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" - try { winget install --id=astral-sh.uv -e --source winget --accept-package-agreements --accept-source-agreements } catch {} + try { winget upgrade --id=astral-sh.uv -e --source winget --accept-package-agreements --accept-source-agreements } catch {} + if (-not (Test-UvVersionOk)) { + try { winget install --id=astral-sh.uv -e --source winget --accept-package-agreements --accept-source-agreements } catch {} + } $ErrorActionPreference = $prevEAP Refresh-SessionPath } @@ -1192,19 +1216,40 @@ shell.Run cmd, 0, False # use Astral's official PowerShell installer. This is the only # supported path on hosts without winget (Windows ARM64 runners, # corporate machines without the Store, etc.). - if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + if (-not (Test-UvVersionOk)) { substep "installing uv via https://astral.sh/uv/install.ps1..." "Yellow" Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") Refresh-SessionPath } } - if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + # A freshly installed uv can sit later on PATH than an older one (active + # venv, Scoop/pipx shim). Prefer a just-installed uv from a known location. + if (-not (Test-UvVersionOk)) { + $origPath = $env:PATH + foreach ($d in @($env:UV_INSTALL_DIR, $env:XDG_BIN_HOME, + (Join-Path $env:USERPROFILE ".local\bin"), + (Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"))) { + if ($d -and (Test-Path $d)) { + $env:PATH = "$d;$origPath" + if (Test-UvVersionOk) { break } + $env:PATH = $origPath + } + } + } + + if (-not (Test-UvVersionOk)) { step "uv" "could not be installed" "Red" substep "Install it from https://docs.astral.sh/uv/" "Yellow" return (Exit-InstallFailure "uv could not be installed") } + # When bytecode compilation is enabled, large installs can exceed uv's 60s + # default on slow machines. Default to 180s, preserving overrides ("0" disables). + if (-not $env:UV_COMPILE_BYTECODE_TIMEOUT) { + $env:UV_COMPILE_BYTECODE_TIMEOUT = "180" + } + # ── Create venv (migrate old layout if possible, otherwise fresh) ── # Pass the resolved executable path to uv so it does not re-resolve # a version string back to a conda interpreter. diff --git a/install.sh b/install.sh index 532ac61bc..eba92d074 100755 --- a/install.sh +++ b/install.sh @@ -1456,7 +1456,11 @@ fi # ── Install uv ── tauri_log "STEP" "Installing uv package manager" -UV_MIN_VERSION="0.7.14" +UV_MIN_VERSION="0.7.22" + +# When bytecode compilation is enabled, large installs can exceed uv's 60s default on slow machines. Default to 180s, preserving overrides ("0" disables). +: "${UV_COMPILE_BYTECODE_TIMEOUT:=180}" +export UV_COMPILE_BYTECODE_TIMEOUT version_ge() { # returns 0 if $1 >= $2 diff --git a/tests/python/test_cross_platform_parity.py b/tests/python/test_cross_platform_parity.py index 0f2e73257..34f984714 100644 --- a/tests/python/test_cross_platform_parity.py +++ b/tests/python/test_cross_platform_parity.py @@ -20,7 +20,7 @@ class TestNoTorchBackendAutoInInstallSh: """ def test_no_torch_backend_auto_outside_fallback(self): - lines = INSTALL_SH.read_text().splitlines() + lines = INSTALL_SH.read_text(encoding = "utf-8").splitlines() # Fallback block: from "GPU detection failed" to the next "fi". fallback_start = None fallback_end = None @@ -48,7 +48,7 @@ class TestNoTorchBackendAutoInInstallSh: def test_fallback_uses_torch_backend_auto(self): """The fallback branch should use --torch-backend=auto as recovery.""" - text = INSTALL_SH.read_text() + text = INSTALL_SH.read_text(encoding = "utf-8") assert ( "GPU detection failed" in text ), "install.sh should have a fallback branch for when GPU detection fails" @@ -58,13 +58,13 @@ class TestInstallShHasGpuDetection: """install.sh must contain the get_torch_index_url function.""" def test_function_exists(self): - text = INSTALL_SH.read_text() + text = INSTALL_SH.read_text(encoding = "utf-8") assert ( "get_torch_index_url()" in text ), "install.sh is missing the get_torch_index_url() function" def test_torch_index_url_assigned(self): - text = INSTALL_SH.read_text() + text = INSTALL_SH.read_text(encoding = "utf-8") assert ( "TORCH_INDEX_URL=$(get_torch_index_url)" in text ), "install.sh should assign TORCH_INDEX_URL from get_torch_index_url()" @@ -115,8 +115,8 @@ class TestCudaMappingParity: def test_same_cuda_suffixes(self): """Both scripts should produce the same ordered list of CUDA index suffixes.""" - sh_text = INSTALL_SH.read_text() - ps1_text = INSTALL_PS1.read_text() + sh_text = INSTALL_SH.read_text(encoding = "utf-8") + ps1_text = INSTALL_PS1.read_text(encoding = "utf-8") sh_thresholds = self._extract_cuda_thresholds_sh(sh_text) ps1_thresholds = self._extract_cuda_thresholds_ps1(ps1_text) @@ -134,13 +134,53 @@ class TestPyTorchMirrorEnvVar: """Both install scripts must support the UNSLOTH_PYTORCH_MIRROR env var.""" def test_install_sh_has_mirror_var(self): - text = INSTALL_SH.read_text() + text = INSTALL_SH.read_text(encoding = "utf-8") assert ( "UNSLOTH_PYTORCH_MIRROR" in text ), "install.sh should reference UNSLOTH_PYTORCH_MIRROR" def test_install_ps1_has_mirror_var(self): - text = INSTALL_PS1.read_text() + text = INSTALL_PS1.read_text(encoding = "utf-8") assert ( "UNSLOTH_PYTORCH_MIRROR" in text ), "install.ps1 should reference UNSLOTH_PYTORCH_MIRROR" + + +class TestUvBytecodeCompileTimeout: + """Installers should relax uv bytecode compilation timeout by default.""" + + @staticmethod + def _version_tuple(version: str) -> tuple[int, ...]: + return tuple(int(part) for part in version.split(".")) + + def test_install_sh_uses_uv_version_with_timeout_env(self): + text = INSTALL_SH.read_text(encoding = "utf-8") + match = re.search(r'^UV_MIN_VERSION="([^"]+)"$', text, re.MULTILINE) + assert match, "install.sh should declare UV_MIN_VERSION" + assert self._version_tuple(match.group(1)) >= self._version_tuple("0.7.22") + + def test_install_ps1_uses_uv_version_with_timeout_env(self): + text = INSTALL_PS1.read_text(encoding = "utf-8") + match = re.search(r'^\s*\$UvMinVersion = "([^"]+)"$', text, re.MULTILINE) + assert match, "install.ps1 should declare $UvMinVersion" + assert self._version_tuple(match.group(1)) >= self._version_tuple("0.7.22") + assert "function Test-UvVersionOk" in text + assert "if (-not (Test-UvVersionOk))" in text + + def test_install_sh_preserves_timeout_override(self): + text = INSTALL_SH.read_text(encoding = "utf-8") + assert ( + ': "${UV_COMPILE_BYTECODE_TIMEOUT:=180}"' in text + ), "install.sh should default UV_COMPILE_BYTECODE_TIMEOUT without overwriting callers" + assert ( + "export UV_COMPILE_BYTECODE_TIMEOUT" in text + ), "install.sh should export UV_COMPILE_BYTECODE_TIMEOUT for uv subprocesses" + + def test_install_ps1_preserves_timeout_override(self): + text = INSTALL_PS1.read_text(encoding = "utf-8") + assert ( + "if (-not $env:UV_COMPILE_BYTECODE_TIMEOUT)" in text + ), "install.ps1 should preserve caller UV_COMPILE_BYTECODE_TIMEOUT overrides" + assert ( + '$env:UV_COMPILE_BYTECODE_TIMEOUT = "180"' in text + ), "install.ps1 should default UV_COMPILE_BYTECODE_TIMEOUT"