diff --git a/install.ps1 b/install.ps1 index 1073ab5726..367dbe12f6 100644 --- a/install.ps1 +++ b/install.ps1 @@ -482,6 +482,37 @@ function Install-UnslothStudio { } } + # Retry Invoke-InstallCommand on transient uv download failures with backoff. + # Returns the last exit code on permanent failure so rollback still fires. + function Invoke-InstallCommandRetry { + param( + [Parameter(Mandatory = $true, Position = 0)][ScriptBlock]$Command, + [string]$Label = "install step" + ) + # Sanitize overrides to a default of 3 (a typo must not disable retries; =1 disables). + # TryParse with bounds avoids an Int32 overflow throw. Bounds: 1..100 retries, 0..3600s. + $maxAttempts = 3 + $parsedAttempts = 0 + if ([int]::TryParse($env:UNSLOTH_INSTALL_RETRIES, [ref]$parsedAttempts) -and $parsedAttempts -ge 1 -and $parsedAttempts -le 100) { + $maxAttempts = $parsedAttempts + } + $delay = 3 + $parsedDelay = 0 + if ([int]::TryParse($env:UNSLOTH_INSTALL_RETRY_DELAY, [ref]$parsedDelay) -and $parsedDelay -ge 0 -and $parsedDelay -le 3600) { + $delay = $parsedDelay + } + $attempt = 1 + while ($true) { + $code = Invoke-InstallCommand $Command + if ($code -eq 0) { return 0 } + if ($attempt -ge $maxAttempts) { return $code } + substep ("retrying ""$Label"" after transient failure (attempt $($attempt + 1)/$maxAttempts, waiting ${delay}s)...") "Yellow" + Start-Sleep -Seconds $delay + $attempt++ + $delay = $delay * 2 + } + } + function New-StudioShortcuts { param( [Parameter(Mandatory = $true)][string]$UnslothExePath @@ -1181,7 +1212,7 @@ shell.Run cmd, 0, False # ── Install uv ── Write-TauriLog "STEP" "Installing uv package manager" - $UvMinVersion = "0.7.22" + $UvMinVersion = "0.8.16" function Test-UvVersionOk { $cmd = Get-Command uv -ErrorAction SilentlyContinue if (-not $cmd) { return $false } @@ -1252,6 +1283,15 @@ shell.Run cmd, 0, False $env:UV_COMPILE_BYTECODE_TIMEOUT = "180" } + # uv >= 0.8.16 retries HTTP/2 streaming body errors; raise retries and read + # timeout for large wheel downloads. User-provided values are preserved. + if (-not $env:UV_HTTP_RETRIES) { + $env:UV_HTTP_RETRIES = "5" + } + if (-not $env:UV_HTTP_TIMEOUT) { + $env:UV_HTTP_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. @@ -1930,21 +1970,21 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (migrated no-torch)" { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } if ($baseInstallExit -eq 0) { # Resolve pydantic WITH deps so pip pins pydantic-core # to the matching version (no-torch-runtime.txt below # is --no-deps). All transitive deps are torch-free. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install pydantic" { uv pip install --python $VenvPython pydantic } } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install no-torch runtime deps" { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } } } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (migrated)" { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red @@ -1958,7 +1998,7 @@ shell.Run cmd, 0, False return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } + $zooOverlayExit = Invoke-InstallCommandRetry -Label "overlay unsloth-zoo (git main)" { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } if ($zooOverlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) @@ -1971,7 +2011,7 @@ shell.Run cmd, 0, False Write-TauriLog "STEP" "Installing PyTorch (AMD ROCm Windows)" substep "installing PyTorch from $ROCmIndexUrl..." $torchSpec = if ($ROCmTorchFloor) { $ROCmTorchFloor } else { "torch" } - $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --force-reinstall --index-url $ROCmIndexUrl $torchSpec torchvision torchaudio } + $torchInstallExit = Invoke-InstallCommandRetry -Label "install PyTorch (AMD ROCm)" { uv pip install --python $VenvPython --force-reinstall --index-url $ROCmIndexUrl $torchSpec torchvision torchaudio } if ($torchInstallExit -ne 0) { Write-Host "[ERROR] Failed to install AMD ROCm PyTorch (exit code $torchInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install AMD ROCm PyTorch (exit code $torchInstallExit)" $torchInstallExit) @@ -1979,7 +2019,7 @@ shell.Run cmd, 0, False } else { Write-TauriLog "STEP" "Installing PyTorch" substep "installing PyTorch ($TorchIndexUrl)..." - $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } + $torchInstallExit = Invoke-InstallCommandRetry -Label "install PyTorch" { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } if ($torchInstallExit -ne 0) { Write-Host "[ERROR] Failed to install PyTorch (exit code $torchInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install PyTorch (exit code $torchInstallExit)" $torchInstallExit) @@ -1991,21 +2031,21 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (no-torch)" { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } if ($baseInstallExit -eq 0) { # Same pydantic-with-deps trick as the migrated branch. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install pydantic" { uv pip install --python $VenvPython pydantic } } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install no-torch runtime deps" { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } } } } elseif ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.6.7" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (local)" { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.6.7" unsloth-zoo } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth" { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red @@ -2020,7 +2060,7 @@ shell.Run cmd, 0, False return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } + $zooOverlayExit = Invoke-InstallCommandRetry -Label "overlay unsloth-zoo (git main)" { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } if ($zooOverlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) @@ -2031,7 +2071,7 @@ shell.Run cmd, 0, False Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.6.7" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (auto torch backend)" { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.6.7" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) @@ -2043,13 +2083,13 @@ shell.Run cmd, 0, False return (Exit-InstallFailure "Failed to overlay local repo (exit code $overlayExit)" $overlayExit) } substep "overlaying unsloth-zoo from git main..." - $zooOverlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } + $zooOverlayExit = Invoke-InstallCommandRetry -Label "overlay unsloth-zoo (git main)" { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth-zoo "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" } if ($zooOverlayExit -ne 0) { Write-Host "[ERROR] Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to overlay unsloth-zoo (exit code $zooOverlayExit)" $zooOverlayExit) } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } + $baseInstallExit = Invoke-InstallCommandRetry -Label "install unsloth (auto torch backend)" { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) diff --git a/install.sh b/install.sh index 5e08680628..fa0535a51b 100755 --- a/install.sh +++ b/install.sh @@ -163,6 +163,40 @@ run_install_cmd() { return $_rc } +# Retry run_install_cmd on transient uv download failures with backoff. Returns +# the last exit code on permanent failure so the set -e rollback trap still fires. +: "${UNSLOTH_INSTALL_RETRIES:=3}" +: "${UNSLOTH_INSTALL_RETRY_DELAY:=3}" +run_install_cmd_retry() { + _ricr_label="$1" + # Sanitize overrides to a default of 3 (a typo must not disable retries; =1 disables). + # Length guard precedes the numeric test so a huge value can't overflow `[ -ge ]`. + # 0?* rejects leading-zero delays ("08"/"09" break the later $((delay*2)) as octal); + # bare "0" stays valid. Bounds: 1..100 retries, 0..3600s base delay. + case "$UNSLOTH_INSTALL_RETRIES" in + ''|*[!0-9]*|0) _ricr_max=3 ;; + *) if [ "${#UNSLOTH_INSTALL_RETRIES}" -le 3 ] && [ "$UNSLOTH_INSTALL_RETRIES" -ge 1 ] 2>/dev/null && [ "$UNSLOTH_INSTALL_RETRIES" -le 100 ] 2>/dev/null; then _ricr_max=$UNSLOTH_INSTALL_RETRIES; else _ricr_max=3; fi ;; + esac + case "$UNSLOTH_INSTALL_RETRY_DELAY" in + ''|*[!0-9]*|0?*) _ricr_delay=3 ;; + *) if [ "${#UNSLOTH_INSTALL_RETRY_DELAY}" -le 4 ] && [ "$UNSLOTH_INSTALL_RETRY_DELAY" -ge 0 ] 2>/dev/null && [ "$UNSLOTH_INSTALL_RETRY_DELAY" -le 3600 ] 2>/dev/null; then _ricr_delay=$UNSLOTH_INSTALL_RETRY_DELAY; else _ricr_delay=3; fi ;; + esac + _ricr_attempt=1 + while :; do + # AND-OR (not `if`) preserves the real failure code: $? after a non-taken + # `if` is 0 in sh/dash/bash, which would break the rollback path. + run_install_cmd "$@" && return 0 + _ricr_rc=$? + if [ "$_ricr_attempt" -ge "$_ricr_max" ]; then + return "$_ricr_rc" + fi + substep "retrying \"$_ricr_label\" after transient failure (attempt $((_ricr_attempt + 1))/$_ricr_max, waiting ${_ricr_delay}s)..." "$C_WARN" + sleep "$_ricr_delay" || true + _ricr_attempt=$((_ricr_attempt + 1)) + _ricr_delay=$((_ricr_delay * 2)) + done +} + # Install bitsandbytes on AMD ROCm hosts. Uses the continuous-release_main # wheel for the ROCm 4-bit GEMV fix (bnb PR #1887, post-0.49.2); bnb <= 0.49.2 # NaNs at decode shape on every AMD GPU. Falls back to PyPI >=0.49.1 if the @@ -1456,12 +1490,19 @@ fi # ── Install uv ── tauri_log "STEP" "Installing uv package manager" -UV_MIN_VERSION="0.7.22" +UV_MIN_VERSION="0.8.16" # 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 +# uv >= 0.8.16 retries HTTP/2 streaming body errors; raise retries and read +# timeout for large wheel downloads. ":=" preserves any user override. +: "${UV_HTTP_RETRIES:=5}" +export UV_HTTP_RETRIES +: "${UV_HTTP_TIMEOUT:=180}" +export UV_HTTP_TIMEOUT + version_ge() { # returns 0 if $1 >= $2 _a=$1 @@ -2420,20 +2461,20 @@ if [ "$_MIGRATED" = true ]; then # PyPI metadata still declares torch as a hard dep), then install # runtime deps (typer, safetensors, transformers, etc.) with --no-deps # to prevent transitive torch resolution. - run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ + run_install_cmd_retry "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.6.7" unsloth-zoo # Resolve pydantic WITH deps so pip pins pydantic-core to the # matching version (no-torch-runtime.txt below is --no-deps). # All transitive deps are torch-free. - run_install_cmd "install pydantic (with deps for compatible core)" \ + run_install_cmd_retry "install pydantic (with deps for compatible core)" \ uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then - run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" + run_install_cmd_retry "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" fi else - run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.6.7" unsloth-zoo fi @@ -2441,7 +2482,7 @@ if [ "$_MIGRATED" = true ]; then substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" fi @@ -2456,7 +2497,7 @@ if [ "$_MIGRATED" = true ]; then _has_hip=$("$_VENV_PY" -c "import torch; print(getattr(torch.version,'hip','') or '')" 2>/dev/null || true) if [ -z "$_has_hip" ]; then substep "repairing ROCm torch (overwritten by dependency resolution)..." - run_install_cmd "repair ROCm torch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "repair ROCm torch" uv pip install --python "$_VENV_PY" \ "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" \ --force-reinstall @@ -2582,7 +2623,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then if [ -z "$_torch_whl" ] || [ -z "$_tv_whl" ] || [ -z "$_ta_whl" ] || \ [ "$_radeon_versions_match" != true ]; then substep "[WARN] Radeon repo lacks a compatible wheel set for this Python; falling back to ROCm index ($TORCH_INDEX_URL)" "$C_WARN" - run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install PyTorch" uv pip install --python "$_VENV_PY" \ "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" else @@ -2594,30 +2635,30 @@ elif [ -n "$TORCH_INDEX_URL" ]; then # filelock / sympy / networkx which are not in the # Radeon listing. if [ -n "$_tri_whl" ]; then - run_install_cmd "install triton + PyTorch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install triton + PyTorch" uv pip install --python "$_VENV_PY" \ --find-links "$_RADEON_BASE_URL" \ "$_tri_whl" "$_torch_whl" "$_tv_whl" "$_ta_whl" else - run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install PyTorch" uv pip install --python "$_VENV_PY" \ --find-links "$_RADEON_BASE_URL" \ "$_torch_whl" "$_tv_whl" "$_ta_whl" fi fi else substep "[WARN] Radeon repo unavailable; falling back to ROCm index ($TORCH_INDEX_URL)" "$C_WARN" - run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install PyTorch" uv pip install --python "$_VENV_PY" \ "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" fi else substep "[WARN] Radeon GPU detected but could not detect full ROCm version; falling back to ROCm index" "$C_WARN" - run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install PyTorch" uv pip install --python "$_VENV_PY" \ "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" fi else substep "installing PyTorch ($TORCH_INDEX_URL)..." - run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" "$TORCH_CONSTRAINT" torchvision torchaudio \ + run_install_cmd_retry "install PyTorch" uv pip install --python "$_VENV_PY" "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" fi # AMD ROCm: install bitsandbytes (once, after torch, for all ROCm paths). @@ -2637,35 +2678,35 @@ elif [ -n "$TORCH_INDEX_URL" ]; then if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ + run_install_cmd_retry "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ "unsloth>=2026.6.7" unsloth-zoo # Same pydantic-with-deps trick as the migrated branch. - run_install_cmd "install pydantic (with deps for compatible core)" \ + run_install_cmd_retry "install pydantic (with deps for compatible core)" \ uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then - run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" + run_install_cmd_retry "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" fi elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then - run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install unsloth (local)" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth "unsloth>=2026.6.7" unsloth-zoo substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" else - run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" fi # AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in @@ -2676,7 +2717,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then _has_hip=$("$_VENV_PY" -c "import torch; print(getattr(torch.version,'hip','') or '')" 2>/dev/null || true) if [ -z "$_has_hip" ]; then substep "repairing ROCm torch (overwritten by dependency resolution)..." - run_install_cmd "repair ROCm torch" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "repair ROCm torch" uv pip install --python "$_VENV_PY" \ "$TORCH_CONSTRAINT" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" \ --force-reinstall @@ -2689,15 +2730,15 @@ else tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.6.7" --torch-backend=auto + run_install_cmd_retry "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.6.7" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." - run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ + run_install_cmd_retry "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" else - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" --torch-backend=auto -- "$PACKAGE_NAME" + run_install_cmd_retry "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" --torch-backend=auto -- "$PACKAGE_NAME" fi fi diff --git a/studio/setup.ps1 b/studio/setup.ps1 index f02ed586be..7af532e7af 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -2035,6 +2035,15 @@ if (Test-Path -LiteralPath $LegacyStudioHome -PathType Container) { $LegacyStudioHome = (Resolve-Path -LiteralPath $LegacyStudioHome).Path } $StudioHomeIsCustom = ($_studioHomeCanon -ne $LegacyStudioHome) +# Directory-local evidence that Studio created $Path, used to adopt a custom-home +# llama.cpp predating the .unsloth-studio-owned marker (see setup.sh). Only the +# prebuilt UNSLOTH_PREBUILT_INFO.json counts; source builds are indistinguishable +# from a user clone on Windows and stay under the strict guard. +function Test-StudioOwnedAdoptable { + param([Parameter(Mandatory = $true)][string]$Path) + if (Test-Path -LiteralPath (Join-Path $Path "UNSLOTH_PREBUILT_INFO.json") -PathType Leaf) { return $true } + return $false +} function Assert-StudioOwnedOrAbsent { param( [Parameter(Mandatory = $true)][string]$Path, @@ -2042,6 +2051,10 @@ function Assert-StudioOwnedOrAbsent { ) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return } if ($StudioHomeIsCustom -and -not (Test-Path -LiteralPath (Join-Path $Path $StudioOwnedMarker) -PathType Leaf)) { + if (Test-StudioOwnedAdoptable $Path) { + Mark-StudioOwned $Path + return + } Write-Host "[ERROR] $Path already exists and is not marked as a Studio-owned $Label." -ForegroundColor Red Write-Host " Move it aside or choose an empty UNSLOTH_STUDIO_HOME before re-running." -ForegroundColor Yellow exit 1 diff --git a/studio/setup.sh b/studio/setup.sh index 4fba761cef..b78029d6e2 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -439,11 +439,25 @@ _STUDIO_HOME_IS_CUSTOM=false if [ "$_studio_home_canon" != "$_LEGACY_STUDIO_HOME" ]; then _STUDIO_HOME_IS_CUSTOM=true fi +# Directory-local evidence that Studio created "$1", used to adopt a custom-home +# llama.cpp predating the .unsloth-studio-owned marker without weakening the guard. +# Only UNSLOTH_PREBUILT_INFO.json counts (written exclusively by the prebuilt +# installer). A top-level llama-quantize symlink is NOT trusted: a user may have +# their own build with one, and this runs right before a destructive rm -rf, so we +# match Windows and keep markerless source builds strict. +_studio_owned_adoptable() { + [ -f "$1/UNSLOTH_PREBUILT_INFO.json" ] && return 0 + return 1 +} _assert_studio_owned_or_absent() { _aso_dir="$1" _aso_label="$2" [ -d "$_aso_dir" ] || return 0 if [ "$_STUDIO_HOME_IS_CUSTOM" = true ] && [ ! -f "$_aso_dir/$_STUDIO_OWNED_MARKER" ]; then + if _studio_owned_adoptable "$_aso_dir"; then + : > "$_aso_dir/$_STUDIO_OWNED_MARKER" 2>/dev/null || true + return 0 + fi echo "ERROR: $_aso_dir already exists and is not marked as a Studio-owned $_aso_label." >&2 echo " Move it aside or choose an empty UNSLOTH_STUDIO_HOME before re-running." >&2 exit 1