diff --git a/install.ps1 b/install.ps1 index f1f16d818d..0c36046195 100644 --- a/install.ps1 +++ b/install.ps1 @@ -6,6 +6,7 @@ function Install-UnslothStudio { $ErrorActionPreference = "Stop" + $script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq "1") # ── Parse flags ── $StudioLocalInstall = $false @@ -17,6 +18,8 @@ function Install-UnslothStudio { switch ($argList[$i]) { "--local" { $StudioLocalInstall = $true } "--no-torch" { $SkipTorch = $true } + "--verbose" { $script:UnslothVerbose = $true } + "-v" { $script:UnslothVerbose = $true } "--package" { $i++ if ($i -ge $argList.Count) { @@ -27,6 +30,12 @@ function Install-UnslothStudio { } } } + # Propagate to child processes so they also respect verbose mode. + # Process-scoped -- does not persist. + if ($script:UnslothVerbose) { + $env:UNSLOTH_VERBOSE = '1' + } + if ($StudioLocalInstall) { $RepoRoot = (Resolve-Path (Split-Path -Parent $PSCommandPath)).Path if (-not (Test-Path (Join-Path $RepoRoot "pyproject.toml"))) { @@ -39,10 +48,55 @@ function Install-UnslothStudio { $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" $VenvDir = Join-Path $StudioHome "unsloth_studio" + $Rule = [string]::new([char]0x2500, 52) + $Sloth = [char]::ConvertFromUtf32(0x1F9A5) + + function Enable-StudioVirtualTerminal { + if ($env:NO_COLOR) { return $false } + try { + if (-not ("StudioVT.Native" -as [type])) { + Add-Type -Namespace StudioVT -Name Native -MemberDefinition @' +[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int nStdHandle); +[DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint m); +[DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint m); +'@ -ErrorAction Stop + } + $h = [StudioVT.Native]::GetStdHandle(-11) + [uint32]$mode = 0 + if (-not [StudioVT.Native]::GetConsoleMode($h, [ref]$mode)) { return $false } + $mode = $mode -bor 0x0004 + return [StudioVT.Native]::SetConsoleMode($h, $mode) + } catch { + return $false + } + } + $script:StudioVtOk = Enable-StudioVirtualTerminal + + function Get-StudioAnsi { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Title', 'Dim', 'Ok', 'Warn', 'Err', 'Reset')] + [string]$Kind + ) + $e = [char]27 + switch ($Kind) { + 'Title' { return "${e}[38;5;150m" } + 'Dim' { return "${e}[38;5;245m" } + 'Ok' { return "${e}[38;5;108m" } + 'Warn' { return "${e}[38;5;136m" } + 'Err' { return "${e}[91m" } + 'Reset' { return "${e}[0m" } + } + } + Write-Host "" - Write-Host "=========================================" - Write-Host " Unsloth Studio Installer (Windows)" - Write-Host "=========================================" + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + Write-Host (" " + (Get-StudioAnsi Title) + $Sloth + " Unsloth Studio Installer (Windows)" + (Get-StudioAnsi Reset)) + Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) + } else { + Write-Host (" {0} Unsloth Studio Installer (Windows)" -f $Sloth) -ForegroundColor DarkGreen + Write-Host " $Rule" -ForegroundColor DarkGray + } Write-Host "" # ── Helper: refresh PATH from registry (deduplicating entries) ── @@ -62,13 +116,96 @@ function Install-UnslothStudio { $env:Path = $unique -join ";" } + function step { + param( + [Parameter(Mandatory = $true)][string]$Label, + [Parameter(Mandatory = $true)][string]$Value, + [string]$Color = "Green" + ) + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + $dim = Get-StudioAnsi Dim + $rst = Get-StudioAnsi Reset + $val = switch ($Color) { + 'Green' { Get-StudioAnsi Ok } + 'Yellow' { Get-StudioAnsi Warn } + 'Red' { Get-StudioAnsi Err } + 'DarkGray' { Get-StudioAnsi Dim } + default { Get-StudioAnsi Ok } + } + $padded = if ($Label.Length -ge 15) { $Label.Substring(0, 15) } else { $Label.PadRight(15) } + Write-Host (" {0}{1}{2}{3}{4}{2}" -f $dim, $padded, $rst, $val, $Value) + } else { + $padded = if ($Label.Length -ge 15) { $Label.Substring(0, 15) } else { $Label.PadRight(15) } + Write-Host (" {0}" -f $padded) -NoNewline -ForegroundColor DarkGray + $fc = switch ($Color) { + 'Green' { 'DarkGreen' } + 'Yellow' { 'Yellow' } + 'Red' { 'Red' } + 'DarkGray' { 'DarkGray' } + default { 'DarkGreen' } + } + Write-Host $Value -ForegroundColor $fc + } + } + + function substep { + param( + [Parameter(Mandatory = $true)][string]$Message, + [string]$Color = "DarkGray" + ) + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + $msgCol = switch ($Color) { + 'Yellow' { (Get-StudioAnsi Warn) } + 'Red' { (Get-StudioAnsi Err) } + default { (Get-StudioAnsi Dim) } + } + $pad = "".PadRight(15) + Write-Host (" {0}{1}{2}{3}" -f $msgCol, $pad, $Message, (Get-StudioAnsi Reset)) + } else { + $fc = switch ($Color) { + 'Yellow' { 'Yellow' } + 'Red' { 'Red' } + default { 'DarkGray' } + } + Write-Host (" {0,-15}{1}" -f "", $Message) -ForegroundColor $fc + } + } + + # Run native commands quietly by default to match install.sh behavior. + # Full command output is shown only when --verbose / UNSLOTH_VERBOSE=1. + function Invoke-InstallCommand { + param( + [Parameter(Mandatory = $true)][ScriptBlock]$Command + ) + $prevEap = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + # Reset to avoid stale values from prior native commands. + $global:LASTEXITCODE = 0 + if ($script:UnslothVerbose) { + # Merge stderr into stdout so progress/warning output stays visible + # without flipping $? on successful native commands (PS 5.1 treats + # stderr records as errors that set $? = $false even on exit code 0). + & $Command 2>&1 | Out-Host + } else { + $output = & $Command 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + Write-Host $output -ForegroundColor Red + } + } + return [int]$LASTEXITCODE + } finally { + $ErrorActionPreference = $prevEap + } + } + function New-StudioShortcuts { param( [Parameter(Mandatory = $true)][string]$UnslothExePath ) if (-not (Test-Path $UnslothExePath)) { - Write-Host "[WARN] Cannot create shortcuts: unsloth.exe not found at $UnslothExePath" -ForegroundColor Yellow + substep "cannot create shortcuts, unsloth.exe not found at $UnslothExePath" "Yellow" return } try { @@ -81,7 +218,7 @@ function Install-UnslothStudio { $localAppDataDir = $env:LOCALAPPDATA if (-not $localAppDataDir -or [string]::IsNullOrWhiteSpace($localAppDataDir)) { - Write-Host "[WARN] LOCALAPPDATA path unavailable; skipped shortcut creation" -ForegroundColor Yellow + substep "LOCALAPPDATA path unavailable; skipped shortcut creation" "Yellow" return } $appDir = Join-Path $localAppDataDir "Unsloth Studio" @@ -104,10 +241,10 @@ function Install-UnslothStudio { $null } if (-not $desktopLink) { - Write-Host "[WARN] Desktop path unavailable; skipped desktop shortcut creation" -ForegroundColor Yellow + substep "Desktop path unavailable; skipped desktop shortcut creation" "Yellow" } if (-not $startMenuLink) { - Write-Host "[WARN] APPDATA/Start Menu path unavailable; skipped Start menu shortcut creation" -ForegroundColor Yellow + substep "APPDATA/Start Menu path unavailable; skipped Start menu shortcut creation" "Yellow" } $iconPath = Join-Path $appDir "unsloth.ico" $bundledIcon = $null @@ -362,27 +499,27 @@ shell.Run cmd, 0, False $shortcut.Save() $createdShortcutCount++ } catch { - Write-Host "[WARN] Could not create shortcut at ${linkPath}: $($_.Exception.Message)" -ForegroundColor Yellow + substep "could not create shortcut at ${linkPath}: $($_.Exception.Message)" "Yellow" } } if ($createdShortcutCount -gt 0) { - Write-Host "[OK] Created Unsloth Studio shortcut(s): $createdShortcutCount" -ForegroundColor Green + substep "Created Unsloth Studio shortcut" } else { - Write-Host "[WARN] No Unsloth Studio shortcuts were created" -ForegroundColor Yellow + substep "no Unsloth Studio shortcuts were created" "Yellow" } } catch { - Write-Host "[WARN] Shortcut creation unavailable: $($_.Exception.Message)" -ForegroundColor Yellow + substep "shortcut creation unavailable: $($_.Exception.Message)" "Yellow" } } catch { - Write-Host "[WARN] Shortcut setup failed; skipping shortcuts: $($_.Exception.Message)" -ForegroundColor Yellow + substep "shortcut setup failed; skipping shortcuts: $($_.Exception.Message)" "Yellow" } } # ── 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 + step "winget" "not available" "Red" + substep "Install it from https://aka.ms/getwinget" "Yellow" + substep "or install Python $PythonVersion and uv manually, then re-run." "Yellow" return } @@ -460,10 +597,10 @@ shell.Run cmd, 0, False # Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null. $DetectedPython = Find-CompatiblePython if ($DetectedPython) { - Write-Host "==> Python already installed: Python $($DetectedPython.Version)" + step "python" "Python $($DetectedPython.Version) already installed" } if (-not $DetectedPython) { - Write-Host "==> Installing Python ${PythonVersion}..." + substep "installing Python ${PythonVersion}..." $pythonPackageId = "Python.Python.$PythonVersion" # Temporarily lower ErrorActionPreference so that winget stderr # (progress bars, warnings) does not become a terminating error @@ -485,7 +622,7 @@ shell.Run cmd, 0, False # 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..." + substep "Python not found on PATH after winget. Retrying with --force..." "Yellow" $ErrorActionPreference = "Continue" try { winget install -e --id $pythonPackageId --accept-package-agreements --accept-source-agreements --force @@ -507,7 +644,7 @@ shell.Run cmd, 0, False # ── Install uv if not present ── if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { - Write-Host "==> Installing uv package manager..." + substep "installing uv package manager..." $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" try { winget install --id=astral-sh.uv -e --accept-package-agreements --accept-source-agreements } catch {} @@ -515,15 +652,15 @@ shell.Run cmd, 0, False 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..." + substep "trying alternative uv installer..." "Yellow" 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 + step "uv" "could not be installed" "Red" + substep "Install it from https://docs.astral.sh/uv/" "Yellow" return } @@ -539,13 +676,13 @@ shell.Run cmd, 0, False if (Test-Path $VenvPython) { # New layout already exists -- nuke for fresh install - Write-Host "==> Removing existing environment for fresh install..." + substep "removing existing environment for fresh install..." Remove-Item -Recurse -Force $VenvDir } elseif (Test-Path (Join-Path $StudioHome ".venv\Scripts\python.exe")) { # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating $OldVenv = Join-Path $StudioHome ".venv" $OldPy = Join-Path $OldVenv "Scripts\python.exe" - Write-Host "==> Found legacy Studio environment, validating..." + substep "found legacy Studio environment, validating..." $prevEAP2 = $ErrorActionPreference $ErrorActionPreference = "Continue" try { @@ -554,32 +691,34 @@ shell.Run cmd, 0, False } catch { $torchOk = $false } $ErrorActionPreference = $prevEAP2 if ($torchOk) { - Write-Host " Legacy environment is healthy -- migrating..." + substep "legacy environment is healthy -- migrating..." Move-Item -Path $OldVenv -Destination $VenvDir -Force - Write-Host " Moved .venv -> unsloth_studio" + substep "moved .venv -> unsloth_studio" $_Migrated = $true } else { - Write-Host " Legacy environment failed validation -- creating fresh environment" + substep "legacy environment failed validation -- creating fresh environment" "Yellow" Remove-Item -Recurse -Force $OldVenv -ErrorAction SilentlyContinue } } elseif (Test-Path (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) { # CWD-relative venv from old install.ps1 -- migrate to absolute path $CwdVenv = Join-Path $env:USERPROFILE "unsloth_studio" - Write-Host "==> Found CWD-relative Studio environment, migrating to $VenvDir..." + substep "found CWD-relative Studio environment, migrating to $VenvDir..." Move-Item -Path $CwdVenv -Destination $VenvDir -Force - Write-Host " Moved ~/unsloth_studio -> ~/.unsloth/studio/unsloth_studio" + substep "moved ~/unsloth_studio -> ~/.unsloth/studio/unsloth_studio" $_Migrated = $true } if (-not (Test-Path $VenvPython)) { - Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment ($VenvDir)..." - uv venv $VenvDir --python "$($DetectedPython.Path)" - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red + step "venv" "creating Python $($DetectedPython.Version) virtual environment" + substep "$VenvDir" + $venvExit = Invoke-InstallCommand { uv venv $VenvDir --python "$($DetectedPython.Path)" } + if ($venvExit -ne 0) { + Write-Host "[ERROR] Failed to create virtual environment (exit code $venvExit)" -ForegroundColor Red return } } else { - Write-Host "==> Using migrated environment at $VenvDir" + step "venv" "using migrated environment" + substep "$VenvDir" } # ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ── @@ -588,7 +727,7 @@ shell.Run cmd, 0, False try { $nvSmiCmd = Get-Command nvidia-smi -ErrorAction SilentlyContinue if ($nvSmiCmd) { - & $nvSmiCmd.Source 2>&1 | Out-Null + & $nvSmiCmd.Source *> $null if ($LASTEXITCODE -eq 0) { $HasNvidiaSmi = $true; $NvidiaSmiExe = $nvSmiCmd.Source } } } catch {} @@ -599,18 +738,18 @@ shell.Run cmd, 0, False )) { if (Test-Path $p) { try { - & $p 2>&1 | Out-Null + & $p *> $null if ($LASTEXITCODE -eq 0) { $HasNvidiaSmi = $true; $NvidiaSmiExe = $p; break } } catch {} } } } if ($HasNvidiaSmi) { - Write-Host "[OK] NVIDIA GPU detected" -ForegroundColor Green + step "gpu" "NVIDIA GPU detected" } else { - Write-Host "[WARN] No NVIDIA GPU detected. Studio will run in chat-only (GGUF) mode." -ForegroundColor Yellow - Write-Host " Training and GPU inference require an NVIDIA GPU with drivers installed." -ForegroundColor Yellow - Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow + step "gpu" "none (chat-only / GGUF)" "Yellow" + substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow" + substep "https://www.nvidia.com/Download/index.aspx" "Yellow" } # ── Choose the correct PyTorch index URL based on driver CUDA version ── @@ -630,7 +769,7 @@ shell.Run cmd, 0, False return "$baseUrl/cpu" } } catch {} - Write-Host "[WARN] Could not determine CUDA version from nvidia-smi, defaulting to cu126" -ForegroundColor Yellow + substep "could not determine CUDA version from nvidia-smi, defaulting to cu126" "Yellow" return "$baseUrl/cu126" } $TorchIndexUrl = Get-TorchIndexUrl @@ -677,74 +816,101 @@ shell.Run cmd, 0, False if ($_Migrated) { # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state # in the new venv location, while preserving existing torch/CUDA - Write-Host "==> Upgrading unsloth in migrated environment..." + substep "upgrading unsloth in migrated environment..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo - $NoTorchReq = Find-NoTorchRuntimeFile - if ($NoTorchReq) { - uv pip install --python $VenvPython --no-deps -r $NoTorchReq + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo } + if ($baseInstallExit -eq 0) { + $NoTorchReq = Find-NoTorchRuntimeFile + if ($NoTorchReq) { + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } + } } } else { - uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo } + } + if ($baseInstallExit -ne 0) { + Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red + return } if ($StudioLocalInstall) { - Write-Host "==> Overlaying local repo (editable)..." - uv pip install --python $VenvPython -e $RepoRoot --no-deps + substep "overlaying local repo (editable)..." + $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } + if ($overlayExit -ne 0) { + Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red + return + } } } elseif ($TorchIndexUrl) { if ($SkipTorch) { - Write-Host "==> Skipping PyTorch (--no-torch flag set)." + substep "skipping PyTorch (--no-torch flag set)." "Yellow" } else { - Write-Host "==> Installing PyTorch ($TorchIndexUrl)..." - uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red + substep "installing PyTorch ($TorchIndexUrl)..." + $torchInstallExit = Invoke-InstallCommand { 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 } } - Write-Host "==> Installing unsloth (this may take a few minutes)..." + substep "installing unsloth (this may take a few minutes)..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo - $NoTorchReq = Find-NoTorchRuntimeFile - if ($NoTorchReq) { - uv pip install --python $VenvPython --no-deps -r $NoTorchReq - } - if ($StudioLocalInstall) { - Write-Host "==> Overlaying local repo (editable)..." - uv pip install --python $VenvPython -e $RepoRoot --no-deps + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.3.16" unsloth-zoo } + if ($baseInstallExit -eq 0) { + $NoTorchReq = Find-NoTorchRuntimeFile + if ($NoTorchReq) { + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps -r $NoTorchReq } + } } } elseif ($StudioLocalInstall) { - uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.16" unsloth-zoo - Write-Host "==> Overlaying local repo (editable)..." - uv pip install --python $VenvPython -e $RepoRoot --no-deps + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.16" unsloth-zoo } } else { - uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" + $baseInstallExit = Invoke-InstallCommand { 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 + return + } + + if ($StudioLocalInstall) { + substep "overlaying local repo (editable)..." + $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } + if ($overlayExit -ne 0) { + Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red + return + } } } else { # Fallback: GPU detection failed to produce a URL -- let uv resolve torch - Write-Host "==> Installing unsloth (this may take a few minutes)..." + substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.3.16" --torch-backend=auto - Write-Host "==> Overlaying local repo (editable)..." - uv pip install --python $VenvPython -e $RepoRoot --no-deps + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.3.16" --torch-backend=auto } + if ($baseInstallExit -ne 0) { + Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red + return + } + substep "overlaying local repo (editable)..." + $overlayExit = Invoke-InstallCommand { uv pip install --python $VenvPython -e $RepoRoot --no-deps } + if ($overlayExit -ne 0) { + Write-Host "[ERROR] Failed to overlay local repo (exit code $overlayExit)" -ForegroundColor Red + return + } } else { - uv pip install --python $VenvPython "$PackageName" --torch-backend=auto + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "$PackageName" --torch-backend=auto } + if ($baseInstallExit -ne 0) { + Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red + return + } } } - 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..." + step "setup" "running unsloth studio setup..." $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" if (-not (Test-Path $UnslothExe)) { Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red @@ -754,17 +920,27 @@ shell.Run cmd, 0, False return } # Tell setup.ps1 to skip base package installation (install.ps1 already did it) - # Tell setup.ps1 to skip base package installation (install.ps1 already did it) $env:SKIP_STUDIO_BASE = "1" $env:STUDIO_PACKAGE_NAME = $PackageName $env:UNSLOTH_NO_TORCH = if ($SkipTorch) { "true" } else { "false" } + # Always set STUDIO_LOCAL_INSTALL explicitly to avoid stale values from + # a previous --local run in the same PowerShell session. if ($StudioLocalInstall) { $env:STUDIO_LOCAL_INSTALL = "1" $env:STUDIO_LOCAL_REPO = $RepoRoot - } - & $UnslothExe studio setup - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red + } else { + $env:STUDIO_LOCAL_INSTALL = "0" + Remove-Item Env:STUDIO_LOCAL_REPO -ErrorAction SilentlyContinue + } + # Use 'studio setup' (not 'studio update') because 'update' pops + # SKIP_STUDIO_BASE, which would cause redundant package reinstallation + # and bypass the fast-path version check from PR #4667. + $studioArgs = @('studio', 'setup') + if ($script:UnslothVerbose) { $studioArgs += '--verbose' } + & $UnslothExe @studioArgs + $setupExit = $LASTEXITCODE + if ($setupExit -ne 0) { + Write-Host "[ERROR] unsloth studio setup failed (exit code $setupExit)" -ForegroundColor Red return } @@ -780,27 +956,18 @@ shell.Run cmd, 0, False [System.Environment]::SetEnvironmentVariable("Path", "$ScriptsDir", "User") } Refresh-SessionPath - Write-Host "[OK] Added unsloth to PATH" -ForegroundColor Green + step "path" "added unsloth to PATH" } - 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 studio -H 0.0.0.0 -p 8888 } else { - Write-Host " To launch, run:" - Write-Host "" - Write-Host " & `"$VenvDir\Scripts\Activate.ps1`"" - Write-Host " unsloth studio -H 0.0.0.0 -p 8888" + step "launch" "manual commands:" + substep "& `"$VenvDir\Scripts\Activate.ps1`"" + substep "unsloth studio -H 0.0.0.0 -p 8888" Write-Host "" } } diff --git a/install.sh b/install.sh index b50256ea5a..9ea80bc161 100755 --- a/install.sh +++ b/install.sh @@ -8,11 +8,36 @@ # Usage (py): ./install.sh --python 3.12 (override auto-detected Python version) set -e +# ── Output style (aligned with studio/setup.sh) ── +RULE="" +_rule_i=0 +while [ "$_rule_i" -lt 52 ]; do + RULE="${RULE}─" + _rule_i=$((_rule_i + 1)) +done +if [ -n "${NO_COLOR:-}" ]; then + C_TITLE= C_DIM= C_OK= C_WARN= C_ERR= C_RST= +elif [ -t 1 ] || [ -n "${FORCE_COLOR:-}" ]; then + _ESC="$(printf '\033')" + C_TITLE="${_ESC}[38;5;150m" + C_DIM="${_ESC}[38;5;245m" + C_OK="${_ESC}[38;5;108m" + C_WARN="${_ESC}[38;5;136m" + C_ERR="${_ESC}[91m" + C_RST="${_ESC}[0m" +else + C_TITLE= C_DIM= C_OK= C_WARN= C_ERR= C_RST= +fi + +step() { printf " ${C_DIM}%-15.15s${C_RST}${3:-$C_OK}%s${C_RST}\n" "$1" "$2"; } +substep() { printf " ${C_DIM}%-15s${2:-$C_DIM}%s${C_RST}\n" "" "$1"; } + # ── Parse flags ── STUDIO_LOCAL_INSTALL=false PACKAGE_NAME="unsloth" _USER_PYTHON="" _NO_TORCH_FLAG=false +_VERBOSE=false _next_is_package=false _next_is_python=false for arg in "$@"; do @@ -31,9 +56,44 @@ for arg in "$@"; do --package) _next_is_package=true ;; --python) _next_is_python=true ;; --no-torch) _NO_TORCH_FLAG=true ;; + --verbose|-v) _VERBOSE=true ;; esac done +if [ "$_VERBOSE" = true ]; then + export UNSLOTH_VERBOSE=1 +fi + +_is_verbose() { + [ "${UNSLOTH_VERBOSE:-0}" = "1" ] +} + +run_maybe_quiet() { + if _is_verbose; then + "$@" + else + "$@" > /dev/null 2>&1 + fi +} + +run_install_cmd() { + _label="$1" + shift + if _is_verbose; then + "$@" && return 0 + _rc=$? + step "error" "$_label failed (exit code $_rc)" "$C_ERR" >&2 + return "$_rc" + fi + _log=$(mktemp) + "$@" >"$_log" 2>&1 && { rm -f "$_log"; return 0; } + _rc=$? + step "error" "$_label failed (exit code $_rc)" "$C_ERR" >&2 + cat "$_log" >&2 + rm -f "$_log" + return $_rc +} + if [ "$_next_is_package" = true ]; then echo "❌ ERROR: --package requires an argument." >&2 exit 1 @@ -643,14 +703,13 @@ WSLPS1_EOF fi if [ "$_css_created" -eq 1 ]; then - echo "[OK] Created Unsloth Studio shortcut(s)" + substep "Created Unsloth Studio shortcut" fi } echo "" -echo "=========================================" -echo " Unsloth Studio Installer" -echo "=========================================" +printf " ${C_TITLE}%s${C_RST}\n" "🦥 Unsloth Studio Installer" +printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" # ── Detect platform ── @@ -660,7 +719,7 @@ if [ "$(uname)" = "Darwin" ]; then elif grep -qi microsoft /proc/version 2>/dev/null; then OS="wsl" fi -echo "==> Platform: $OS" +step "platform" "$OS" # ── Architecture detection & Python version ── _ARCH=$(uname -m) @@ -740,8 +799,8 @@ MISSING=$(echo "$MISSING" | sed 's/^ *//') if [ -n "$MISSING" ]; then echo "" - echo "==> Unsloth Studio needs these packages: $MISSING" - echo " These are needed to build the GGUF inference engine." + step "deps" "missing: $MISSING" "$C_WARN" + substep "These are needed to build the GGUF inference engine." case "$OS" in macos) @@ -766,7 +825,7 @@ if [ -n "$MISSING" ]; then esac echo "" else - echo "==> All system dependencies found." + step "deps" "all system dependencies found" fi # ── Install uv ── @@ -812,10 +871,10 @@ _uv_version_ok() { } if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then - echo "==> Installing uv package manager..." + substep "installing uv package manager..." _uv_tmp=$(mktemp) download "https://astral.sh/uv/install.sh" "$_uv_tmp" - sh "$_uv_tmp" Found legacy Studio environment, validating..." + substep "found legacy Studio environment, validating..." if "$STUDIO_HOME/.venv/bin/python" -c " import torch device = 'cuda' if torch.cuda.is_available() else 'cpu' @@ -866,8 +925,9 @@ if [ "$SKIP_TORCH" = true ] && [ "$MAC_INTEL" = true ] && [ -z "$_USER_PYTHON" ] fi if [ ! -x "$VENV_DIR/bin/python" ]; then - echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_DIR})..." - uv venv "$VENV_DIR" --python "$PYTHON_VERSION" + step "venv" "creating Python ${PYTHON_VERSION} virtual environment" + substep "$VENV_DIR" + run_install_cmd "create venv" uv venv "$VENV_DIR" --python "$PYTHON_VERSION" fi # Guard against Python 3.13.8 torch import bug on Apple Silicon @@ -880,12 +940,13 @@ if [ -z "$_USER_PYTHON" ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then echo " Recreating venv with Python 3.12..." rm -rf "$VENV_DIR" PYTHON_VERSION="3.12" - uv venv "$VENV_DIR" --python "$PYTHON_VERSION" + run_install_cmd "recreate venv" uv venv "$VENV_DIR" --python "$PYTHON_VERSION" fi fi if [ -x "$VENV_DIR/bin/python" ]; then - echo "==> Using environment at ${VENV_DIR}" + step "venv" "using environment" + substep "${VENV_DIR}" fi # ── Resolve repo root (for --local installs) ── @@ -960,71 +1021,71 @@ _VENV_PY="$VENV_DIR/bin/python" if [ "$_MIGRATED" = true ]; then # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state # in the new venv location, while preserving existing torch/CUDA - echo "==> Upgrading unsloth in migrated environment..." + substep "upgrading unsloth in migrated environment..." if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps (current # 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. - uv pip install --python "$_VENV_PY" --no-deps \ + run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.3.16" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then - uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" + run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" fi else - uv pip install --python "$_VENV_PY" \ + run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.3.16" unsloth-zoo fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - echo "==> Overlaying local repo (editable)..." - uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + substep "overlaying local repo (editable)..." + run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps fi elif [ -n "$TORCH_INDEX_URL" ]; then # Fresh: Step 1 - install torch from explicit index (skip when --no-torch or Intel Mac) if [ "$SKIP_TORCH" = true ]; then - echo "==> Skipping PyTorch (--no-torch or Intel Mac x86_64)." + substep "skipping PyTorch (--no-torch or Intel Mac x86_64)." "$C_WARN" else - echo "==> Installing PyTorch ($TORCH_INDEX_URL)..." - uv pip install --python "$_VENV_PY" "torch>=2.4,<2.11.0" torchvision torchaudio \ + substep "installing PyTorch ($TORCH_INDEX_URL)..." + run_install_cmd "install PyTorch" uv pip install --python "$_VENV_PY" "torch>=2.4,<2.11.0" torchvision torchaudio \ --index-url "$TORCH_INDEX_URL" fi # Fresh: Step 2 - install unsloth, preserving pre-installed torch - echo "==> Installing unsloth (this may take a few minutes)..." + substep "installing unsloth (this may take a few minutes)..." if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - uv pip install --python "$_VENV_PY" --no-deps \ + run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ "unsloth>=2026.3.16" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then - uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" + run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - echo "==> Overlaying local repo (editable)..." - uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + substep "overlaying local repo (editable)..." + run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps fi elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then - uv pip install --python "$_VENV_PY" \ + run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth "unsloth>=2026.3.16" unsloth-zoo - echo "==> Overlaying local repo (editable)..." - uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + substep "overlaying local repo (editable)..." + run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else - uv pip install --python "$_VENV_PY" \ + run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth "$PACKAGE_NAME" fi else # Fallback: GPU detection failed to produce a URL -- let uv resolve torch - echo "==> Installing unsloth (this may take a few minutes)..." + substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.3.16" --torch-backend=auto - echo "==> Overlaying local repo (editable)..." - uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.3.16" --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 else - uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto fi fi @@ -1059,19 +1120,33 @@ if [ -n "$VENV_ABS_BIN" ]; then export PATH="$VENV_ABS_BIN:$PATH" fi -echo "==> Running unsloth setup..." +if ! command -v bash >/dev/null 2>&1; then + step "setup" "bash is required to run studio setup" "$C_ERR" + substep "Please install bash and re-run install.sh" + exit 1 +fi + +step "setup" "running unsloth studio update..." +# install.sh already installs base packages (unsloth + unsloth-zoo) and +# no-torch-runtime.txt above, so tell install_python_stack.py to skip +# the base step to avoid redundant reinstallation. +_SKIP_BASE=1 +# Run setup.sh outside set -e so that a llama.cpp build failure (exit 1) +# does not skip PATH setup, shortcuts, and launch below. We capture the +# exit code and propagate it after post-install steps finish. +_SETUP_EXIT=0 if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - SKIP_STUDIO_BASE=1 \ + SKIP_STUDIO_BASE="$_SKIP_BASE" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=1 \ STUDIO_LOCAL_REPO="$_REPO_ROOT" \ UNSLOTH_NO_TORCH="$SKIP_TORCH" \ - bash "$SETUP_SH" > "$_SHELL_PROFILE" echo '# Added by Unsloth installer' >> "$_SHELL_PROFILE" echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_SHELL_PROFILE" - echo "==> Added ~/.local/bin to PATH in $_SHELL_PROFILE" + step "path" "added ~/.local/bin to PATH in $_SHELL_PROFILE" fi fi export PATH="$_LOCAL_BIN:$PATH" @@ -1105,17 +1180,30 @@ esac create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" +# If setup.sh failed, report and exit now. +# PATH and shortcuts are already set up so the user can fix and retry. +if [ "$_SETUP_EXIT" -ne 0 ]; then + echo "" + step "error" "studio setup failed (exit code $_SETUP_EXIT)" "$C_ERR" + substep "Check the output above for details, then re-run:" + if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + substep " unsloth studio update --local" + else + substep " unsloth studio update" + fi + echo "" + exit "$_SETUP_EXIT" +fi + echo "" -echo "=========================================" -echo " Unsloth Studio installed!" -echo "=========================================" +printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!" +printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" # Launch studio automatically in interactive terminals; # in non-interactive environments (Docker, CI, cloud-init) just print instructions. if [ -t 1 ]; then - echo "==> Launching Unsloth Studio..." - echo "" + step "launch" "starting Unsloth Studio..." "$VENV_DIR/bin/unsloth" studio -H 0.0.0.0 -p 8888 _LAUNCH_EXIT=$? if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then @@ -1130,13 +1218,10 @@ if [ -t 1 ]; then fi exit "$_LAUNCH_EXIT" else - echo " To launch, run:" - echo "" - echo " unsloth studio -H 0.0.0.0 -p 8888" - echo "" - echo " Or activate the environment first:" - echo "" - echo " source ${VENV_DIR}/bin/activate" - echo " unsloth studio -H 0.0.0.0 -p 8888" + step "launch" "manual commands:" + substep "unsloth studio -H 0.0.0.0 -p 8888" + substep "or activate env first:" + substep "source ${VENV_DIR}/bin/activate" + substep "unsloth studio -H 0.0.0.0 -p 8888" echo "" fi diff --git a/studio/backend/startup_banner.py b/studio/backend/startup_banner.py index 54acac0540..16b41d484c 100644 --- a/studio/backend/startup_banner.py +++ b/studio/backend/startup_banner.py @@ -20,7 +20,7 @@ def stdout_supports_color() -> bool: return True try: return sys.stdout.isatty() - except Exception: + except (AttributeError, OSError, ValueError): return False @@ -52,28 +52,36 @@ def style(text: str, code: str) -> str: ipv6_bind = bind_host in ("::", "::1") if ipv6_bind: - local_url = f"http://[::1]:{port}" + loopback_url = f"http://[::1]:{port}" alt_local = f"http://localhost:{port}" else: - local_url = f"http://127.0.0.1:{port}" + loopback_url = f"http://127.0.0.1:{port}" alt_local = f"http://localhost:{port}" if ":" in display_host: external_url = f"http://[{display_host}]:{port}" else: external_url = f"http://{display_host}:{port}" + listen_all = bind_host in ("0.0.0.0", "::") loopback_bind = bind_host in ("127.0.0.1", "localhost", "::1") - api_base = local_url if listen_all or loopback_bind else external_url + + # Use loopback URL only when the server is reachable on loopback; + # otherwise show the actual bound address. + primary_url = loopback_url if listen_all or loopback_bind else external_url + tip_url = alt_local if listen_all or loopback_bind else external_url + api_base = primary_url lines: list[str] = [ "", style("🦥 Unsloth Studio is running", title), style("─" * 52, dim), - style(" On this machine — open this in your browser:", dim), - style(f" {local_url}", local_url_style), - style(f" (same as {alt_local})", dim), + style(" On this machine -- open this in your browser:", dim), + style(f" {primary_url}", local_url_style), ] + if (listen_all or loopback_bind) and primary_url != alt_local: + lines.append(style(f" (same as {alt_local})", dim)) + if listen_all and display_host not in ( "127.0.0.1", "localhost", @@ -88,7 +96,7 @@ def style(text: str, code: str) -> str: style(f" {external_url}", secondary), ] ) - elif not listen_all and bind_host not in ("127.0.0.1", "localhost", "::1"): + elif not listen_all and not loopback_bind and external_url != primary_url: lines.extend( [ "", @@ -105,7 +113,7 @@ def style(text: str, code: str) -> str: style(f" {api_base}/api/health", secondary), style("─" * 52, dim), style( - " Tip: if you are on the same computer, use the Local link above.", + f" Tip: if you are on this computer, open {tip_url}/ in your browser.", dim, ), "", diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index e8ae22f470..f2981ea665 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -110,7 +110,7 @@ def _stdout_supports_color() -> bool: try: if not sys.stdout.isatty(): return False - except Exception: + except (AttributeError, OSError, ValueError): return False if IS_WINDOWS: try: @@ -121,7 +121,7 @@ def _stdout_supports_color() -> bool: mode = ctypes.c_ulong() kernel32.GetConsoleMode(handle, ctypes.byref(mode)) kernel32.SetConsoleMode(handle, mode.value | 0x0004) - except Exception: + except (ImportError, AttributeError, OSError): return False return True @@ -460,7 +460,7 @@ def install_python_stack() -> int: # 3. Core packages: unsloth-zoo + unsloth (or custom package name) if skip_base: - print(_green(f"✅ {package_name} already installed — skipping base packages")) + pass elif NO_TORCH: # No-torch update path: install unsloth + unsloth-zoo with --no-deps # (current PyPI metadata still declares torch as a hard dep), then diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 9a1d332a60..6e19fbea83 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -12,9 +12,8 @@ .NOTES Default output is minimal (step/substep), aligned with studio/setup.sh. - FULL / LEGACY LOGGING (defensible audit trail, multi-line [OK]/[WARN]/paths): + FULL / LEGACY LOGGING (defensible audit trail, detailed multi-line output): unsloth studio setup --verbose - (sets UNSLOTH_VERBOSE=1; same as install_python_stack.py) Or: $env:UNSLOTH_VERBOSE='1'; powershell -File .\studio\setup.ps1 Or: .\setup.ps1 --verbose #> @@ -23,14 +22,20 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $PackageDir = Split-Path -Parent $ScriptDir -# Same as: unsloth studio setup --verbose (see unsloth_cli/commands/studio.py) +# Verbose can be enabled either by CLI flag or by UNSLOTH_VERBOSE=1. +$script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq '1') foreach ($a in $args) { if ($a -eq '--verbose' -or $a -eq '-v') { - $env:UNSLOTH_VERBOSE = '1' + $script:UnslothVerbose = $true break } } -$script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq '1') +# Propagate to child processes (e.g. install_python_stack.py) so they +# also respect verbose mode. Process-scoped -- does not persist. +if ($script:UnslothVerbose) { + $env:UNSLOTH_VERBOSE = '1' +} +$script:LlamaCppDegraded = $false # Detect if running from pip install (no frontend/ dir in studio) $FrontendDir = Join-Path $ScriptDir "frontend" @@ -331,6 +336,51 @@ function Write-SetupVerboseDetail { } } +function Invoke-SetupCommand { + param( + [Parameter(Mandatory = $true)][scriptblock]$Command, + [switch]$AlwaysQuiet + ) + $prevEap = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + # Reset to avoid stale values from prior native commands. + $global:LASTEXITCODE = 0 + if ($script:UnslothVerbose -and -not $AlwaysQuiet) { + # Merge stderr into stdout so progress/warning output stays visible + # without flipping $? on successful native commands (PS 5.1 treats + # stderr records as errors that set $? = $false even on exit code 0). + & $Command 2>&1 | Out-Host + } else { + $output = & $Command 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + Write-Host $output -ForegroundColor Red + } + } + return [int]$LASTEXITCODE + } finally { + $ErrorActionPreference = $prevEap + } +} + +function Write-LlamaFailureLog { + param( + [string]$Output, + [int]$MaxLines = 120 + ) + if (-not $Output) { return } + $lines = @( + ($Output -split "`r?`n") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + if ($lines.Count -eq 0) { return } + if ($lines.Count -gt $MaxLines) { + Write-Host " Showing last $MaxLines lines:" -ForegroundColor DarkGray + $lines = $lines | Select-Object -Last $MaxLines + } + foreach ($line in $lines) { + Write-Host " | $line" -ForegroundColor DarkGray + } +} function step { param( [Parameter(Mandatory = $true)][string]$Label, @@ -409,7 +459,7 @@ $NvidiaSmiExe = $null # Absolute path -- survives Refresh-Environment try { $nvSmiCmd = Get-Command nvidia-smi -ErrorAction SilentlyContinue if ($nvSmiCmd) { - & $nvSmiCmd.Source 2>&1 | Out-Null + & $nvSmiCmd.Source *> $null if ($LASTEXITCODE -eq 0) { $HasNvidiaSmi = $true $NvidiaSmiExe = $nvSmiCmd.Source @@ -426,7 +476,7 @@ if (-not $HasNvidiaSmi) { foreach ($p in $nvSmiDefaults) { if (Test-Path $p) { try { - & $p 2>&1 | Out-Null + & $p *> $null if ($LASTEXITCODE -eq 0) { $HasNvidiaSmi = $true $NvidiaSmiExe = $p @@ -459,7 +509,7 @@ try { } catch {} if ($LongPathsEnabled) { - Write-Host "[OK] Windows Long Paths enabled" -ForegroundColor Green + step "long paths" "enabled" } else { Write-Host "Windows Long Paths not enabled (required for Triton compilation and deep dependency paths)." -ForegroundColor Yellow Write-Host " Requesting admin access to fix..." -ForegroundColor Yellow @@ -470,12 +520,12 @@ if ($LongPathsEnabled) { -Verb RunAs -Wait -PassThru -ErrorAction Stop if ($proc.ExitCode -eq 0) { $LongPathsEnabled = $true - Write-Host "[OK] Windows Long Paths enabled (via UAC)" -ForegroundColor Green + step "long paths" "enabled (via UAC)" } else { - Write-Host "[WARN] Failed to enable Long Paths (exit code: $($proc.ExitCode))" -ForegroundColor Yellow + step "long paths" "failed to enable (exit code: $($proc.ExitCode))" "Yellow" } } catch { - Write-Host "[WARN] Could not enable Long Paths (UAC was declined or not available)" -ForegroundColor Yellow + step "long paths" "could not enable (UAC declined/unavailable)" "Yellow" Write-Host " Run this manually in an Admin terminal:" -ForegroundColor Yellow Write-Host ' reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f' -ForegroundColor Cyan } @@ -490,7 +540,7 @@ if (-not $HasGit) { $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { try { - winget install Git.Git --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Invoke-SetupCommand { winget install Git.Git --source winget --accept-package-agreements --accept-source-agreements } | Out-Null Refresh-Environment $HasGit = $null -ne (Get-Command git -ErrorAction SilentlyContinue) } catch { } @@ -514,7 +564,7 @@ if (-not $HasCmake) { $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { try { - winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Invoke-SetupCommand { winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements } | Out-Null Refresh-Environment $HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue) } catch { } @@ -579,7 +629,7 @@ if ($vsResult) { $CmakeGenerator = $vsResult.Generator $VsInstallPath = $vsResult.InstallPath step "vs" "$CmakeGenerator ($($vsResult.Source))" - if ($vsResult.ClExe) { Write-Host " cl.exe: $($vsResult.ClExe)" -ForegroundColor Gray } + if ($vsResult.ClExe) { substep "cl.exe: $($vsResult.ClExe)" } } else { Write-Host "[ERROR] Visual Studio Build Tools could not be found or installed." -ForegroundColor Red Write-Host " Manual install:" -ForegroundColor Red @@ -603,14 +653,14 @@ try { $smiOut = & $NvidiaSmiExe 2>&1 | Out-String if ($smiOut -match "CUDA Version:\s+([\d]+)\.([\d]+)") { $DriverMaxCuda = "$($Matches[1]).$($Matches[2])" - Write-Host " Driver supports up to CUDA $DriverMaxCuda" -ForegroundColor Gray + substep "driver supports up to CUDA $DriverMaxCuda" } } catch {} # Detect compute capability early so we can validate toolkit support $CudaArch = Get-CudaComputeCapability if ($CudaArch) { - Write-Host " GPU Compute Capability = $($CudaArch.Insert($CudaArch.Length-1, '.')) (sm_$CudaArch)" -ForegroundColor Gray + substep "GPU Compute Capability = $($CudaArch.Insert($CudaArch.Length-1, '.')) (sm_$CudaArch)" } # -- Find a toolkit that's compatible with the driver AND the GPU -- @@ -643,16 +693,16 @@ if ($DriverMaxCuda) { if ($CudaArch) { $archOk = Test-NvccArchSupport -NvccExe $candidateNvcc -Arch $CudaArch if (-not $archOk) { - Write-Host " [INFO] CUDA_PATH toolkit (CUDA $tkMaj.$tkMin) does not support GPU arch sm_$CudaArch" -ForegroundColor Yellow - Write-Host " Looking for a newer toolkit..." -ForegroundColor Yellow + substep "CUDA_PATH toolkit (CUDA $tkMaj.$tkMin) does not support GPU arch sm_$CudaArch" "Yellow" + substep "Looking for a newer toolkit..." "Yellow" } } if ($archOk) { $NvccPath = $candidateNvcc - Write-Host " [OK] Using existing CUDA Toolkit at CUDA_PATH (nvcc: $NvccPath)" -ForegroundColor Green + substep "using existing CUDA Toolkit at CUDA_PATH (nvcc: $NvccPath)" } } else { - Write-Host " [INFO] CUDA_PATH ($existingCudaPath) has CUDA $tkMaj.$tkMin which exceeds driver max $DriverMaxCuda" -ForegroundColor Yellow + substep "CUDA_PATH ($existingCudaPath) has CUDA $tkMaj.$tkMin which exceeds driver max $DriverMaxCuda" "Yellow" } } } @@ -661,11 +711,11 @@ if ($DriverMaxCuda) { if (-not $NvccPath) { $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda if ($NvccPath) { - Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green + substep "found compatible CUDA Toolkit (nvcc: $NvccPath)" if ($existingCudaPath) { $selectedRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent if ($existingCudaPath.TrimEnd('\') -ne $selectedRoot.TrimEnd('\')) { - Write-Host " [INFO] Overriding CUDA_PATH from $existingCudaPath to $selectedRoot" -ForegroundColor Yellow + substep "overriding CUDA_PATH from $existingCudaPath to $selectedRoot" "Yellow" } } } else { @@ -736,26 +786,26 @@ if (-not $NvccPath) { } if ($BestVersion) { - Write-Host " Installing CUDA Toolkit $BestVersion via winget... " -ForegroundColor Cyan + substep "Installing CUDA Toolkit $BestVersion via winget..." $prevEAPCuda = $ErrorActionPreference $ErrorActionPreference = "Continue" - winget install --id=Nvidia.CUDA --version=$BestVersion -e --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Invoke-SetupCommand { winget install --id=Nvidia.CUDA --version=$BestVersion -e --source winget --accept-package-agreements --accept-source-agreements } | Out-Null $ErrorActionPreference = $prevEAPCuda Refresh-Environment $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda if ($NvccPath) { - Write-Host " [OK] CUDA Toolkit $BestVersion installed (nvcc: $NvccPath)" -ForegroundColor Green + substep "CUDA Toolkit $BestVersion installed (nvcc: $NvccPath)" } } else { - Write-Host " [WARN] No compatible CUDA Toolkit version found in winget (need <= $DriverMaxCuda)" -ForegroundColor Yellow + substep "no compatible CUDA Toolkit version found in winget (need <= $DriverMaxCuda)" "Yellow" } } else { - Write-Host " Installing CUDA Toolkit (latest) via winget..." -ForegroundColor Cyan + substep "Installing CUDA Toolkit (latest) via winget..." winget install --id=Nvidia.CUDA -e --source winget --accept-package-agreements --accept-source-agreements Refresh-Environment $NvccPath = Find-Nvcc if ($NvccPath) { - Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + substep "CUDA Toolkit installed (nvcc: $NvccPath)" } } } @@ -781,7 +831,7 @@ $CudaToolkitRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent # Always persist CUDA_PATH to User registry so the compatible toolkit is used # in future sessions (overwrites any existing value pointing to a newer, incompatible version) [Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'User') -Write-Host " Persisted CUDA_PATH=$CudaToolkitRoot to user environment" -ForegroundColor Gray +substep "Persisted CUDA_PATH=$CudaToolkitRoot to user environment" # Clear all versioned CUDA_PATH_V* env vars in this process to prevent # cmake/MSBuild from discovering a conflicting CUDA installation. $cudaPathVars = @([Environment]::GetEnvironmentVariables('Process').Keys | Where-Object { $_ -match '^CUDA_PATH_V' }) @@ -793,7 +843,7 @@ $tkDirName = Split-Path $CudaToolkitRoot -Leaf if ($tkDirName -match '^v(\d+)\.(\d+)') { $cudaPathVerVar = "CUDA_PATH_V$($Matches[1])_$($Matches[2])" [Environment]::SetEnvironmentVariable($cudaPathVerVar, $CudaToolkitRoot, 'Process') - Write-Host " Set $cudaPathVerVar (cleared other CUDA_PATH_V* vars)" -ForegroundColor Gray + substep "Set $cudaPathVerVar (cleared other CUDA_PATH_V* vars)" } # Ensure nvcc's bin dir is on PATH for this process $nvccBinDir = Split-Path $NvccPath -Parent @@ -808,7 +858,7 @@ if (-not $userPath -or $userPath -notlike "*$nvccBinDir*") { } else { [Environment]::SetEnvironmentVariable('Path', "$nvccBinDir", 'User') } - Write-Host " Persisted CUDA bin dir to user PATH" -ForegroundColor Gray + substep "Persisted CUDA bin dir to user PATH" } # -- Ensure CUDA ↔ Visual Studio integration files exist -- @@ -821,10 +871,10 @@ if ($VsInstallPath -and $CudaToolkitRoot) { if ((Test-Path $cudaExtras) -and (Test-Path $vsCustomizations)) { $hasTargets = Get-ChildItem $vsCustomizations -Filter "CUDA *.targets" -ErrorAction SilentlyContinue if (-not $hasTargets) { - Write-Host " [INFO] CUDA VS integration missing -- copying .targets files..." -ForegroundColor Yellow + substep "CUDA VS integration missing -- copying .targets files..." "Yellow" try { Copy-Item "$cudaExtras\*" $vsCustomizations -Force -ErrorAction Stop - Write-Host " [OK] CUDA VS integration files installed" -ForegroundColor Green + substep "CUDA VS integration files installed" } catch { # Direct copy failed (needs admin). Try elevated copy via Start-Process. try { @@ -832,17 +882,17 @@ if ($VsInstallPath -and $CudaToolkitRoot) { Start-Process powershell -ArgumentList "-NoProfile -Command $copyCmd" -Verb RunAs -Wait -ErrorAction Stop $hasTargetsRetry = Get-ChildItem $vsCustomizations -Filter "CUDA *.targets" -ErrorAction SilentlyContinue if ($hasTargetsRetry) { - Write-Host " [OK] CUDA VS integration files installed (elevated)" -ForegroundColor Green + substep "CUDA VS integration files installed (elevated)" } else { throw "Copy did not produce .targets files" } } catch { - Write-Host " [WARN] Could not copy CUDA VS integration files" -ForegroundColor Yellow - Write-Host " The llama.cpp build may fail with 'No CUDA toolset found'." -ForegroundColor Yellow - Write-Host " Manual fix: copy contents of" -ForegroundColor Yellow - Write-Host " $cudaExtras" -ForegroundColor Cyan - Write-Host " into:" -ForegroundColor Yellow - Write-Host " $vsCustomizations" -ForegroundColor Cyan + substep "could not copy CUDA VS integration files" "Yellow" + substep "The llama.cpp build may fail with 'No CUDA toolset found'." "Yellow" + substep "Manual fix: copy contents of" "Yellow" + substep "$cudaExtras" + substep "into:" "Yellow" + substep "$vsCustomizations" } } } @@ -850,16 +900,16 @@ if ($VsInstallPath -and $CudaToolkitRoot) { } step "cuda" $NvccPath -Write-Host " CUDA_PATH = $CudaToolkitRoot" -ForegroundColor Gray -Write-Host " CudaToolkitDir = $CudaToolkitRoot\" -ForegroundColor Gray +substep "CUDA_PATH = $CudaToolkitRoot" +substep "CudaToolkitDir = $CudaToolkitRoot\" # $CudaArch was detected earlier (before toolkit selection) so it could # influence which toolkit we picked. Just log the final state here. if (-not $CudaArch) { - Write-Host " [WARN] Could not detect compute capability -- cmake will use defaults" -ForegroundColor Yellow + substep "could not detect compute capability -- cmake will use defaults" "Yellow" } } else { - Write-Host "[SKIP] CUDA Toolkit -- no NVIDIA GPU detected" -ForegroundColor Yellow + step "cuda" "skipped (no NVIDIA GPU detected)" "Yellow" } # ============================================ @@ -885,18 +935,18 @@ if ($IsPipInstall) { ($NodeMajor -eq 22 -and $NodeMinor -ge 12) -or ($NodeMajor -ge 23) if ($NodeOk -and $NpmMajor -ge 11) { - Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green + substep "Node $NodeVersion and npm $NpmVersion already meet requirements." $NeedNode = $false } else { - Write-Host "[WARN] Node $NodeVersion / npm $NpmVersion too old." -ForegroundColor Yellow + substep "Node $NodeVersion / npm $NpmVersion too old." "Yellow" } } } catch { - Write-Host "[WARN] Node/npm not found." -ForegroundColor Yellow + substep "Node/npm not found." "Yellow" } if ($NeedNode) { - Write-Host "Installing Node.js LTS via winget..." -ForegroundColor Cyan + substep "installing Node.js LTS via winget..." try { winget install OpenJS.NodeJS.LTS --source winget --accept-package-agreements --accept-source-agreements Refresh-Environment @@ -912,19 +962,19 @@ if ($IsPipInstall) { # ── bun (optional, faster package installs) ── # Installed via npm — Node is already guaranteed above. Works on all platforms. if (-not (Get-Command bun -ErrorAction SilentlyContinue)) { - Write-Host " Installing bun (faster frontend package installs)..." -ForegroundColor DarkGray + substep "installing bun (faster frontend package installs)..." $prevEAP_bun = $ErrorActionPreference $ErrorActionPreference = "Continue" - npm install -g bun 2>&1 | Out-Null + Invoke-SetupCommand { npm install -g bun } | Out-Null $ErrorActionPreference = $prevEAP_bun Refresh-Environment if (Get-Command bun -ErrorAction SilentlyContinue) { - Write-Host "[OK] bun installed ($(bun --version))" -ForegroundColor Green + substep "bun installed ($(bun --version))" } else { - Write-Host "[OK] bun install skipped (npm will be used instead)" -ForegroundColor DarkGray + substep "bun install skipped (npm will be used instead)" } } else { - Write-Host "[OK] bun already installed ($(bun --version))" -ForegroundColor Green + substep "bun already installed ($(bun --version))" } } @@ -939,7 +989,7 @@ if ($HasPython) { if ($PyVer -match "(\d+)\.(\d+)") { $PyMajor = [int]$Matches[1]; $PyMinor = [int]$Matches[2] if ($PyMajor -eq 3 -and $PyMinor -ge 11 -and $PyMinor -lt 14) { - Write-Host "[OK] Python $PyVer" -ForegroundColor Green + substep "Python $PyVer" $PythonOk = $true } else { Write-Host "[ERROR] Python $PyVer is outside supported range (need >= 3.11 and < 3.14)." -ForegroundColor Red @@ -979,12 +1029,12 @@ if ($LASTEXITCODE -eq 0 -and $ScriptsDir -and (Test-Path $ScriptsDir)) { if (-not ($ProcessPathEntries | Where-Object { $_.TrimEnd('\') -eq $ScriptsDir })) { $env:PATH = "$ScriptsDir;$env:PATH" } - Write-Host " Persisted Python Scripts dir to user PATH: $ScriptsDir" -ForegroundColor Gray + substep "Persisted Python Scripts dir to user PATH: $ScriptsDir" } } Write-Host "" -Write-Host "--- System prerequisites ready ---" -ForegroundColor Green +step "system" "prerequisites ready" Write-Host "" # ========================================================================== @@ -1019,12 +1069,12 @@ if ($IsPipInstall) { $NeedFrontendBuild = $false step "frontend" "up to date" } else { - Write-Host "[INFO] Frontend source changed since last build -- rebuilding..." -ForegroundColor Yellow + substep "Frontend source changed since last build -- rebuilding..." "Yellow" } } if ($NeedFrontendBuild -and -not $IsPipInstall) { Write-Host "" - Write-Host "Building frontend..." -ForegroundColor Cyan + substep "building frontend..." # ── Tailwind v4 .gitignore workaround ── # Tailwind v4's oxide scanner respects .gitignore in parent directories. @@ -1041,7 +1091,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $hidden = "$gi._twbuild" Rename-Item -Path $gi -NewName (Split-Path $hidden -Leaf) -Force $HiddenGitignores += $gi - Write-Host " [INFO] Temporarily hiding $gi (venv .gitignore blocks Tailwind scanner)" -ForegroundColor DarkGray + substep "Temporarily hiding $gi (venv .gitignore blocks Tailwind scanner)" } } $WalkDir = Split-Path $WalkDir -Parent @@ -1061,8 +1111,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { # the cache + retry once before falling back to npm. if ($UseBun) { Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - & bun install *> $null - $bunExit = $LASTEXITCODE + $bunExit = Invoke-SetupCommand { bun install } # On Windows, .bin/ entries can be tsc, tsc.cmd, or tsc.ps1 $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") @@ -1073,9 +1122,8 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { if (Test-Path "node_modules") { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } - & bun pm cache rm *> $null - & bun install *> $null - $bunExit = $LASTEXITCODE + Invoke-SetupCommand { bun pm cache rm } | Out-Null + $bunExit = Invoke-SetupCommand { bun install } $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { @@ -1086,7 +1134,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $UseBun = $false } } else { - Write-Host " [WARN] bun install failed (exit $bunExit), falling back to npm" -ForegroundColor Yellow + substep "bun install failed (exit $bunExit), falling back to npm" "Yellow" if (Test-Path "node_modules") { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } @@ -1094,8 +1142,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } } if (-not $UseBun) { - & npm install *> $null - $npmExit = $LASTEXITCODE + $npmExit = Invoke-SetupCommand { npm install } if ($npmExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm @@ -1107,8 +1154,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } # Always use npm to run the build (Node runtime — avoids bun Windows runtime issues) - & npm run build *> $null - $buildExit = $LASTEXITCODE + $buildExit = Invoke-SetupCommand { npm run build } if ($buildExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm @@ -1135,27 +1181,27 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } if (Test-Path $OxcValidatorDir) { - Write-Host "Installing OXC validator runtime..." -ForegroundColor Cyan + substep "installing OXC validator runtime..." $prevEAP_oxc = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $OxcValidatorDir - npm install 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { + $oxcInstallExit = Invoke-SetupCommand { npm install } + if ($oxcInstallExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_oxc - Write-Host "[ERROR] OXC validator npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red + Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red exit 1 } Pop-Location $ErrorActionPreference = $prevEAP_oxc - Write-Host "[OK] OXC validator runtime installed" -ForegroundColor Green + step "oxc runtime" "installed" } # ========================================================================== # PHASE 3: Python environment + dependencies # ========================================================================== Write-Host "" -Write-Host "Setting up Python environment..." -ForegroundColor Cyan +substep "setting up Python environment..." # Find Python -- skip Anaconda/Miniconda distributions. # Conda-bundled CPython ships modified DLL search paths that break @@ -1215,7 +1261,7 @@ if (-not $PythonCmd) { if (-not $cmdInfo.Source) { continue } if ($cmdInfo.Source -like "*\WindowsApps\*") { continue } if (Test-IsConda $cmdInfo.Source) { - Write-Host " [SKIP] $($cmdInfo.Source) (conda Python breaks torch DLL loading)" -ForegroundColor Yellow + substep "skipping $($cmdInfo.Source) (conda Python breaks torch DLL loading)" "Yellow" continue } $ver = & $cmdInfo.Source --version 2>&1 @@ -1239,7 +1285,7 @@ if (-not $PythonCmd) { exit 1 } -Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green +substep "Using $PythonCmd ($(& $PythonCmd --version 2>&1))" # The venv must already exist (created by install.ps1). # This script (setup.ps1 / "unsloth studio update") only updates packages. @@ -1294,7 +1340,7 @@ if (Test-Path $VenvDir -PathType Container) { if ($shouldRebuild) { $reason = if ($installedTorchTag) { "torch $installedTorchTag != required $expectedTorchTag" } else { "torch could not be imported" } - Write-Host " [INFO] Stale venv detected ($reason) -- rebuilding..." -ForegroundColor Yellow + substep "Stale venv detected ($reason) -- rebuilding..." "Yellow" try { Remove-Item $VenvDir -Recurse -Force -ErrorAction Stop } catch { @@ -1311,7 +1357,7 @@ if (-not (Test-Path $VenvDir)) { Write-Host " irm https://unsloth.ai/install.ps1 | iex" -ForegroundColor Yellow exit 1 } else { - Write-Host " Reusing existing virtual environment at $VenvDir" -ForegroundColor Green + substep "reusing existing virtual environment at $VenvDir" } # pip and python write to stderr even on success (progress bars, warnings). @@ -1329,9 +1375,9 @@ $UseUv = $false if (Get-Command uv -ErrorAction SilentlyContinue) { $UseUv = $true } else { - Write-Host " Installing uv package manager..." -ForegroundColor Cyan + substep "installing uv package manager..." try { - powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 2>&1 | Out-Null + Invoke-SetupCommand { powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" } | Out-Null Refresh-Environment # Re-activate venv since Refresh-Environment rebuilds PATH from # registry and drops the venv's Scripts directory @@ -1396,7 +1442,11 @@ if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") { if (-not $SkipPythonDeps) { -Fast-Install --upgrade pip | Out-Null +if ($script:UnslothVerbose) { + Fast-Install --upgrade pip +} else { + Fast-Install --upgrade pip | Out-Null +} # Pre-install PyTorch with CUDA support. # On Windows, the default PyPI torch wheel is CPU-only. @@ -1411,7 +1461,7 @@ $TorchCacheDir = "C:\tc" if (-not (Test-Path $TorchCacheDir)) { New-Item -ItemType Directory -Path $TorchCacheDir -Force | Out-Null } $env:TORCHINDUCTOR_CACHE_DIR = $TorchCacheDir [Environment]::SetEnvironmentVariable('TORCHINDUCTOR_CACHE_DIR', $TorchCacheDir, 'User') -Write-Host "[OK] TORCHINDUCTOR_CACHE_DIR set to $TorchCacheDir (avoids MAX_PATH issues)" -ForegroundColor Green +substep "TORCHINDUCTOR_CACHE_DIR set to $TorchCacheDir (avoids MAX_PATH issues)" if ($HasNvidiaSmi) { $CuTag = Get-PytorchCudaTag @@ -1420,36 +1470,57 @@ if ($HasNvidiaSmi) { } if ($CuTag -eq "cpu") { - Write-Host " Installing PyTorch (CPU-only)..." -ForegroundColor Cyan - $output = Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/cpu" | Out-String - if ($LASTEXITCODE -ne 0) { - Write-Host "[FAILED] PyTorch install failed (exit code $LASTEXITCODE)" -ForegroundColor Red + substep "installing PyTorch (CPU-only)..." + if ($script:UnslothVerbose) { + Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/cpu" + $torchInstallExit = $LASTEXITCODE + $output = "" + } else { + $output = Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/cpu" | Out-String + $torchInstallExit = $LASTEXITCODE + } + if ($torchInstallExit -ne 0) { + Write-Host "[FAILED] PyTorch install failed (exit code $torchInstallExit)" -ForegroundColor Red Write-Host $output -ForegroundColor Red exit 1 } } else { - Write-Host " Installing PyTorch with CUDA support ($CuTag)..." -ForegroundColor Cyan - Write-Host " (This download is ~2.8 GB -- may take a few minutes)" -ForegroundColor Gray - $output = Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/$CuTag" | Out-String - if ($LASTEXITCODE -ne 0) { - Write-Host "[FAILED] PyTorch CUDA install failed (exit code $LASTEXITCODE)" -ForegroundColor Red + substep "installing PyTorch with CUDA support ($CuTag)..." + substep "(This download is ~2.8 GB -- may take a few minutes)" + if ($script:UnslothVerbose) { + Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/$CuTag" + $torchInstallExit = $LASTEXITCODE + $output = "" + } else { + $output = Fast-Install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/$CuTag" | Out-String + $torchInstallExit = $LASTEXITCODE + } + if ($torchInstallExit -ne 0) { + Write-Host "[FAILED] PyTorch CUDA install failed (exit code $torchInstallExit)" -ForegroundColor Red Write-Host $output -ForegroundColor Red exit 1 } # Install Triton for Windows (enables torch.compile -- without it training can hang) - Write-Host " Installing Triton for Windows..." -ForegroundColor Cyan - $output = Fast-Install "triton-windows<3.7" | Out-String - if ($LASTEXITCODE -ne 0) { - Write-Host "[WARN] Triton install failed -- torch.compile may not work" -ForegroundColor Yellow + substep "installing Triton for Windows..." + if ($script:UnslothVerbose) { + Fast-Install "triton-windows<3.7" + $tritonInstallExit = $LASTEXITCODE + $output = "" + } else { + $output = Fast-Install "triton-windows<3.7" | Out-String + $tritonInstallExit = $LASTEXITCODE + } + if ($tritonInstallExit -ne 0) { + substep "Triton install failed -- torch.compile may not work" "Yellow" Write-Host $output -ForegroundColor Yellow } else { - Write-Host "[OK] Triton for Windows installed (enables torch.compile)" -ForegroundColor Green + substep "Triton for Windows installed (enables torch.compile)" } } # Ordered heavy dependency installation -- shared cross-platform script -Write-Host " Running ordered dependency installation..." -ForegroundColor Cyan +substep "running ordered dependency installation..." python "$PSScriptRoot\install_python_stack.py" # Restore ErrorActionPreference after pip/python work $ErrorActionPreference = $prevEAP @@ -1459,15 +1530,22 @@ $ErrorActionPreference = $prevEAP # at runtime (slow, ~10-15s), we pre-install into a separate directory. # The training subprocess just prepends .venv_t5/ to sys.path -- instant switch. Write-Host "" -Write-Host " Pre-installing transformers 5.x for newer model support..." -ForegroundColor Cyan +substep "pre-installing transformers 5.x for newer model support..." $VenvT5Dir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv_t5" if (Test-Path $VenvT5Dir) { Remove-Item -Recurse -Force $VenvT5Dir } New-Item -ItemType Directory -Path $VenvT5Dir -Force | Out-Null $prevEAP_t5 = $ErrorActionPreference $ErrorActionPreference = "Continue" foreach ($pkg in @("transformers==5.3.0", "huggingface_hub==1.7.1", "hf_xet==1.4.2")) { - $output = Fast-Install --target $VenvT5Dir --no-deps $pkg | Out-String - if ($LASTEXITCODE -ne 0) { + if ($script:UnslothVerbose) { + Fast-Install --target $VenvT5Dir --no-deps $pkg + $t5PkgExit = $LASTEXITCODE + $output = "" + } else { + $output = Fast-Install --target $VenvT5Dir --no-deps $pkg | Out-String + $t5PkgExit = $LASTEXITCODE + } + if ($t5PkgExit -ne 0) { Write-Host "[FAIL] Could not install $pkg into .venv_t5/" -ForegroundColor Red Write-Host $output -ForegroundColor Red $ErrorActionPreference = $prevEAP_t5 @@ -1476,9 +1554,16 @@ foreach ($pkg in @("transformers==5.3.0", "huggingface_hub==1.7.1", "hf_xet==1.4 } # tiktoken is needed by Qwen-family tokenizers -- install with deps since # regex/requests may be missing on Windows -$output = Fast-Install --target $VenvT5Dir tiktoken | Out-String -if ($LASTEXITCODE -ne 0) { - Write-Host "[WARN] Could not install tiktoken into .venv_t5/ -- Qwen tokenizers may fail" -ForegroundColor Yellow +if ($script:UnslothVerbose) { + Fast-Install --target $VenvT5Dir tiktoken + $tiktokenInstallExit = $LASTEXITCODE + $output = "" +} else { + $output = Fast-Install --target $VenvT5Dir tiktoken | Out-String + $tiktokenInstallExit = $LASTEXITCODE +} +if ($tiktokenInstallExit -ne 0) { + substep "Could not install tiktoken into .venv_t5/ -- Qwen tokenizers may fail" "Yellow" } $ErrorActionPreference = $prevEAP_t5 step "transformers" "5.x pre-installed" @@ -1504,10 +1589,8 @@ $resolveExit = $LASTEXITCODE $ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" } if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) { Write-Host "" - Write-Host "[WARN] Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" -ForegroundColor Yellow - if ($resolveOutput) { - $resolveOutput | ForEach-Object { Write-Host $_ } - } + substep "Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" "Yellow" + Write-LlamaFailureLog -Output ($resolveOutput | Out-String) # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp. @@ -1537,20 +1620,20 @@ if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) { } Write-Host "" -Write-Host "Resolved llama.cpp release tag: $ResolvedLlamaTag" -ForegroundColor Gray +substep "Resolved llama.cpp release tag: $ResolvedLlamaTag" if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { Write-Host "" - Write-Host "[WARN] UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install" -ForegroundColor Yellow + substep "UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install" "Yellow" $NeedLlamaSourceBuild = $true } else { Write-Host "" - Write-Host "Installing prebuilt llama.cpp bundle (preferred path)..." -ForegroundColor Cyan + substep "installing prebuilt llama.cpp bundle (preferred path)..." if (Test-Path $LlamaCppDir) { - Write-Host "Existing llama.cpp install detected -- validating staged prebuilt update before replacement" -ForegroundColor Gray + substep "Existing llama.cpp install detected -- validating staged prebuilt update before replacement" } if ($SkipPrebuiltInstall) { - Write-Host "[WARN] Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build" -ForegroundColor Yellow + substep "Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build" "Yellow" } else { $prebuiltArgs = @( "$PSScriptRoot\install_llama_prebuilt.py", @@ -1563,17 +1646,28 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { } $prevEAPPrebuilt = $ErrorActionPreference $ErrorActionPreference = "Continue" - & python @prebuiltArgs - $prebuiltExit = $LASTEXITCODE + if ($script:UnslothVerbose) { + # Show live output in verbose mode while still capturing for error log + $prebuiltLog = Join-Path $env:TEMP "unsloth-prebuilt-$PID.log" + & python @prebuiltArgs 2>&1 | Tee-Object -FilePath $prebuiltLog | Out-Host + $prebuiltExit = $LASTEXITCODE + $prebuiltOutput = if (Test-Path $prebuiltLog) { Get-Content $prebuiltLog -Raw } else { "" } + Remove-Item $prebuiltLog -ErrorAction SilentlyContinue + } else { + $prebuiltOutput = & python @prebuiltArgs 2>&1 | Out-String + $prebuiltExit = $LASTEXITCODE + } $ErrorActionPreference = $prevEAPPrebuilt if ($prebuiltExit -eq 0) { step "llama.cpp" "prebuilt installed and validated" } else { + step "llama.cpp" "prebuilt install failed (continuing)" "Yellow" + Write-LlamaFailureLog -Output $prebuiltOutput if (Test-Path $LlamaCppDir) { - Write-Host "[WARN] Prebuilt update failed; existing install was restored or cleaned before source build fallback" -ForegroundColor Yellow + substep "Prebuilt update failed; existing install was restored or cleaned before source build fallback" "Yellow" } - Write-Host "[WARN] Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build" -ForegroundColor Yellow + substep "Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build" "Yellow" $NeedLlamaSourceBuild = $true } } @@ -1603,10 +1697,10 @@ if ($NeedLlamaSourceBuild) { if ($OpenSslRoot) { $OpenSslAvailable = $true - Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green + substep "OpenSSL dev found at $OpenSslRoot" } else { - Write-Host "" - Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan + Write-Host "" + substep "installing OpenSSL dev (for HTTPS in llama-server)..." $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements @@ -1615,17 +1709,17 @@ if ($NeedLlamaSourceBuild) { if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { $OpenSslRoot = $root $OpenSslAvailable = $true - Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green + substep "OpenSSL dev installed at $OpenSslRoot" break } } } if (-not $OpenSslAvailable) { - Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow + substep "OpenSSL dev not available -- llama-server will be built without HTTPS" "Yellow" } } } else { - Write-Host "[SKIP] OpenSSL dev install -- prebuilt llama.cpp already validated" -ForegroundColor Yellow + substep "OpenSSL dev install skipped -- prebuilt llama.cpp already validated" "Yellow" } # ========================================================================== @@ -1671,21 +1765,22 @@ if (-not $NeedLlamaSourceBuild) { Write-Host "" if (-not $HasNvidiaSmi) { # CPU-only machines depend entirely on llama-server for GGUF chat -- cmake is required - Write-Host "[ERROR] CMake is required to build llama-server for GGUF chat mode." -ForegroundColor Red - Write-Host " Install CMake from https://cmake.org/download/ and re-run setup." -ForegroundColor Yellow - exit 1 - } - Write-Host "[SKIP] llama-server build -- cmake not available" -ForegroundColor Yellow - Write-Host " GGUF inference and export will not be available." -ForegroundColor Yellow - Write-Host " Install CMake from https://cmake.org/download/ and re-run setup." -ForegroundColor Yellow + substep "CMake is required to build llama-server for GGUF chat mode." "Yellow" + substep "Continuing setup without llama.cpp build." "Yellow" + substep "Install CMake from https://cmake.org/download/ and re-run setup." "Yellow" + } + step "llama.cpp" "build skipped (cmake not available)" "Yellow" + substep "GGUF inference and export will not be available." "Yellow" + substep "Install CMake from https://cmake.org/download/ and re-run setup." "Yellow" + $script:LlamaCppDegraded = $true } else { Write-Host "" if ($HasNvidiaSmi) { - Write-Host "Building llama.cpp with CUDA support..." -ForegroundColor Cyan + substep "building llama.cpp with CUDA support..." } else { - Write-Host "Building llama.cpp (CPU-only, no NVIDIA GPU detected)..." -ForegroundColor Cyan + substep "building llama.cpp (CPU-only, no NVIDIA GPU detected)..." } - Write-Host " This typically takes 5-10 minutes on first build." -ForegroundColor Gray + substep "This typically takes 5-10 minutes on first build." Write-Host "" # Start total build timer @@ -1725,19 +1820,19 @@ if (-not $NeedLlamaSourceBuild) { if (Test-Path (Join-Path $LlamaCppDir ".git")) { Write-Host " Syncing llama.cpp to $ResolvedLlamaTag..." -ForegroundColor Gray if ($UseConcreteRef) { - git -C $LlamaCppDir fetch --depth 1 origin $ResolvedLlamaTag 2>&1 | Out-Null + $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin $ResolvedLlamaTag } } else { - git -C $LlamaCppDir fetch --depth 1 origin 2>&1 | Out-Null + $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin } } - if ($LASTEXITCODE -ne 0) { - Write-Host " [WARN] git fetch failed -- using existing source" -ForegroundColor Yellow + if ($gitFetchExit -ne 0) { + substep "git fetch failed -- using existing source" "Yellow" } else { - git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { + $gitCheckoutExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD } + if ($gitCheckoutExit -ne 0) { $BuildOk = $false $FailedStep = "git checkout" } else { - git -C $LlamaCppDir clean -fdx 2>&1 | Out-Null + Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir clean -fdx } | Out-Null } } } else { @@ -1749,8 +1844,8 @@ if (-not $NeedLlamaSourceBuild) { $cloneArgs += @("--branch", $ResolvedLlamaTag) } $cloneArgs += @("https://github.com/ggml-org/llama.cpp.git", $buildTmp) - git @cloneArgs 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { + $cloneExit = Invoke-SetupCommand -AlwaysQuiet { git @cloneArgs } + if ($cloneExit -ne 0) { $BuildOk = $false $FailedStep = "git clone" if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } @@ -1808,8 +1903,8 @@ if (-not $NeedLlamaSourceBuild) { $maxArch = Get-NvccMaxArch -NvccExe $NvccPath if ($maxArch) { $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$maxArch" - Write-Host " [WARN] GPU is sm_$CudaArch but nvcc only supports up to sm_$maxArch" -ForegroundColor Yellow - Write-Host " Building with sm_$maxArch (PTX will JIT for your GPU at runtime)" -ForegroundColor Yellow + substep "GPU is sm_$CudaArch but nvcc only supports up to sm_$maxArch" "Yellow" + substep "Building with sm_$maxArch (PTX will JIT for your GPU at runtime)" "Yellow" } # else: omit flag entirely, let cmake pick defaults } @@ -1819,10 +1914,11 @@ if (-not $NeedLlamaSourceBuild) { } $cmakeOutput = cmake @CmakeArgs 2>&1 | Out-String - if ($LASTEXITCODE -ne 0) { + $cmakeConfigureExit = $LASTEXITCODE + if ($cmakeConfigureExit -ne 0) { $BuildOk = $false $FailedStep = "cmake configure" - Write-Host $cmakeOutput -ForegroundColor Red + Write-LlamaFailureLog -Output $cmakeOutput if ($cmakeOutput -match 'No CUDA toolset found|CUDA_TOOLKIT_ROOT_DIR|nvcc') { Write-Host "" Write-Host " Hint: CUDA VS integration may be missing. Try running as admin:" -ForegroundColor Yellow @@ -1845,10 +1941,11 @@ if (-not $NeedLlamaSourceBuild) { Write-Host "" $output = cmake --build $BuildDir --config Release --target llama-server -j $NumCpu 2>&1 | Out-String - if ($LASTEXITCODE -ne 0) { + $cmakeBuildServerExit = $LASTEXITCODE + if ($cmakeBuildServerExit -ne 0) { $BuildOk = $false $FailedStep = "cmake build (llama-server)" - Write-Host $output -ForegroundColor Red + Write-LlamaFailureLog -Output $output } } @@ -1857,9 +1954,10 @@ if (-not $NeedLlamaSourceBuild) { Write-Host "" Write-Host "--- cmake build (llama-quantize) ---" -ForegroundColor Cyan $output = cmake --build $BuildDir --config Release --target llama-quantize -j $NumCpu 2>&1 | Out-String - if ($LASTEXITCODE -ne 0) { - Write-Host " [WARN] llama-quantize build failed (GGUF export may be unavailable)" -ForegroundColor Yellow - Write-Host $output -ForegroundColor Yellow + $cmakeBuildQuantizeExit = $LASTEXITCODE + if ($cmakeBuildQuantizeExit -ne 0) { + substep "llama-quantize build failed (GGUF export may be unavailable)" "Yellow" + Write-LlamaFailureLog -Output $output } } @@ -1900,9 +1998,9 @@ if (-not $NeedLlamaSourceBuild) { step "llama.cpp" "built" step "build time" "${totalMin}m ${totalSec}s" "DarkGray" } else { - step "llama.cpp" "build failed at: $FailedStep (${totalMin}m ${totalSec}s)" "Red" + step "llama.cpp" "build failed at: $FailedStep (${totalMin}m ${totalSec}s); continuing" "Yellow" substep "To retry: delete $LlamaCppDir and re-run setup." "Yellow" - exit 1 + $script:LlamaCppDegraded = $true } } } @@ -1913,12 +2011,28 @@ if (-not $NeedLlamaSourceBuild) { $DoneLabel = if ($env:SKIP_STUDIO_BASE -eq "1") { "Unsloth Studio Setup Complete" } else { "Unsloth Studio Updated" } if ($script:StudioVtOk -and -not $env:NO_COLOR) { Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) - Write-Host (" " + (Get-StudioAnsi Title) + $DoneLabel + (Get-StudioAnsi Reset)) + if ($script:LlamaCppDegraded) { + Write-Host (" " + (Get-StudioAnsi Warn) + "$DoneLabel (limited: llama.cpp unavailable)" + (Get-StudioAnsi Reset)) + } else { + Write-Host (" " + (Get-StudioAnsi Title) + $DoneLabel + (Get-StudioAnsi Reset)) + } Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) } else { Write-Host " $Rule" -ForegroundColor DarkGray - Write-Host " $DoneLabel" -ForegroundColor Green + if ($script:LlamaCppDegraded) { + Write-Host " $DoneLabel (limited: llama.cpp unavailable)" -ForegroundColor Yellow + } else { + Write-Host " $DoneLabel" -ForegroundColor Green + } Write-Host " $Rule" -ForegroundColor DarkGray } step "launch" "unsloth studio -H 0.0.0.0 -p 8888" Write-Host "" + +# Match studio/setup.sh: exit non-zero for degraded llama.cpp when called +# from install.ps1 (SKIP_STUDIO_BASE=1) so the installer can detect the +# failure. Direct 'unsloth studio update' does not set SKIP_STUDIO_BASE, +# so it keeps degraded installs successful. +if ($script:LlamaCppDegraded -and $env:SKIP_STUDIO_BASE -eq "1") { + exit 1 +} diff --git a/studio/setup.sh b/studio/setup.sh index 7502270276..c2a6891ec0 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -28,12 +28,43 @@ fi step() { printf " ${C_DIM}%-15.15s${C_RST}${3:-$C_OK}%s${C_RST}\n" "$1" "$2"; } substep() { printf " ${C_DIM}%-15s%s${C_RST}\n" "" "$1"; } +_is_verbose() { + [ "${UNSLOTH_VERBOSE:-0}" = "1" ] +} + +verbose_substep() { + if _is_verbose; then + substep "$1" + fi + return 0 +} + +run_maybe_quiet() { + if _is_verbose; then + "$@" + else + "$@" > /dev/null 2>&1 + fi +} + # ── Helper: run command quietly, show output only on failure ── _run_quiet() { local on_fail=$1 local label=$2 shift 2 + if _is_verbose; then + local exit_code + "$@" && return 0 + exit_code=$? + step "error" "$label failed (exit code $exit_code)" "$C_ERR" >&2 + if [ "$on_fail" = "exit" ]; then + exit "$exit_code" + else + return "$exit_code" + fi + fi + local tmplog tmplog=$(mktemp) || { step "error" "Failed to create temporary file" "$C_ERR" >&2 @@ -65,11 +96,18 @@ run_quiet_no_exit() { _run_quiet return "$@" } +print_llama_error_log() { + local log_file=$1 + [ -s "$log_file" ] || return 0 + substep "llama.cpp diagnostics (last 120 lines):" + tail -n 120 "$log_file" | sed 's/^/ | /' >&2 +} + # ── Banner ── echo "" printf " ${C_TITLE}%s${C_RST}\n" "🦥 Unsloth Studio Setup" printf " ${C_DIM}%s${C_RST}\n" "$RULE" - +verbose_substep "verbose diagnostics enabled" # ── Clean up stale caches ── rm -rf "$REPO_ROOT/unsloth_compiled_cache" rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache" @@ -97,6 +135,7 @@ fi if [ "$_NEED_FRONTEND_BUILD" = false ]; then step "frontend" "up to date" + verbose_substep "frontend dist is newer than source inputs" else # ── Node ── @@ -117,7 +156,7 @@ if command -v node &>/dev/null && command -v npm &>/dev/null; then # In Colab, just upgrade npm directly - nvm doesn't work well if [ "$NPM_MAJOR" -lt 11 ]; then substep "upgrading npm..." - npm install -g npm@latest > /dev/null 2>&1 + run_maybe_quiet npm install -g npm@latest fi NEED_NODE=false fi @@ -127,7 +166,11 @@ fi if [ "$NEED_NODE" = true ]; then substep "installing nvm..." export NODE_OPTIONS=--dns-result-order=ipv4first - curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash > /dev/null 2>&1 + if _is_verbose; then + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash + else + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash > /dev/null 2>&1 + fi export NVM_DIR="$HOME/.nvm" set +u @@ -141,7 +184,11 @@ if [ "$NEED_NODE" = true ]; then substep "installing Node LTS..." run_quiet "nvm install" nvm install --lts - nvm use --lts > /dev/null 2>&1 + if _is_verbose; then + nvm use --lts + else + nvm use --lts > /dev/null 2>&1 + fi set -u NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1) @@ -158,13 +205,14 @@ if [ "$NEED_NODE" = true ]; then fi step "node" "$(node -v) | npm $(npm -v)" +verbose_substep "node check: NEED_NODE=$NEED_NODE NODE_OK=${NODE_OK:-unknown} NPM_MAJOR=${NPM_MAJOR:-unknown}" # ── Install bun (optional, faster package installs) ── # Uses npm to install bun globally -- Node is already guaranteed above, # avoids platform-specific installers, PATH issues, and admin requirements. if ! command -v bun &>/dev/null; then substep "installing bun..." - if npm install -g bun > /dev/null 2>&1 && command -v bun &>/dev/null; then + if run_maybe_quiet npm install -g bun && command -v bun &>/dev/null; then substep "bun installed ($(bun --version))" else substep "bun install skipped (npm will be used instead)" @@ -228,21 +276,25 @@ _try_bun_install() { _bun_install_ok=false if command -v bun &>/dev/null; then - echo " Using bun for package install (faster)" + substep "using bun for package install (faster)" if _try_bun_install; then _bun_install_ok=true else # First attempt failed, likely due to corrupt cache entries. # Clear the cache and retry once. echo " Clearing bun cache and retrying..." - bun pm cache rm > /dev/null 2>&1 || true + run_maybe_quiet bun pm cache rm || true if _try_bun_install; then _bun_install_ok=true fi fi fi if [ "$_bun_install_ok" = false ]; then - run_quiet "npm install" npm install + run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error + _npm_install_rc=$? + if [ "$_npm_install_rc" -ne 0 ]; then + exit "$_npm_install_rc" + fi fi run_quiet "npm run build" npm run build @@ -265,7 +317,11 @@ fi # end frontend build check # ── oxc-validator runtime ── if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" - run_quiet "npm install (oxc validator runtime)" npm install + run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error + _oxc_install_rc=$? + if [ "$_oxc_install_rc" -ne 0 ]; then + exit "$_oxc_install_rc" + fi cd "$SCRIPT_DIR" fi @@ -287,9 +343,19 @@ if [ ! -x "$VENV_DIR/bin/python" ]; then # packages (huggingface-hub, datasets, transformers) and only pulls # in genuinely missing ones (structlog, fastapi, etc.). substep "Colab detected, installing Studio backend dependencies..." + _COLAB_REQS_TMP="$(mktemp)" sed 's/[><=!~;].*//' "$SCRIPT_DIR/backend/requirements/studio.txt" \ - | grep -v '^#' | grep -v '^$' \ - | pip install -q -r /dev/stdin 2>/dev/null || true + | grep -v '^#' | grep -v '^$' > "$_COLAB_REQS_TMP" + if [ -s "$_COLAB_REQS_TMP" ]; then + if ! run_quiet_no_exit "install Colab backend deps" pip install -q -r "$_COLAB_REQS_TMP"; then + rm -f "$_COLAB_REQS_TMP" + step "python" "Colab backend dependency install failed" "$C_ERR" + exit 1 + fi + else + step "python" "no Colab backend dependencies resolved from requirements file" "$C_WARN" + fi + rm -f "$_COLAB_REQS_TMP" _COLAB_NO_VENV=true else step "python" "venv not found at $VENV_DIR" "$C_ERR" @@ -308,7 +374,13 @@ install_python_stack() { USE_UV=false if command -v uv &>/dev/null; then USE_UV=true -elif curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1; then +elif { + if _is_verbose; then + curl -LsSf https://astral.sh/uv/install.sh | sh + else + curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1 + fi +}; then export PATH="$HOME/.local/bin:$PATH" command -v uv &>/dev/null && USE_UV=true fi @@ -325,7 +397,8 @@ cd "$SCRIPT_DIR" # On Colab without a venv, skip venv-dependent Python deps sections but # continue to llama.cpp install so GGUF inference is available. if [ "$_COLAB_NO_VENV" = true ]; then - echo "✅ Studio backend dependencies installed into system Python" + step "python" "backend deps installed into system Python" + substep "continuing to llama.cpp install for GGUF inference support" fi # ── Check if Python deps need updating ── @@ -375,6 +448,7 @@ if [ "$_SKIP_PYTHON_DEPS" = false ]; then step "transformers" "5.x pre-installed" else step "python" "dependencies up to date" + verbose_substep "python deps check: installed=$_PKG_NAME@${INSTALLED_VER:-unknown} latest=${LATEST_VER:-unknown}" fi # ── 7. Prefer prebuilt llama.cpp bundles before any source build path ── @@ -383,6 +457,7 @@ mkdir -p "$UNSLOTH_HOME" LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false +_LLAMA_CPP_DEGRADED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-latest}" _HELPER_RELEASE_REPO="${UNSLOTH_LLAMA_RELEASE_REPO:-unslothai/llama.cpp}" @@ -400,7 +475,7 @@ else fi if [ -z "$_RESOLVED_LLAMA_TAG" ]; then step "llama.cpp" "failed to resolve prebuilt tag via $_HELPER_RELEASE_REPO" "$C_WARN" - cat "$_RESOLVE_LLAMA_LOG" >&2 || true + print_llama_error_log "$_RESOLVE_LLAMA_LOG" set +e # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream @@ -426,6 +501,7 @@ fi rm -f "$_RESOLVE_LLAMA_LOG" substep "resolved llama.cpp tag: $_RESOLVED_LLAMA_TAG" +verbose_substep "requested llama.cpp tag: $_REQUESTED_LLAMA_TAG (repo: $_HELPER_RELEASE_REPO)" if [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then step "llama.cpp" "UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt" "$C_WARN" @@ -447,14 +523,25 @@ else if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then _PREBUILT_CMD+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") fi + _PREBUILT_LOG="$(mktemp)" set +e - "${_PREBUILT_CMD[@]}" - _PREBUILT_STATUS=$? + if _is_verbose; then + "${_PREBUILT_CMD[@]}" 2>&1 | tee "$_PREBUILT_LOG" + _PREBUILT_STATUS=${PIPESTATUS[0]} + else + "${_PREBUILT_CMD[@]}" >"$_PREBUILT_LOG" 2>&1 + _PREBUILT_STATUS=$? + fi set -e if [ "$_PREBUILT_STATUS" -eq 0 ]; then step "llama.cpp" "prebuilt installed and validated" + verbose_substep "llama.cpp install dir: $LLAMA_CPP_DIR" + rm -f "$_PREBUILT_LOG" else + step "llama.cpp" "prebuilt install failed (continuing)" "$C_WARN" + print_llama_error_log "$_PREBUILT_LOG" + rm -f "$_PREBUILT_LOG" if [ -d "$LLAMA_CPP_DIR" ]; then substep "prebuilt update failed; existing install restored" fi @@ -523,12 +610,15 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = false ]; then : elif [ "${_SKIP_GGUF_BUILD:-}" = true ]; then step "llama.cpp" "skipped (missing build deps)" "$C_WARN" + [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true else { if ! command -v cmake &>/dev/null; then step "llama.cpp" "skipped (cmake not found)" "$C_WARN" + [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true elif ! command -v git &>/dev/null; then step "llama.cpp" "skipped (git not found)" "$C_WARN" + [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true else BUILD_OK=true _CLONE_BRANCH_ARGS=() @@ -691,8 +781,10 @@ else [ -f "$LLAMA_CPP_DIR/llama-quantize" ] && step "llama-quantize" "built" elif [ "$BUILD_OK" = true ]; then step "llama.cpp" "binary not found after build" "$C_WARN" + _LLAMA_CPP_DEGRADED=true else step "llama.cpp" "build failed" "$C_ERR" + [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true fi fi } @@ -702,14 +794,35 @@ fi # end _SKIP_GGUF_BUILD check if [ "$IS_COLAB" = true ]; then echo "" printf " ${C_DIM}%s${C_RST}\n" "$RULE" - printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete" + if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Setup Complete (limited: llama.cpp unavailable)" + else + printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete" + fi printf " ${C_DIM}%s${C_RST}\n" "$RULE" substep "from colab import start" substep "start()" else printf " ${C_DIM}%s${C_RST}\n" "$RULE" - printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed" + if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Installed (limited: llama.cpp unavailable)" + else + printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed" + fi printf " ${C_DIM}%s${C_RST}\n" "$RULE" - printf " ${C_DIM}%-15s${C_OK}%s${C_RST}\n" "launch" "unsloth studio -H 0.0.0.0 -p 8888" + if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + printf " ${C_DIM}%-15s${C_WARN}%s${C_RST}\n" "launch" "unsloth studio -H 0.0.0.0 -p 8888" + else + printf " ${C_DIM}%-15s${C_OK}%s${C_RST}\n" "launch" "unsloth studio -H 0.0.0.0 -p 8888" + fi fi echo "" + +# When called from install.sh (SKIP_STUDIO_BASE=1), exit non-zero so the +# installer can report the GGUF failure after finishing PATH/shortcut setup. +# When called directly via 'unsloth studio update', keep the install +# successful -- the footer above already reports the limitation and Studio +# is still usable for non-GGUF workflows. +if [ "$_LLAMA_CPP_DEGRADED" = true ] && [ "${SKIP_STUDIO_BASE:-0}" = "1" ]; then + exit 1 +fi diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 135fa4c3bf..2fecb9d6b1 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -267,10 +267,7 @@ def setup( help = "Full pip/build output during setup for troubleshooting.", ), ): - """Deprecated: use 'unsloth studio update' or re-run install.sh.""" - typer.echo( - "Note: 'unsloth studio setup' is deprecated. Use 'unsloth studio update' or re-run install.sh." - ) + """Run Studio setup (called by install.ps1 / install.sh).""" _run_setup_script(verbose = verbose)