unsloth/install.ps1
Daniel Han ef0491e0fe
Fix Windows installer Python detection and winget error handling (#4483)
* Fix Windows installer Python detection and winget error handling

The PowerShell installer crashes on some Windows machines due to two
issues:

1. Windows Store App Execution Aliases: Get-Command finds the stub at
   WindowsApps\python.exe, then python --version writes to stderr.
   With $ErrorActionPreference = "Stop" on PowerShell 5.1, stderr
   from native commands becomes a terminating error, killing the
   script before it tries to install Python.

2. winget "already installed" exit code: winget returns -1978335189
   (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) when the package is
   already at the latest version. The script treated any non-zero exit
   as failure. The fallback Get-Command check could also find the
   Store stub or fail if Python was partially uninstalled.

Changes:

- Add Find-CompatiblePython helper that tries the py launcher first,
  then python3/python via Get-Command -All, explicitly skipping any
  WindowsApps stubs. All invocations wrapped in try-catch so stderr
  never triggers ErrorActionPreference.

- Replace exit-code-based winget error handling with outcome-based:
  re-detect Python after install, retry with --force if not found,
  show actionable manual install instructions on final failure.

- Deduplicate PATH entries in Refresh-SessionPath to prevent unbounded
  growth from repeated machine+user path prepending.

* Address reviewer feedback: wrap winget calls, remove blanket WindowsApps filter

Three fixes based on code review:

1. Wrap all winget install calls in $ErrorActionPreference = "Continue"
   blocks so that winget stderr (progress bars, warnings) does not
   become a terminating error on PowerShell 5.1. This matches the
   pattern already used in studio/setup.ps1 line 983.

2. Remove the blanket *\WindowsApps\* path filter that rejected all
   WindowsApps executables including valid Microsoft Store Python
   installs. Instead, rely on the existing try-catch + version regex
   probing to determine if a candidate is functional. Non-functional
   entries (App Execution Alias stubs) fail the try-catch and are
   skipped naturally.

3. Use $pyLauncher.Source (resolved path) instead of bare py name,
   add -CommandType Application to avoid matching aliases/functions,
   and derive winget package ID from $PythonVersion variable instead
   of hardcoding Python.Python.3.13.

* Add back WindowsApps filter for python3/python fallback path

The App Execution Alias stubs in WindowsApps can open the Microsoft
Store as a side effect when invoked, even though the try-catch handles
the error. Since the py launcher (tried first) already detects
legitimate Store Python -- Store packages include py since Python
3.11 -- filtering WindowsApps in the python3/python fallback is safe
and avoids the Store popup.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
2026-03-20 02:01:23 -07:00

210 lines
9.8 KiB
PowerShell

# Unsloth Studio Installer for Windows PowerShell
# Usage: irm https://raw.githubusercontent.com/unslothai/unsloth/main/install.ps1 | iex
# Local: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass; .\install.ps1
function Install-UnslothStudio {
$ErrorActionPreference = "Stop"
$VenvName = "unsloth_studio"
$PythonVersion = "3.13"
Write-Host ""
Write-Host "========================================="
Write-Host " Unsloth Studio Installer (Windows)"
Write-Host "========================================="
Write-Host ""
# ── Helper: refresh PATH from registry (deduplicating entries) ──
function Refresh-SessionPath {
$machine = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
$user = [System.Environment]::GetEnvironmentVariable("Path", "User")
$merged = "$machine;$user;$env:Path"
$seen = @{}
$unique = @()
foreach ($p in $merged -split ";") {
$key = $p.TrimEnd("\").ToLowerInvariant()
if ($key -and -not $seen.ContainsKey($key)) {
$seen[$key] = $true
$unique += $p
}
}
$env:Path = $unique -join ";"
}
# ── Check winget ──
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Host "Error: winget is not available." -ForegroundColor Red
Write-Host " Install it from https://aka.ms/getwinget" -ForegroundColor Yellow
Write-Host " or install Python $PythonVersion and uv manually, then re-run." -ForegroundColor Yellow
return
}
# ── Helper: detect a working Python 3.11-3.13 on the system ──
# Returns the version string (e.g. "3.13") or "" if none found.
# Uses try-catch + stderr redirection so that App Execution Alias stubs
# (WindowsApps) and other non-functional executables are probed safely
# without triggering $ErrorActionPreference = "Stop".
function Find-CompatiblePython {
# Try the Python Launcher first (most reliable on Windows)
$pyLauncher = Get-Command py -CommandType Application -ErrorAction SilentlyContinue
if ($pyLauncher) {
foreach ($minor in @("3.13", "3.12", "3.11")) {
try {
$out = & $pyLauncher.Source "-$minor" --version 2>&1 | Out-String
if ($out -match "Python (3\.1[1-3])\.\d+") { return $Matches[1] }
} catch {}
}
}
# Try python3 / python via Get-Command -All to look past stubs that
# might shadow a real Python further down PATH.
# Skip WindowsApps entries: the App Execution Alias stubs live there
# and can open the Microsoft Store as a side effect. Legitimate Store
# Python is already detected via the py launcher above (Store packages
# include py since Python 3.11).
foreach ($name in @("python3", "python")) {
foreach ($cmd in @(Get-Command $name -All -ErrorAction SilentlyContinue)) {
if (-not $cmd.Source) { continue }
if ($cmd.Source -like "*\WindowsApps\*") { continue }
try {
$out = & $cmd.Source --version 2>&1 | Out-String
if ($out -match "Python (3\.1[1-3])\.\d+") { return $Matches[1] }
} catch {}
}
}
return ""
}
# ── Install Python if no compatible version (3.11-3.13) found ──
$DetectedPythonVersion = Find-CompatiblePython
if ($DetectedPythonVersion) {
Write-Host "==> Python already installed: Python $DetectedPythonVersion"
}
if (-not $DetectedPythonVersion) {
Write-Host "==> Installing Python ${PythonVersion}..."
$pythonPackageId = "Python.Python.$PythonVersion"
# Temporarily lower ErrorActionPreference so that winget stderr
# (progress bars, warnings) does not become a terminating error
# on PowerShell 5.1 where native-command stderr is ErrorRecord.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
winget install -e --id $pythonPackageId --accept-package-agreements --accept-source-agreements
$wingetExit = $LASTEXITCODE
} catch { $wingetExit = 1 }
$ErrorActionPreference = $prevEAP
Refresh-SessionPath
# Re-detect after install (PATH may have changed)
$DetectedPythonVersion = Find-CompatiblePython
if (-not $DetectedPythonVersion) {
# Python still not functional after winget -- force reinstall.
# This handles both real failures AND "already installed" codes where
# winget thinks Python is present but it's not actually on PATH
# (e.g. user partially uninstalled, or installed via a different method).
Write-Host " Python not found on PATH after winget. Retrying with --force..."
$ErrorActionPreference = "Continue"
try {
winget install -e --id $pythonPackageId --accept-package-agreements --accept-source-agreements --force
$wingetExit = $LASTEXITCODE
} catch { $wingetExit = 1 }
$ErrorActionPreference = $prevEAP
Refresh-SessionPath
$DetectedPythonVersion = Find-CompatiblePython
}
if (-not $DetectedPythonVersion) {
Write-Host "[ERROR] Python installation failed (exit code $wingetExit)" -ForegroundColor Red
Write-Host " Please install Python $PythonVersion manually from https://www.python.org/downloads/" -ForegroundColor Yellow
Write-Host " Make sure to check 'Add Python to PATH' during installation." -ForegroundColor Yellow
Write-Host " Then re-run this installer." -ForegroundColor Yellow
return
}
}
# ── Install uv if not present ──
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host "==> Installing uv package manager..."
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try { winget install --id=astral-sh.uv -e --accept-package-agreements --accept-source-agreements } catch {}
$ErrorActionPreference = $prevEAP
Refresh-SessionPath
# Fallback: if winget didn't put uv on PATH, try the PowerShell installer
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host " Trying alternative uv installer..."
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Refresh-SessionPath
}
}
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host "Error: uv could not be installed." -ForegroundColor Red
Write-Host " Install it from https://docs.astral.sh/uv/" -ForegroundColor Yellow
return
}
# ── Create venv (skip if it already exists and has a valid interpreter) ──
$VenvPython = Join-Path $VenvName "Scripts\python.exe"
if (-not (Test-Path $VenvPython)) {
if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName }
Write-Host "==> Creating Python ${DetectedPythonVersion} virtual environment (${VenvName})..."
uv venv $VenvName --python $DetectedPythonVersion
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
} else {
Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation."
}
# ── Install unsloth directly into the venv (no activation needed) ──
Write-Host "==> Installing unsloth (this may take a few minutes)..."
uv pip install --python $VenvPython unsloth --torch-backend=auto
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
# ── Run studio setup ──
# setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools,
# CUDA Toolkit, Node.js, and other dependencies automatically via winget.
Write-Host "==> Running unsloth studio setup..."
$UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe"
if (-not (Test-Path $UnslothExe)) {
Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red
Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow
Write-Host " This usually means an older unsloth version was installed that does not include the Studio CLI." -ForegroundColor Yellow
Write-Host " Try re-running the installer or see: https://github.com/unslothai/unsloth?tab=readme-ov-file#-quickstart" -ForegroundColor Yellow
return
}
& $UnslothExe studio setup
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
Write-Host ""
Write-Host "========================================="
Write-Host " Unsloth Studio installed!"
Write-Host "========================================="
Write-Host ""
# Launch studio automatically in interactive terminals;
# in non-interactive environments (CI, Docker) just print instructions.
$IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
if ($IsInteractive) {
Write-Host "==> Launching Unsloth Studio..."
Write-Host ""
$UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe"
& $UnslothExe studio -H 0.0.0.0 -p 8888
} else {
Write-Host " To launch, run:"
Write-Host ""
Write-Host " .\${VenvName}\Scripts\activate"
Write-Host " unsloth studio -H 0.0.0.0 -p 8888"
Write-Host ""
}
}
Install-UnslothStudio