diff --git a/install.ps1 b/install.ps1 index ee28a14e5f..0d9d8e198d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -906,11 +906,34 @@ shell.Run cmd, 0, False } } + # ── Stage standalone launcher outside the venv ── + # Copying the distlib-generated unsloth.exe to $LOCALAPPDATA\Unsloth Studio\bin + # (and prepending that dir to User PATH) means future `unsloth studio update` + # invocations run from the standalone copy, so pip/uv can freely rewrite + # the venv's in-use Scripts\unsloth.exe during dependency upgrades. + $VenvScriptsExe = Join-Path $VenvDir "Scripts\unsloth.exe" + $StandaloneBinDir = Join-Path $env:LOCALAPPDATA "Unsloth Studio\bin" + $StandaloneExe = Join-Path $StandaloneBinDir "unsloth.exe" + if (Test-Path $VenvScriptsExe) { + if (-not (Test-Path $StandaloneBinDir)) { + New-Item -ItemType Directory -Force $StandaloneBinDir | Out-Null + } + try { Copy-Item -LiteralPath $VenvScriptsExe -Destination $StandaloneExe -Force } catch {} + $_userPath = [System.Environment]::GetEnvironmentVariable("Path", "User") + if (-not $_userPath -or $_userPath -notlike "*$StandaloneBinDir*") { + $_newPath = if ($_userPath) { "$StandaloneBinDir;$_userPath" } else { $StandaloneBinDir } + [System.Environment]::SetEnvironmentVariable("Path", $_newPath, "User") + Refresh-SessionPath + } + } + # ── Run studio setup ── # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. step "setup" "running unsloth studio setup..." - $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" + # Prefer the standalone launcher (so the running .exe is not inside the venv) + # and fall back to the venv copy if the staging step above did not succeed. + $UnslothExe = if (Test-Path $StandaloneExe) { $StandaloneExe } else { $VenvScriptsExe } if (-not (Test-Path $UnslothExe)) { Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 1ba41360c7..a5c88bd845 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1418,6 +1418,22 @@ $ErrorActionPreference = "Continue" $ActivateScript = Join-Path $VenvDir "Scripts\Activate.ps1" . $ActivateScript +# ── Standalone launcher locations (Windows in-use .exe lock workaround) ── +# Windows forbids DeleteFile on a mapped (running) .exe but permits +# MoveFileEx. Keeping the CLI launcher outside the venv means pip/uv +# never fights a lock when it reinstalls the unsloth wheel during update. +$StandaloneBinDir = Join-Path $env:LOCALAPPDATA "Unsloth Studio\bin" +$StandaloneExe = Join-Path $StandaloneBinDir "unsloth.exe" +$VenvExe = Join-Path $VenvDir "Scripts\unsloth.exe" + +# Sweep stale parked launchers left by prior migrations (safe: not in use) +foreach ($_dir in @((Join-Path $VenvDir "Scripts"), $StandaloneBinDir)) { + if (Test-Path $_dir) { + Get-ChildItem $_dir -Filter "unsloth.exe.old-*" -ErrorAction SilentlyContinue | + ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue } + } +} + # Try to use uv (much faster than pip), fall back to pip if unavailable $UseUv = $false if (Get-Command uv -ErrorAction SilentlyContinue) { @@ -1490,6 +1506,15 @@ if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") { if (-not $SkipPythonDeps) { +# Migration: if there is no standalone launcher yet AND install.ps1 is not +# the caller (SKIP_STUDIO_BASE != 1 => uv will reinstall the unsloth wheel), +# park the (possibly-running) venv launcher so uv's reinstall succeeds. +# Fresh-install flow leaves $VenvExe alone: install.ps1 creates the +# standalone copy before invoking setup, so this branch is a no-op there. +if (-not (Test-Path $StandaloneExe) -and (Test-Path $VenvExe) -and $env:SKIP_STUDIO_BASE -ne "1") { + try { Move-Item -LiteralPath $VenvExe -Destination "$VenvExe.old-$PID" -Force } catch {} +} + if ($script:UnslothVerbose) { Fast-Install --upgrade pip } else { @@ -1587,6 +1612,39 @@ if ($stackExit -ne 0) { $ErrorActionPreference = $prevEAP } +# ── Sync standalone launcher and prepend its dir to user PATH ── +# Runs unconditionally (both fast-path and reinstall branches) so that users +# on an old layout migrate on the first update where no standalone exists. +# Copy path uses park-and-replace (MoveFileEx) so even a currently-running +# $StandaloneExe can be swapped when distlib's launcher bytes actually change. +if (Test-Path $VenvExe) { + if (-not (Test-Path $StandaloneBinDir)) { + New-Item -ItemType Directory -Force $StandaloneBinDir | Out-Null + } + $needsCopy = -not (Test-Path $StandaloneExe) + if (-not $needsCopy) { + try { + $needsCopy = (Get-FileHash $VenvExe).Hash -ne (Get-FileHash $StandaloneExe).Hash + } catch { + $needsCopy = $true + } + } + if ($needsCopy) { + if (Test-Path $StandaloneExe) { + try { Move-Item -LiteralPath $StandaloneExe -Destination "$StandaloneExe.old-$PID" -Force } catch {} + } + try { Copy-Item -LiteralPath $VenvExe -Destination $StandaloneExe -Force } catch {} + } +} + +if (Test-Path $StandaloneExe) { + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not $userPath -or $userPath -notlike "*$StandaloneBinDir*") { + $newPath = if ($userPath) { "$StandaloneBinDir;$userPath" } else { $StandaloneBinDir } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + } +} + # ── Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/ ── # Runs outside the deps fast-path gate so that upgrades from the legacy # single .venv_t5 are always migrated to the tiered layout.