install: close six WoA/WSL review gaps in provisioning, shortcuts, uninstall

Review round on the Windows-on-ARM + NVIDIA WSL2 path; each item reproduced
against the live scripts before fixing.

provision_llama_cuda.sh now serializes with install_llama_prebuilt.py on the
same <parent>/.<name>.install.lock file (its filelock backend is flock(2), so
shell flock interoperates; append-mode open so the Python O_EXCL fallback's
PID file is never truncated). The detached background builder could otherwise
race an installer rerun or `unsloth studio update`, both of which mv/rm -rf
inside the llama.cpp dir. Losing the 2h wait exits 0: another provisioner is
already doing the job.

The step-0 early-skip trusted a co-located libggml-cuda.so alone, which
wrongly skips one case: an in-place rebuild interrupted after the .so links
but before llama-server relinks leaves new .so + old CPU server. A completion
stamp (build/bin/.unsloth-cuda-ok) written only after the script's own final
CUDA check closes that window; skip now requires ldd evidence or the stamp.
The rejected functional --list-devices probe stays rejected: the stamp does
not gamble thermals on an env-fragile probe.

The WSL shortcut skip (install.ps1 owns the canonical WoA .lnk) was only a
transient env var, so the first `unsloth studio update`, whose wsl.exe shim
carries no env into install.sh --shortcuts-only, recreated the duplicate
blank-icon shortcut. The skip is now also persisted as
/root/.unsloth/.skip-wsl-windows-shortcut, checked by install.sh and removed
with the install by both uninstallers.

--with-llama-cpp-dir (and UNSLOTH_LOCAL_LLAMA_CPP_DIR) were parsed but
silently ignored on the WSL fallback path, which builds its own llama.cpp
inside the distro. Reject with guidance (UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR
pin the WSL-side build), mirroring the --local reject.

uninstall.sh's Windows shortcut sweep only removed wsl.exe-target .lnks, so
the WoA fallback shortcuts (powershell.exe + launch-studio-wsl.ps1) survived
while their launcher dir was deleted, leaving dangling shortcuts. The
owner-matched cleanup now removes them first.

uninstall.ps1 swept every "Unsloth Studio (*.lnk" as legacy, but install.sh
creates exactly that per-distro name for current WSL installs, and the WSL
cleanup below only removes evidenced distros. The sweep now keeps a live
wsl.exe launcher whose distro is not in the same evidence set, so a surviving
WSL install keeps its shortcut; everything else is still swept.

Verified: bash -n on all three shell scripts, PowerShell AST parse on both
ps1 files, flock mutual-exclusion and stamp skip/rebuild decisions exercised
standalone, and the uninstall icon suites (sh + ps1) pass. The
test_install_host_defaults.sh failure pre-exists on the branch merge base.
This commit is contained in:
Daniel Han 2026-07-18 08:23:23 +00:00
commit 9ae5565803
5 changed files with 89 additions and 12 deletions

View file

@ -2111,6 +2111,13 @@ exit 0
if ($envOverride) {
substep "note: $envOverrideVar='$envOverride' is not used for the Windows-on-ARM WSL install -- Studio installs inside WSL at /root/.unsloth." "Yellow"
}
# --with-llama-cpp-dir names a Windows-side llama.cpp, but this install runs
# llama.cpp inside WSL2 and would silently ignore the user's explicit binary
# choice. Reject like --local and point at the supported WSL-side pins.
if ($WithLlamaCppDir -or $env:UNSLOTH_LOCAL_LLAMA_CPP_DIR) {
Restore-StudioVenvRollback
return (Exit-InstallFailure "--with-llama-cpp-dir / UNSLOTH_LOCAL_LLAMA_CPP_DIR can't be honored on Windows-on-ARM + NVIDIA: llama.cpp runs inside WSL2 and can't use a Windows path. Remove it, or pin the WSL-side build with UNSLOTH_LLAMA_TAG or UNSLOTH_LLAMA_PR instead." 1)
}
$wslReady = $false
if (Get-Command wsl.exe -ErrorAction SilentlyContinue) {
@ -2208,7 +2215,10 @@ exit 0
# install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a
# %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own
# WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank.
$_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; '
# Persist the skip as a marker file too: `unsloth studio update` reruns
# install.sh --shortcuts-only through the wsl.exe shim, which carries no env,
# so without the marker the first update would recreate the duplicate .lnk.
$_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; '
if ($_instRef -eq 'main') {
$wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh'
} else {

View file

@ -1234,7 +1234,8 @@ STUB_EOF
fi
_css_created=1
elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ]; then
elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ] \
&& [ ! -f "$HOME/.unsloth/.skip-wsl-windows-shortcut" ]; then
# ── WSL: create Windows Desktop and Start Menu shortcuts ──
# Detect current WSL distro for targeted shortcut
_css_distro="${WSL_DISTRO_NAME:-}"

View file

@ -399,17 +399,50 @@ function Uninstall-UnslothStudio {
}
# ── Remove desktop and Start Menu shortcuts ──
# Canonical name is "Unsloth Studio.lnk"; also sweep legacy distro-suffixed names
# ("Unsloth Studio (WSL - <distro>).lnk") left by pre-release dev builds.
# Canonical name is "Unsloth Studio.lnk". Distro-suffixed names
# ("Unsloth Studio (WSL - <distro>).lnk") belong to per-distro WSL installs, which
# the WSL-fallback section below only cleans for evidenced distros (env var,
# wsl-distro.txt marker, or the legacy ARM64 probe) -- scope this sweep to the
# same set so a surviving WSL install keeps its launcher. Anything that is not a
# live wsl.exe launcher (pre-release leftovers) is still swept.
_Step "Removing desktop and Start Menu shortcuts..."
$_scCands = @()
if ($env:UNSLOTH_WSL_DISTRO) { $_scCands += $env:UNSLOTH_WSL_DISTRO }
try {
if ($env:LOCALAPPDATA) {
$_scDf = Join-Path (Join-Path $env:LOCALAPPDATA "Unsloth") "wsl-distro.txt"
if (Test-Path -LiteralPath $_scDf) {
$_scRd = (Get-Content -LiteralPath $_scDf -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($_scRd -and $_scRd.Trim()) { $_scCands += $_scRd.Trim() }
}
}
} catch { }
if ((-not $_scCands) -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64')) {
$_scCands = @('Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian')
}
$_scWs = $null
try { $_scWs = New-Object -ComObject WScript.Shell } catch { }
$shortcutDirs = @()
try { $d = [Environment]::GetFolderPath("Desktop"); if ($d) { $shortcutDirs += $d } } catch { }
if ($env:APPDATA) { $shortcutDirs += (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs") }
foreach ($dir in $shortcutDirs) {
if (-not (Test-Path -LiteralPath $dir)) { continue }
_RemovePath (Join-Path $dir "Unsloth Studio.lnk")
Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue |
ForEach-Object { _RemovePath $_.FullName }
Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue | ForEach-Object {
$_scKeep = $false
if ($_scWs) {
try {
$_sc = $_scWs.CreateShortcut($_.FullName)
if ("$($_sc.TargetPath) $($_sc.Arguments)" -match "wsl\.exe") {
$_scD = $null
if ($_sc.Arguments -match '-d\s+"?([^"\s]+)"?') { $_scD = $Matches[1] }
elseif ($_.Name -match '^Unsloth Studio \(WSL - (.+)\)\.lnk$') { $_scD = $Matches[1] }
if ($_scD -and ($_scCands -notcontains $_scD)) { $_scKeep = $true }
}
} catch { }
}
if (-not $_scKeep) { _RemovePath $_.FullName }
}
}
# Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile
# disappears promptly instead of lingering stale (mirrors install.ps1's

View file

@ -320,6 +320,16 @@ case "$_os" in
$shim = (Join-Path $ud "bin").TrimEnd("\","/");
$up = [Environment]::GetEnvironmentVariable("Path","User");
if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") }
# The WoA-fallback shortcuts target powershell.exe + launch-studio-wsl.ps1
# (not wsl.exe), so the sweep above keeps them; remove them here before
# their launcher dir is deleted or they would dangle.
foreach ($d in $dirs) {
if (-not $d -or -not (Test-Path -LiteralPath $d)) { continue }
$l = Join-Path $d "Unsloth Studio.lnk";
if (Test-Path -LiteralPath $l) {
try { $sc2 = $ws.CreateShortcut($l); if ($sc2.Arguments -match "launch-studio-wsl\.ps1") { Remove-Item -LiteralPath $l -Force -ErrorAction SilentlyContinue } } catch { }
}
}
if (Test-Path -LiteralPath $ud) { Remove-Item -LiteralPath $ud -Recurse -Force -ErrorAction SilentlyContinue }
}
# Keep the shared icon while any Unsloth shortcut still uses it (native

View file

@ -13,6 +13,22 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}"
SERVER="$LLAMA_DIR/build/bin/llama-server"
log() { printf ' - %s\n' "$*"; }
# Serialize against install_llama_prebuilt.py (same lock file as its
# install_lock_path: <parent>/.<name>.install.lock; its filelock backend is
# flock(2), so this interoperates) and against a second copy of this script: the
# detached background builder can otherwise race an installer rerun or `unsloth
# studio update`, both of which mv/rm -rf inside $LLAMA_DIR. Append-mode open so
# the Python O_EXCL fallback's PID file is never truncated. 2h cap matches a
# worst-case source build; losing the wait means another provisioner is already
# doing this exact job, so exiting 0 is correct.
_LOCK_DIR="$(dirname "$LLAMA_DIR")"
mkdir -p "$_LOCK_DIR" 2>/dev/null
if command -v flock >/dev/null 2>&1; then
if exec 9>>"$_LOCK_DIR/.$(basename "$LLAMA_DIR").install.lock" 2>/dev/null; then
flock -w 7200 9 || { log "another llama.cpp install holds the lock; skipping"; exit 0; }
fi
fi
# Detect CUDA two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed
# libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship no
# libggml-cuda.so, so its presence is the reliable signal.
@ -23,18 +39,24 @@ is_cuda_server() {
return 1
}
# 0. Already provisioned? A co-located libggml-cuda.so* is the trusted signal: the
# prebuilt resolver validates the server it installs, and a *source* build that gets
# interrupted is caught by the build-failure wipe+rebuild below (section 6), so the
# early-skip can rely on the structural check. We deliberately do NOT run a functional
# 0. Already provisioned? Skip when the server links libggml-cuda directly (ldd)
# or when a co-located libggml-cuda.so* is paired with the completion stamp this
# script writes after its own final CUDA check. The stamp closes the one gap in
# the structural check: an in-place rebuild interrupted after libggml-cuda.so is
# linked but before llama-server relinks leaves new .so + old CPU server, which
# the bare .so test would wrongly skip. We deliberately do NOT run a functional
# `--list-devices` probe here: this script runs in a stripped-down detached shell whose
# loader path can miss /usr/lib/wsl/lib, so the CUDA backend may fail to enumerate even
# on a perfectly good server -- and a false negative would wipe a validated build and
# trigger a needless, thermally-dangerous source rebuild on the NVIDIA-ARM laptops this
# targets. Trust the .so; never gamble the machine's thermals on an env-fragile probe.
_CUDA_STAMP="$LLAMA_DIR/build/bin/.unsloth-cuda-ok"
if is_cuda_server "$SERVER"; then
log "CUDA llama-server already present: $SERVER"
exit 0
if ldd "$SERVER" 2>/dev/null | grep -qi 'libggml-cuda' || [ -e "$_CUDA_STAMP" ]; then
log "CUDA llama-server already present: $SERVER"
exit 0
fi
log "CUDA .so present but the build never stamped complete (interrupted relink?); rebuilding"
fi
# 1. Require an NVIDIA GPU (this script is only meaningful with one).
@ -259,6 +281,7 @@ _cmake_build_extras
_restore_build
if is_cuda_server "$SERVER"; then
: > "$_CUDA_STAMP" 2>/dev/null || true
log "CUDA llama-server ready: $SERVER"
[ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null
elif [ -x "$SERVER" ]; then