mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-09 15:58:41 +00:00
* Windows installer: fix DiskPart UAC, drive-root cache, spurious rename warning, CPU-base messaging
amd-smi gate (DiskPart UAC mid-install): the AMD torch wheel ships hipInfo.exe
inside the venv, and the bitsandbytes fix prepends that venv Scripts dir to PATH.
shutil.which("hipinfo") then found it and flipped _amd_smi_allowed() to True, so
the post-install AMD probe fell through to `amd-smi list` (the venv hipInfo failed
to report gcnArchName, which is why the arch came from the GPU-name table) and
amd-smi elevated, popping the DiskPart UAC. Fix: a hipinfo resolved inside the
active venv (sys.prefix) is the torch-wheel binary, not a HIP SDK, and must not
open the gate. Mirrored in install_python_stack.py, install_llama_prebuilt.py, and
backend utils/hardware/amd.py (the runtime VRAM poller had the same latent prompt).
TORCHINDUCTOR_CACHE_DIR: move from C:\tc to <StudioHome>\TORCHINDUCTOR_CACHE_DIR so
the inductor/Triton cache lives under the user's Studio home, not the system drive
root. Long paths are already enabled above so deep inductor paths still fit.
unsloth.exe rename: skip the rename (and its "pip may fail with WinError 32"
warning) when SKIP_STUDIO_BASE=1. In the install.ps1 flow base packages are not
reinstalled, so unsloth.exe is never rewritten; the self-rename only failed because
setup runs via unsloth.exe (the running launcher holds its own file). The
'studio update' flow still attempts it.
CPU PyTorch messaging: clarify that the CPU base is temporary and setup replaces it
with GPU ROCm wheels, and print an explicit "GPU ROCm PyTorch installed" line after
the AMD wheels land, so the log makes clear the final install is GPU-accelerated.
Adds two regression tests covering the venv-internal vs external hipInfo gate.
Verified end-to-end on a Strix Halo box (Radeon 8060S / gfx1151): install.ps1
--local from this branch completed exit 0 with no DiskPart prompt, no rename
warning, the cache under the Studio home, and "GPU ROCm PyTorch installed
(gfx1151)"; Studio then booted and detected "ROCm (HIP 7.13.99004) -- AMD Radeon
8060S Graphics".
* Windows installer: drop the unreliable unsloth.exe rename and its WinError 32 warning
setup.ps1 used to rename the running unsloth.exe out of the way before the
base-package upgrade so pip could replace it. That rename never actually
worked: setup runs *via* unsloth.exe, so renaming our own running
uv-trampoline launcher failed with a sharing violation (WinError 32) and only
printed a scary 'could not rename unsloth.exe; pip may fail with WinError 32'
warning on every Windows install and update.
It also was not needed. pip tolerates a running/locked console-script .exe: it
moves the old one aside and writes the new one. The base upgrade routes through
pip on Windows, so the upgrade succeeds (or, in the install.ps1 flow with
SKIP_STUDIO_BASE=1, the base is not touched at all) and unsloth.exe is left
intact either way.
Removing the rename block and its failed-install restore block removes the
false warning for all Windows devices in both the install and update flows.
* Windows installer: gate venv-internal hipInfo.exe in PowerShell amd-smi probe; harden venv path checks
Follow-up to PR #6296.
- install.ps1 and setup.ps1: ignore the AMD torch wheel hipInfo.exe that lives
inside the Studio venv when probing for a HIP SDK, so amd-smi no longer reopens
the DiskPart UAC during install/update. Mirrors _path_inside_venv in the Python
installers, which already do this.
- amd.py, install_llama_prebuilt.py, install_python_stack.py: normcase the venv
containment check (Windows paths are case-insensitive) and run the
HIP_PATH/ROCM_PATH candidate through it too.
- setup.ps1: fall back to a short TORCHINDUCTOR cache dir when long paths are
unavailable, and create the dir wildcard-safely.
- tests: isolate sys.prefix in the gate helper, add HIP_PATH/ROCM_PATH cases, and
assert the PowerShell venv exclusion.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Windows installer: install ROCm PyTorch directly for a known AMD arch
When the GPU arch is known (name-inferred from the GPU-name table) but ROCm
could not be probe-verified (no HIP SDK, no amd-smi), the bootstrap installed
a CPU PyTorch base that setup.ps1 then force-reinstalled as ROCm. The
repo.amd.com wheels bundle their own runtime (no HIP SDK required), which
setup.ps1 already relies on, so the CPU base was a pure wasted download/install.
- Gate the ROCm index on a known arch, not only on probe-verified ROCm, so a
mapped arch installs ROCm torch directly. Unmapped arches and no-GPU hosts
still get CPU (unchanged).
- Fall back to a CPU base if the ROCm-index install fails, so a transient
repo.amd.com outage does not abort the install (setup.ps1 retries ROCm).
- Correct the stale comment that claimed ROCm wheels need a confirmed HIP SDK.
- Add a regression test for the arch-based gate.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Windows installer: correct the unsloth.exe rename-removal comment
The comment claimed the base upgrade 'routes through pip on Windows' and that
pip 'moves the old unsloth.exe aside, then writes the new one'. That is not what
the code does. install_python_stack tries uv first; on a locked launcher uv
aborts and falls back to pip, but the pip fallback strips --upgrade-package and
base.txt lists only bare unsloth/unsloth-zoo, so pip finds them already
satisfied and no-ops. The running unsloth.exe is left intact at its current
version either way. Reword the comment to describe the real uv-first /
pip-fallback-no-op behavior. No functional change.
* Windows installer: close two gaps in the venv-internal hipinfo exclusion
Review follow-up. The amd-smi/DiskPart gate could still reopen in two cases:
- setup.ps1 ran the HIP probe long before $VenvDir is assigned, so without
VIRTUAL_ENV (the `unsloth studio update` path) $venvRoots was empty and the
venv-internal hipInfo.exe was not recognized. Seed the venv root from
UNSLOTH_SETUP_PYTHON and the default Studio home too (both installers).
- The HIP_PATH/ROCM_PATH candidate was accepted without the venv filter, so an
env var pointing into the venv (AMD wheel) still set $HipSdkInstalled. Run
Test-HipinfoIsVenvInternal on the candidate as well (both installers).
Extend the PS gate test to assert both. Both .ps1 parse clean; install tests
pass (the venv-internal / HIP probe coverage at 359 passed).
* Windows installer: correct the CPU-base message for arches with no ROCm wheels
After gating the ROCm index on a known arch, a mapped arch sets $ROCmIndexUrl
and installs ROCm directly, so it no longer reaches the "temporary CPU base"
branch. That branch is now reached only by a name-inferred arch with no ROCm
wheels (e.g. RDNA2 gfx103X), where setup.ps1 does NOT install ROCm. The old
text ("setup replaces it with GPU ROCm wheels ... the final install IS
GPU-accelerated") was therefore always wrong there. Say plainly that PyTorch
stays on CPU for this GPU.
* Windows installer: seed the venv-internal hipInfo check from a custom Studio home
Test-HipinfoIsVenvInternal seeded the venv root from VIRTUAL_ENV, VenvDir, the
setup python, and the default %USERPROFILE% path only. A standalone
`unsloth studio update` with a custom UNSLOTH_STUDIO_HOME (or STUDIO_HOME alias)
and none of those set would not recognize the venv hipInfo on PATH, reopening the
amd-smi/DiskPart gate. Seed the custom home too, in both installers, and assert
it in the gate test.
* Studio installer: resolve venv aliases and expand ~ in the hipInfo venv filter
Two review points on the amd-smi/DiskPart UAC gate:
1. _path_inside_venv compared os.path.abspath of sys.prefix and the hipInfo
path, which does not resolve symlinks, junctions, or 8.3 short names. A venv
reached through an aliased path then fails the check, so its bundled
hipInfo.exe is mistaken for an external HIP SDK and amd-smi runs (the
DiskPart prompt this fix exists to suppress). Switch to os.path.realpath in
all three copies (amd.py, install_llama_prebuilt.py, install_python_stack.py).
2. setup.ps1's early venv-internal hipInfo probe seeded the venv root from a
custom Studio home (UNSLOTH_STUDIO_HOME / STUDIO_HOME) without expanding a
leading ~, while the canonical resolver does. With a tilde form,
[IO.Path]::GetFullPath kept the literal ~ relative to cwd, so the custom-home
hipInfo escaped the filter and reopened the gate. Expand ~ in the probe the
same way as the resolver.
tests/studio/install/test_pr5940_followups.py: 30 passed (adds a symlink
realpath case and a setup.ps1 tilde-expansion guard).
* Studio installer: mirror the hipInfo venv filter and ROCm wheel pins into install.ps1
Follow-up review on the same install.ps1 paths:
1. install.ps1's venv-internal hipInfo probe (Test-HipinfoIsVenvInternal)
seeded the venv root from a custom Studio home without expanding a leading
~, unlike the canonical resolver and setup.ps1. A tilde form left
[IO.Path]::GetFullPath with the literal ~ (relative to cwd), so the
custom-home hipInfo escaped the filter and reopened the amd-smi/DiskPart
gate. Expand ~ in the probe, matching the setup.ps1 fix.
2. The AMD ROCm path installed torchvision/torchaudio bare while pinning torch
to below 2.12. AMD's per-arch index publishes the companions independently
and may ship torchvision 0.27 (for torch 2.12) before removing 0.26, so a
bare resolve can pick an ABI-incompatible set and fall back to CPU. Add
torchvision/torchaudio floor maps and pass the pinned specs, mirroring
setup.ps1 and install_python_stack.py.
3. The ROCm-to-CPU fallback torch install used Invoke-InstallCommand (no
retry), the only torch step in the file without it. Switch to
Invoke-InstallCommandRetry so the recovery path survives a transient index
failure.
tests/studio/install/test_pr5940_followups.py: 33 passed (parametrized tilde
check over both installers, a torch/companion floor-map parity test, and a
CPU-fallback retry guard).
* Studio installer: scan all PATH hipinfo so the venv copy can't shadow a real HIP SDK
The amd-smi HIP-SDK probe used shutil.which("hipinfo") / Get-Command hipinfo,
which return only the first hit on PATH. The AMD torch wheel ships hipInfo.exe
inside the venv and the bnb fix (plus the Studio backend) prepend the venv
Scripts dir to PATH, so that venv-internal copy lands first. When a real HIP SDK
hipinfo sits later on PATH with HIP_PATH/ROCM_PATH unset, the first-hit probe
stopped at the venv copy, treated it as "not a HIP SDK", and closed the amd-smi
gate -- AMD users in that PATH-only SDK setup lost amd-smi telemetry and could
fall back to CPU. Scan every PATH entry and keep the first hipinfo that is not
venv-internal; only the venv copy is ignored, so the UAC/DiskPart suppression is
unchanged.
Applied to all three Python copies (install_llama_prebuilt.py,
install_python_stack.py, backend/utils/hardware/amd.py) via a new
_external_hipinfo_on_path helper, and both PowerShell callers (install.ps1,
setup.ps1) now use Get-Command hipinfo -All filtered by Test-HipinfoIsVenvInternal.
tests/studio/install/test_pr5940_followups.py: 36 passed (real-PATH scan tests, a
shadow-regression test for the exact venv-first ordering, and a parity check that
every Python copy uses the scanning helper).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Studio uninstallers: fix leftovers (false "removed", shared icon, llama lock)
Auditing a dual native+WSL uninstall on a real device surfaced three leftovers:
1. uninstall.ps1 removed the data dir (which holds unsloth.ico) before the
shortcuts that reference that icon, so Explorer's icon cache briefly held it
open. Remove-Item -Recurse reported success yet left the locked file, and the
dir was never re-attempted, so it orphaned with a false "removed" log.
_RemovePath now verifies the path is actually gone (retrying transient locks)
and reports honestly, and the data dir is re-swept after the shortcuts go.
2. install.sh writes a shared unsloth.ico to %LOCALAPPDATA%\Unsloth Studio for
the WSL shortcut, but uninstall.sh never removed it, orphaning the icon (and
dir) after a WSL uninstall. uninstall.sh now drops that icon and the dir when
empty, in both the powershell.exe and drvfs-fallback paths.
3. ~/.unsloth/.llama.cpp.install.lock was never removed, so the rmdir of
~/.unsloth failed and the dir lingered. Both uninstallers now remove the lock.
Verified by running both uninstallers on a real dual install: device fully clean
(no install dirs, shortcuts, PATH/registry entries, shared icon, or lock left).
* install.sh: auto-route Strix Halo WSL to an existing Ubuntu 24.04
ROCm-on-WSL is the GPU runtime for Strix Halo and only targets Ubuntu
24.04. When the installer runs in a newer default distro (e.g. 26.04) it
cannot enable the GPU and silently falls back to CPU. If a 24.04 distro
already exists, re-run the install there and stop in the current one so the
GPU path is taken without the user having to know about the distro
requirement.
Runs before venv creation so the wrong distro is left untouched, guards
against re-route loops via UNSLOTH_WSL_REROUTED, leaves a working ROCm
distro alone (librocdxg present), and skips the GGUF-only / opt-out /
non-Strix cases. When no 24.04 distro exists we keep today's behaviour:
continue to CPU and print the `wsl --install Ubuntu-24.04` guidance, never
auto-downloading a distro.
Adds tests/sh/test_strixhalo_wsl_reroute.sh (hermetic: extracts the
function, rewrites its paths to fixtures, mocks wsl.exe) covering the full
decision matrix, wired into tests/run_all.sh.
* uninstall.ps1: keep shared unsloth.ico for a surviving WSL shortcut
A dual native+WSL install shares %LOCALAPPDATA%\Unsloth Studio\unsloth.ico:
install.sh points the WSL shortcut's icon there while the native install owns the
dir. The native uninstaller removed the whole dir unconditionally, so uninstalling
native while keeping WSL left the WSL shortcut with a blank icon. The old code only
avoided this when Explorer happened to hold the icon open, which is unreliable; on a
real dual install the dir was deleted and the WSL shortcut went blank.
_RemoveDataDirKeepingWslIcon now scans the Start Menu + Desktop for a surviving
"Unsloth Studio (WSL ...).lnk" and, if found, removes everything in the data dir
except unsloth.ico (keeping the dir) instead of deleting it; with no WSL shortcut it
removes the dir as before. uninstall.sh still drops the icon and the empty dir when
WSL itself is uninstalled, so every uninstall order ends clean.
Adds tests/studio/test_uninstall_dual_install_icon.ps1 (AST-extracts the helper and
runs it against a temp dir with controlled shortcut dirs) covering the dual,
native-only, empty, and missing-dir cases, wired into the windows-inference smoke
workflow. Verified on a real dual install: native uninstall now keeps unsloth.ico
and the WSL shortcut's icon stays intact.
* installer: condense AMD/ROCm code comments (no behavior change)
Tighten the comments added for the Strix Halo native+WSL installer work so
they are shorter and clearer without losing intent: the venv-internal hipInfo
amd-smi gate, the ROCm torch/companion floor maps, the WSL 24.04 reroute, and
the dual-install uninstall icon handling. Comment-only; code paths unchanged.
107 insertions, 166 deletions across 11 files.
* install.sh: run the Strix Halo WSL reroute before any STUDIO_HOME write
The reroute fired after mkdir -p "$STUDIO_HOME" and the legacy-venv migration,
so rerouting 26.04 -> 24.04 left an empty ~/.unsloth/studio stub in the origin
distro (and ran venv migration in the distro about to be abandoned). Move the
reroute ahead of the venv section so the origin distro is left untouched, matching
the function's own comment. Behavior is identical on every non-reroute path.
* installer: fix ROCm CPU-fallback, hipinfo gate edge cases, uninstall icon, WSL 22.04
- install.ps1: clear $ROCmIndexUrl/$ROCmTorchFloor after the CPU fallback so the
flavor-repair block does not retry the failed ROCm index and abort the install;
pin the ROCm companion specs ($visionSpec/$audioSpec) in the repair path too.
- install.ps1 + setup.ps1: skip a bare drive root in Test-HipinfoIsVenvInternal so a
non-venv UNSLOTH_SETUP_PYTHON does not match the whole drive; iterate
HIP_PATH/HIP_PATH_57/ROCM_PATH and take the first non-venv hipinfo.
- amd.py, install_llama_prebuilt.py, install_python_stack.py: strip surrounding
quotes from PATH entries before probing for hipinfo.
- install.sh: pipefail the WSL reroute curl|sh; do not reroute supported Ubuntu 22.04.
- uninstall.sh: keep the shared unsloth.ico while any Unsloth shortcut (native or
another WSL distro) still references it, in both the powershell and drvfs paths.
- tests: regression coverage for all of the above.
* installer: forward reroute options, guard ROCm bootstrap, harden hipinfo gate
- install.sh: forward the caller's --package/--python/--verbose/--tauri and a custom
UNSLOTH_STUDIO_HOME into the WSL reroute (was a bare default install); bail on
--local; run the reroute BEFORE dependency/uv install so the origin distro is left
untouched; set UNSLOTH_SKIP_ROCM_WSL_SETUP after a failed reroute so the later
ROCm-on-WSL bootstrap does not install into the unsupported origin distro.
- install.ps1 + setup.ps1: Get-Command hipinfo -CommandType Application so only real
executables match (not an alias/function named hipinfo).
- uninstall.ps1: guard $env:APPDATA when building the default shortcut search dirs.
- tests: cover option forwarding, --local bail, the bootstrap guard, and the gate change.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* installer: guard origin ROCm bootstrap on every CPU-only fallback; harden ~ expansion
WSL reroute: the no-wsl.exe, no-24.04-target and --local fallbacks all tell the
user the install continues CPU-only, but only the failed-reroute branch set
UNSLOTH_SKIP_ROCM_WSL_SETUP=1. The later _maybe_bootstrap_rocm_wsl gate keys off
that flag, so the other three branches could still install ROCm into the
unsupported origin distro (e.g. 26.04). Set the skip guard on all of them.
Forward UNSLOTH_ROCM_WSL_AUTO into the reroute so a Tauri/consented GPU bootstrap
carries through to the rerouted 24.04 child instead of dropping to the prompt path.
install.ps1/setup.ps1: guard the venv-probe ~ expansion on a non-empty
$env:USERPROFILE so Join-Path does not throw on a profile-less service account.
Tests: add no-wsl.exe and UNSLOTH_ROCM_WSL_AUTO reroute cases, the USERPROFILE
guard assertion, and route shell-test fixtures through a single trap-cleaned root.
* installer: pin + soften Windows ROCm Python repair, reroute to 22.04, harden gates
install_python_stack.py: the Windows AMD ROCm repair in _ensure_rocm_torch()
installed bare torch/torchvision/torchaudio via the fatal pip_install -- the same
asymmetry already fixed on the PowerShell side. A transient repo.amd.com failure
could abort the whole install even after install.ps1/setup.ps1 fell back to CPU.
Pin companions per-arch (gfx120X/Strix -> the rocm7.2 trio, mirroring the PS floor
maps) and make the retry nonfatal: keep the existing build and let the user re-run
update to retry ROCm, so the chain install.ps1 -> setup.ps1 -> stack stays CPU-safe.
install.sh: reroute now targets an installed Ubuntu 24.04 OR 22.04 (24.04 preferred);
both are AMD-supported for ROCm-on-WSL, matching the leave-alone set, so a box with
only 22.04 reaches the GPU instead of staying CPU-only.
install.ps1/setup.ps1: a bare ~ for UNSLOTH_STUDIO_HOME left an empty Join-Path child
(PS 5.1 throws); fall back to USERPROFILE directly and only join a real remainder.
_path_inside_venv (amd.py + both installers): guard a root-dir sys.prefix so commonpath
can't classify every path on the drive as venv-internal (defensive; venv never at root).
uninstall.sh: guard an empty LOCALAPPDATA in the PS-interop icon cleanup (mirror APPDATA).
Tests: add 22.04-target reroute cases, Windows ROCm pin+nonfatal coverage (text +
behavioral), root-dir guard coverage, and bare-~/LOCALAPPDATA guard assertions.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* install.sh: match WSL reroute target by exact distro name, not substring
The 24.04/22.04 reroute target was chosen with grep -F (substring), so a custom
distro such as 'Ubuntu-24.04-test' (with no exact Ubuntu-24.04) was picked as the
target; the later 'wsl -d Ubuntu-24.04' then fails and the Strix Halo install stays
CPU-only. Match whole lines (grep -ixF) and reuse the matched name so only a real
Ubuntu-24.04/22.04 is targeted. Adds substring-rejection + exact-vs-custom tests.
* install.sh: keep the WSL reroute target to Ubuntu 24.04 (helper-supported only)
The ROCm-on-WSL bootstrap (scripts/install_rocm_wsl_strixhalo.sh) dies on any
VERSION_ID other than 24.04 and pins the noble repo, so treating 22.04 as
GPU-supported let the parent report a successful reroute while the child fell
back to CPU. Drop 22.04 from the supported set and the reroute target list;
24.04 stays the sole target (keeping the exact whole-line distro match). An
already-working ROCm on any other version is still left alone by the librocdxg
check above.
tests: reroute 22.04 cases updated to the 24.04-only behavior; make the
"no wsl.exe" case hermetic so a real host wsl.exe can't leak in on dev boxes;
stop the tauri exit-order check from mis-flagging the reroute helper's
[ "$TAURI_MODE" = true ] && ... --tauri one-liner.
* installer: tighten comment wording across the Strix Halo install/uninstall paths
Condense the verbose multi-line comment blocks (amd-smi hipinfo gate, ROCm
torch install + CPU fallback, WSL reroute, uninstall icon-keep) into fewer,
clearer lines. Comments and a few docstrings only; no code, logic, or
behavior change. Verified with bash -n, the PowerShell parser, and ast.parse,
and the installer test suite still passes.
* add AGPL-3.0 SPDX headers to the .sh/.ps1 scripts missing them
Every shell and PowerShell script under the Studio/installer surface now
carries the standard SPDX-License-Identifier: AGPL-3.0-only + copyright
header (after the shebang where present): the installer (install.sh,
install.ps1), build.sh, the .github and src-tauri scripts, the installer
test suite, and the moe kernel test. Header-only, line endings preserved;
bash -n, the PowerShell parser, and the installer tests all pass.
* installer: drop the duplicate AGPL header from install.sh and install.ps1
Both already carry an SPDX-License-Identifier: AGPL-3.0-only header below
their usage comment block; the prior header pass added a second one at the
top because it only scanned the first few lines. Remove the duplicate so each
file keeps a single original header.
* installer: force-reinstall CPU fallback torch; propagate Tauri NEED_SUDO from reroute
install.ps1/setup.ps1: when the AMD ROCm wheel install fails and we fall back to a
CPU base, force-reinstall the torch/vision/audio triplet. A failed ROCm install can
leave an unpinned ROCm torch (e.g. 2.10.0+rocm on gfx110X/gfx90a) that still
satisfies the CPU torch>=2.4,<2.11.0 range, so without --force-reinstall uv keeps the
ROCm build and only swaps the companions -- a mismatched venv the flavor-repair block
won't fix. setup.ps1 scopes the forced reinstall to the ROCm-fallback path
() so the genuine CPU-only install stays fast.
install.sh: the Strix Halo WSL reroute treated every nonzero child exit as a reroute
failure and fell back to CPU. In --tauri mode the child uses exit 2 ([TAURI:NEED_SUDO])
to ask the desktop app to elevate for the target distro; capture the child's exit code
and propagate exit 2 in Tauri mode (the child already printed the NEED_SUDO line)
instead of masking it. CLI mode still falls back to CPU on a generic failure.
Tests: reroute Tauri exit-2 propagation (and non-Tauri CPU-fallback) cases;
run_func now preserves the child exit code; force-reinstall assertions for both
PowerShell installers.
Note: codex's _rr_q apostrophe finding is a false positive -- the helper already
emits POSIX-correct 'O'\''Brien' and round-trips under both sh and bash.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* setup.ps1: fix $cpuForce array collapse in the ROCm->CPU torch fallback
An if-expression assignment ($cpuForce = if ($ROCmCpuFallback) { @("--force-reinstall") })
collapses the single-element array to a scalar string, so @cpuForce splatting enumerated
it character-by-character into broken single-letter args (- - f o r c e ...), which made
uv/pip reject the install and aborted the whole Studio setup on the AMD ROCm->CPU fallback
path. Build $cpuForce as a real array assigned outside the if-expression so the splat passes
a single --force-reinstall arg. Genuine CPU-only installs stay fast (empty array, no flag).
Test now asserts the array-build form and rejects the if-expression form.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* uninstall: remove the isolated Node.js runtime (~/.unsloth/node)
The isolated Node.js runtime (install_node_prebuilt.py, added with the managed-Node
change) installs to ~/.unsloth/node in default mode -- a sibling of studio, so deleting
<studio> leaves it behind (~200MB orphaned after uninstall). Both uninstallers already
remove the other default-mode siblings (llama.cpp/.cache/.staging); add node alongside
them. uninstall.ps1 also adds it to the handle-lock sweep so a held node.exe can't block
the delete. Env/custom mode nests node under the custom root, removed with that root.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
496 lines
24 KiB
PowerShell
496 lines
24 KiB
PowerShell
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
#
|
|
# Unsloth Studio uninstaller for Windows PowerShell.
|
|
# Stops running servers and removes install dir, launcher data, CLI shim,
|
|
# desktop and Start Menu shortcuts, the user PATH entry, and the PathBackup
|
|
# registry key. Honors custom roots set via UNSLOTH_STUDIO_HOME / STUDIO_HOME
|
|
# at install time (read back from share\studio.conf).
|
|
#
|
|
# Usage: irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex
|
|
# Local: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass; .\scripts\uninstall.ps1
|
|
|
|
function Uninstall-UnslothStudio {
|
|
$ErrorActionPreference = "Continue"
|
|
|
|
function _Step { param([string]$Msg) Write-Host $Msg }
|
|
function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color }
|
|
|
|
# Remove a file/dir/symlink if present. Idempotent; retries since a just-killed
|
|
# process can briefly hold a handle (Windows refuses the delete until released).
|
|
function _RemovePath {
|
|
param([string]$Path)
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return }
|
|
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
for ($attempt = 1; $attempt -le 4; $attempt++) {
|
|
try {
|
|
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
|
|
} catch {
|
|
if ($attempt -lt 4) { Start-Sleep -Milliseconds 700; continue }
|
|
_Substep "could not remove: $Path ($($_.Exception.Message))" "Yellow"
|
|
return
|
|
}
|
|
# Remove-Item -Recurse can report success yet leave a transiently-locked
|
|
# child (e.g. unsloth.ico in Explorer's icon cache); verify + retry so we
|
|
# never falsely claim "removed" or orphan the dir.
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
_Substep "removed: $Path" "Green"
|
|
return
|
|
}
|
|
if ($attempt -lt 4) { Start-Sleep -Milliseconds 700; continue }
|
|
_Substep "still present (files held open): $Path" "Yellow"
|
|
}
|
|
}
|
|
|
|
# Remove the shared data dir, but keep unsloth.ico if a WSL shortcut still points
|
|
# at it (else that shortcut blanks); uninstall.sh drops it when WSL is removed.
|
|
function _RemoveDataDirKeepingWslIcon {
|
|
param(
|
|
[string]$DataDir,
|
|
# WSL-shortcut search dirs; default Start Menu + Desktop, overridable for tests.
|
|
[string[]]$ShortcutDirs = $null
|
|
)
|
|
if ([string]::IsNullOrWhiteSpace($DataDir)) { return }
|
|
if (-not (Test-Path -LiteralPath $DataDir)) { return }
|
|
# $null = not passed (use defaults); test $null not truthiness so an explicit
|
|
# @() is honored (-not @() is $true).
|
|
if ($null -eq $ShortcutDirs) {
|
|
# Guard $env:APPDATA: it can be unset in service/CI Windows contexts, where
|
|
# an unguarded Join-Path emits a noisy parameter-binding error.
|
|
$ShortcutDirs = @()
|
|
if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) {
|
|
$ShortcutDirs += Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"
|
|
}
|
|
try {
|
|
$desktop = [Environment]::GetFolderPath("Desktop")
|
|
if (-not [string]::IsNullOrWhiteSpace($desktop)) { $ShortcutDirs += $desktop }
|
|
} catch {}
|
|
}
|
|
$wslShortcuts = @()
|
|
foreach ($d in $ShortcutDirs) {
|
|
if ($d -and (Test-Path -LiteralPath $d)) {
|
|
$wslShortcuts += Get-ChildItem -LiteralPath $d -Filter "Unsloth Studio (WSL*.lnk" -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
if (@($wslShortcuts).Count -eq 0) {
|
|
_RemovePath $DataDir
|
|
return
|
|
}
|
|
# A WSL shortcut survives: drop everything except its shared icon.
|
|
_Substep "keeping $(Join-Path $DataDir 'unsloth.ico') for the WSL shortcut" "Gray"
|
|
Get-ChildItem -LiteralPath $DataDir -Force -ErrorAction SilentlyContinue | ForEach-Object {
|
|
if ($_.Name -ne "unsloth.ico") { _RemovePath $_.FullName }
|
|
}
|
|
}
|
|
|
|
# A path is a Studio-owned root iff one of install.ps1's sentinels exists:
|
|
# <root>\share\studio.conf, <root>\unsloth_studio\.unsloth-studio-owned,
|
|
# or <root>\bin\unsloth.exe.
|
|
function _IsStudioRoot {
|
|
param([string]$Path)
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
|
if (Test-Path -LiteralPath (Join-Path $Path "share\studio.conf") -PathType Leaf) { return $true }
|
|
if (Test-Path -LiteralPath (Join-Path $Path "unsloth_studio\.unsloth-studio-owned") -PathType Leaf) { return $true }
|
|
if (Test-Path -LiteralPath (Join-Path $Path "bin\unsloth.exe") -PathType Leaf) { return $true }
|
|
return $false
|
|
}
|
|
|
|
# Hard deny list. Refuse to recursively delete drive roots, USERPROFILE
|
|
# itself, parent of USERPROFILE, or system directories.
|
|
function _IsUnsafeRoot {
|
|
param([string]$Path)
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $true }
|
|
$norm = $null
|
|
try { $norm = [System.IO.Path]::GetFullPath($Path).TrimEnd('\','/') } catch { return $true }
|
|
if ([string]::IsNullOrWhiteSpace($norm)) { return $true }
|
|
# Drive root, e.g. C:\
|
|
if ($norm -match '^[A-Za-z]:[\\/]?$') { return $true }
|
|
$userProfile = $env:USERPROFILE
|
|
if ($userProfile) {
|
|
$userProfile = $userProfile.TrimEnd('\','/')
|
|
if ($norm -ieq $userProfile) { return $true }
|
|
try {
|
|
$parent = Split-Path -LiteralPath $userProfile -Parent
|
|
if ($parent -and ($norm -ieq $parent.TrimEnd('\','/'))) { return $true }
|
|
} catch { }
|
|
}
|
|
$systemRoots = @(
|
|
$env:SystemRoot, $env:windir, $env:ProgramFiles, ${env:ProgramFiles(x86)},
|
|
$env:ProgramData, $env:APPDATA, $env:LOCALAPPDATA
|
|
)
|
|
foreach ($s in $systemRoots) {
|
|
if (-not [string]::IsNullOrWhiteSpace($s)) {
|
|
$s2 = $s.TrimEnd('\','/')
|
|
if ($norm -ieq $s2) { return $true }
|
|
}
|
|
}
|
|
return $false
|
|
}
|
|
|
|
# Parse UNSLOTH_EXE='<path>' out of a share\studio.conf and return the
|
|
# implied install root (three dirnames up from the venv exe).
|
|
function _RootFromConf {
|
|
param([string]$ConfFile)
|
|
if (-not (Test-Path -LiteralPath $ConfFile -PathType Leaf)) { return $null }
|
|
$line = Get-Content -LiteralPath $ConfFile -ErrorAction SilentlyContinue |
|
|
Where-Object { $_ -match "^UNSLOTH_EXE\s*=" } | Select-Object -First 1
|
|
if (-not $line) { return $null }
|
|
# Tolerate ' value ' single-quoted with '' -> ' apostrophe escape.
|
|
if ($line -match "^UNSLOTH_EXE\s*=\s*'(.*)'\s*$") {
|
|
$exe = $Matches[1] -replace "''", "'"
|
|
try {
|
|
$bin = Split-Path -LiteralPath $exe -Parent
|
|
$studio = Split-Path -LiteralPath $bin -Parent
|
|
$root = Split-Path -LiteralPath $studio -Parent
|
|
if ($root) { return $root }
|
|
} catch { }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# Expand a leading ~ or ~/ ~\ to $env:USERPROFILE so env-mode roots
|
|
# written with the tilde shape install.ps1 supports (lines 152-154) are
|
|
# found here too.
|
|
function _ExpandTilde {
|
|
param([string]$Path)
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $Path }
|
|
$p = $Path.Trim()
|
|
if ($p -eq '~') { return $env:USERPROFILE }
|
|
if ($p.StartsWith('~/') -or $p.StartsWith('~\')) {
|
|
if ($env:USERPROFILE) {
|
|
return (Join-Path $env:USERPROFILE $p.Substring(2).TrimStart('/','\'))
|
|
}
|
|
}
|
|
return $p
|
|
}
|
|
|
|
# Discover non-default Studio roots from env vars + studio.conf files.
|
|
# Mirrors install.ps1's precedence: UNSLOTH_STUDIO_HOME wins, STUDIO_HOME
|
|
# is ignored when both are set, so uninstalling install A doesn't also
|
|
# delete install B if the user has a stale STUDIO_HOME pointing at B.
|
|
function _CustomStudioRoots {
|
|
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
$defaultRoot = $null
|
|
if ($env:USERPROFILE) {
|
|
$defaultRoot = (Join-Path $env:USERPROFILE ".unsloth\studio")
|
|
}
|
|
|
|
$emit = {
|
|
param($Path)
|
|
if ([string]::IsNullOrWhiteSpace($Path)) { return }
|
|
$expanded = _ExpandTilde $Path
|
|
$norm = $null
|
|
try { $norm = [System.IO.Path]::GetFullPath($expanded).TrimEnd('\','/') } catch { return }
|
|
if (-not $norm) { return }
|
|
if ($defaultRoot -and ($norm -ieq $defaultRoot.TrimEnd('\','/'))) { return }
|
|
if ($seen.Add($norm)) { Write-Output $norm }
|
|
}
|
|
|
|
$envRoot = $null
|
|
if ($env:UNSLOTH_STUDIO_HOME) {
|
|
$envRoot = $env:UNSLOTH_STUDIO_HOME
|
|
} elseif ($env:STUDIO_HOME) {
|
|
$envRoot = $env:STUDIO_HOME
|
|
}
|
|
if ($envRoot) {
|
|
$expandedEnv = _ExpandTilde $envRoot
|
|
& $emit $expandedEnv
|
|
$confRoot = _RootFromConf (Join-Path $expandedEnv "share\studio.conf")
|
|
if ($confRoot) { & $emit $confRoot }
|
|
}
|
|
# Default-mode conf at LOCALAPPDATA\Unsloth Studio.
|
|
if ($env:LOCALAPPDATA) {
|
|
$confRoot = _RootFromConf (Join-Path $env:LOCALAPPDATA "Unsloth Studio\studio.conf")
|
|
if ($confRoot) { & $emit $confRoot }
|
|
}
|
|
}
|
|
|
|
# Return $true iff the PID's image path lives under one of $KnownRoots.
|
|
# Prevents killing an unrelated process that happens to listen on a stale
|
|
# Studio port.
|
|
function _PidUnderKnownRoot {
|
|
param([int]$Pid_, [string[]]$KnownRoots)
|
|
if (-not $KnownRoots -or $KnownRoots.Count -eq 0) { return $false }
|
|
try {
|
|
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$Pid_" -ErrorAction SilentlyContinue
|
|
if (-not $proc) { return $false }
|
|
$exe = $proc.ExecutablePath
|
|
if (-not $exe) { return $false }
|
|
foreach ($r in $KnownRoots) {
|
|
if ($r -and ($exe -ilike "$r\*")) { return $true }
|
|
}
|
|
} catch { }
|
|
return $false
|
|
}
|
|
|
|
# Stop a Studio backend whose port is recorded in <DataDir>\studio.port.
|
|
# Only kills if the listening PID's exe path is under a known Studio root.
|
|
function _StopByPortFile {
|
|
param([string]$PortFile, [string[]]$KnownRoots)
|
|
if (-not (Test-Path -LiteralPath $PortFile -PathType Leaf)) { return }
|
|
$port = Get-Content -LiteralPath $PortFile -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($port) { $port = $port.Trim() }
|
|
if (-not ($port -match '^[0-9]+$')) {
|
|
Remove-Item -LiteralPath $PortFile -Force -ErrorAction SilentlyContinue
|
|
return
|
|
}
|
|
try {
|
|
$conns = Get-NetTCPConnection -State Listen -LocalPort ([int]$port) -ErrorAction SilentlyContinue
|
|
foreach ($c in $conns) {
|
|
if (-not (_PidUnderKnownRoot -Pid_ ([int]$c.OwningProcess) -KnownRoots $KnownRoots)) { continue }
|
|
try {
|
|
Stop-Process -Id $c.OwningProcess -Force -ErrorAction SilentlyContinue
|
|
} catch { }
|
|
}
|
|
} catch {
|
|
# netstat fallback for older PowerShell. Require LISTENING state so
|
|
# we never kill a process whose remote endpoint just happens to be
|
|
# the cached port (browser -> :443 etc.).
|
|
try {
|
|
$lines = & netstat.exe -ano 2>$null |
|
|
Select-String -Pattern "LISTENING" |
|
|
Select-String -Pattern ":$port\s"
|
|
foreach ($l in $lines) {
|
|
$parts = ($l.ToString() -split '\s+') | Where-Object { $_ }
|
|
$pid_ = $parts[-1]
|
|
if ($pid_ -match '^\d+$') {
|
|
if (-not (_PidUnderKnownRoot -Pid_ ([int]$pid_) -KnownRoots $KnownRoots)) { continue }
|
|
try { Stop-Process -Id ([int]$pid_) -Force -ErrorAction SilentlyContinue } catch { }
|
|
}
|
|
}
|
|
} catch { }
|
|
}
|
|
Remove-Item -LiteralPath $PortFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Stop processes whose ExecutablePath lives under an unsloth_studio venv.
|
|
# Anchoring on the venv path avoids matching unrelated python.exe / studio.exe.
|
|
function _StopStudioProcesses {
|
|
param([string[]]$KnownRoots)
|
|
try {
|
|
$procs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
$_.ExecutablePath -and ($_.ExecutablePath -match '\\unsloth_studio\\.*\\(unsloth|python|studio)\.exe$') -and
|
|
$_.CommandLine -and ($_.CommandLine -match 'studio')
|
|
}
|
|
foreach ($p in $procs) {
|
|
# Optional scope: only kill if the exe is under a known root.
|
|
if ($KnownRoots) {
|
|
$match = $false
|
|
foreach ($r in $KnownRoots) {
|
|
if ($p.ExecutablePath -and ($p.ExecutablePath -ilike "$r\*")) { $match = $true; break }
|
|
}
|
|
if (-not $match) { continue }
|
|
}
|
|
try {
|
|
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
} catch { }
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
# Stop processes that would block deleting the paths we remove. Unlike
|
|
# _StopStudioProcesses (venv exe only), this also catches llama-server/llama-cli,
|
|
# the unsloth.exe shim, and orphaned mp workers under SYSTEM python holding a
|
|
# venv DLL (an open DLL handle blocks the dir delete) -- found by scanning each
|
|
# candidate's loaded modules, not just its image path.
|
|
function _StopProcessesLockingRoots {
|
|
param([string[]]$Roots)
|
|
$clean = @($Roots | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\','/') })
|
|
if ($clean.Count -eq 0) { return }
|
|
$underRoot = {
|
|
param($p)
|
|
if (-not $p) { return $false }
|
|
foreach ($r in $clean) { if ($p -ieq $r -or $p -ilike "$r\*") { return $true } }
|
|
return $false
|
|
}
|
|
# 1. Image path under a target root (venv python, shim, llama-server).
|
|
try {
|
|
foreach ($proc in (Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)) {
|
|
if ((& $underRoot $proc.ExecutablePath)) {
|
|
try { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } catch { }
|
|
}
|
|
}
|
|
} catch { }
|
|
# 2. A loaded module under a target root (orphaned mp-fork python holding a
|
|
# venv DLL). Scoped to names that load our DLLs to keep the scan fast.
|
|
try {
|
|
$cands = Get-Process -Name python, pythonw, unsloth, llama-server, llama-cli -ErrorAction SilentlyContinue
|
|
foreach ($proc in $cands) {
|
|
$hit = $false
|
|
try {
|
|
foreach ($m in $proc.Modules) { if ((& $underRoot $m.FileName)) { $hit = $true; break } }
|
|
} catch { } # access denied enumerating modules -> skip
|
|
if ($hit) { try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch { } }
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
# Default install root + default data dir.
|
|
$defaultStudioHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth\studio" } else { $null }
|
|
$defaultDataDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth Studio" } else { $null }
|
|
# Default-mode ~/.unsloth holds a SHARED llama.cpp build + .cache that are
|
|
# siblings of studio (not under it), so deleting <studio> misses them -- handle
|
|
# explicitly. No-op in env/custom mode (nested under the custom root, removed
|
|
# with it). A user-set UNSLOTH_LLAMA_CPP_PATH is left alone.
|
|
$defaultUnslothHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth" } else { $null }
|
|
$defaultLlamaCpp = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "llama.cpp" } else { $null }
|
|
$defaultCache = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".cache" } else { $null }
|
|
# Isolated Node.js runtime (install_node_prebuilt.py), a sibling of studio in
|
|
# default mode. No-op in env/custom mode (nested under the custom root) and absent.
|
|
$defaultNode = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "node" } else { $null }
|
|
# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging,
|
|
# sibling of the install dir). Usually pruned after activate, but an interrupted
|
|
# build can leave a "<name>.staging-XXXX" tree; removing it lets the empty-dir
|
|
# cleanup of ~/.unsloth below succeed. No-op in env/custom mode and when absent.
|
|
$defaultStaging = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".staging" } else { $null }
|
|
|
|
# Build known-root list FIRST so the port-file kill can verify ownership.
|
|
$customRoots = @(_CustomStudioRoots)
|
|
$knownRoots = @()
|
|
if ($defaultStudioHome) { $knownRoots += $defaultStudioHome }
|
|
$knownRoots += $customRoots
|
|
|
|
# ── Stop running servers ──
|
|
_Step "Stopping any running Unsloth Studio servers..."
|
|
if ($defaultDataDir) {
|
|
_StopByPortFile -PortFile (Join-Path $defaultDataDir "studio.port") -KnownRoots $knownRoots
|
|
}
|
|
foreach ($r in $customRoots) {
|
|
_StopByPortFile -PortFile (Join-Path $r "share\studio.port") -KnownRoots $knownRoots
|
|
}
|
|
_StopStudioProcesses -KnownRoots $knownRoots
|
|
# Also stop anything holding a handle on the exact paths we delete (llama-server,
|
|
# the CLI shim, an mp-fork python with a venv DLL) so the dir delete isn't refused.
|
|
_StopProcessesLockingRoots -Roots (@($knownRoots) + @($defaultDataDir, $defaultLlamaCpp, $defaultCache, $defaultNode))
|
|
|
|
# ── Remove custom-root install trees ──
|
|
_Step "Removing data and install directories..."
|
|
foreach ($r in $customRoots) {
|
|
if (_IsUnsafeRoot $r) {
|
|
_Substep "refusing to remove unsafe path: $r" "Yellow"
|
|
continue
|
|
}
|
|
if (-not (_IsStudioRoot $r)) {
|
|
_Substep "refusing to remove non-Studio path: $r" "Yellow"
|
|
continue
|
|
}
|
|
_RemovePath $r
|
|
}
|
|
# Default install dir (always at %USERPROFILE%\.unsloth\studio when present).
|
|
if ($defaultStudioHome) { _RemovePath $defaultStudioHome }
|
|
# Default data dir.
|
|
if ($defaultDataDir) { _RemoveDataDirKeepingWslIcon $defaultDataDir }
|
|
# Default-mode shared llama.cpp build + cache (siblings of studio under
|
|
# ~/.unsloth). No-op in env/custom mode and when absent.
|
|
if ($defaultLlamaCpp) { _RemovePath $defaultLlamaCpp }
|
|
if ($defaultCache) { _RemovePath $defaultCache }
|
|
# Isolated Node.js runtime (sibling of studio under ~/.unsloth). No-op in env/
|
|
# custom mode (nested under the custom root, removed with it) and when absent.
|
|
if ($defaultNode) { _RemovePath $defaultNode }
|
|
if ($defaultStaging) { _RemovePath $defaultStaging }
|
|
# llama.cpp install lock (serializes the shared build); a stray lock keeps
|
|
# ~/.unsloth from being pruned below. No-op in env/custom mode and when absent.
|
|
if ($defaultUnslothHome) { _RemovePath (Join-Path $defaultUnslothHome ".llama.cpp.install.lock") }
|
|
# Drop ~/.unsloth itself, but ONLY if now empty -- never nuke unrelated content.
|
|
if ($defaultUnslothHome -and (Test-Path -LiteralPath $defaultUnslothHome) -and
|
|
-not (Get-ChildItem -LiteralPath $defaultUnslothHome -Force -ErrorAction SilentlyContinue)) {
|
|
_RemovePath $defaultUnslothHome
|
|
}
|
|
|
|
# ── Remove desktop and Start Menu shortcuts ──
|
|
_Step "Removing desktop and Start Menu shortcuts..."
|
|
try {
|
|
$desktop = [Environment]::GetFolderPath("Desktop")
|
|
if ($desktop) { _RemovePath (Join-Path $desktop "Unsloth Studio.lnk") }
|
|
} catch { }
|
|
if ($env:APPDATA) {
|
|
_RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk")
|
|
}
|
|
# Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile
|
|
# disappears promptly instead of lingering stale (mirrors install.ps1's
|
|
# New-StudioShortcuts). Preserves start2.bin (the pin layout).
|
|
try {
|
|
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
|
|
if (Test-Path -LiteralPath $smehTemp) {
|
|
Get-ChildItem -LiteralPath $smehTemp -Filter "TileCache_*" -ErrorAction SilentlyContinue |
|
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath (Join-Path $smehTemp "StartUnifiedTileModelCache.dat") -Force -ErrorAction SilentlyContinue
|
|
Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction SilentlyContinue
|
|
}
|
|
} catch { }
|
|
|
|
# Re-sweep: the first pass may have left unsloth.ico locked by Explorer/SMEH for
|
|
# the native shortcut; that handle is now freed. (A surviving WSL shortcut still
|
|
# keeps the icon -- see the helper.)
|
|
if ($defaultDataDir -and (Test-Path -LiteralPath $defaultDataDir)) { _RemoveDataDirKeepingWslIcon $defaultDataDir }
|
|
|
|
# ── Clean user PATH and registry backup ──
|
|
_Step "Cleaning user PATH and registry..."
|
|
try {
|
|
$regKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
|
|
if ($regKey) {
|
|
try {
|
|
$rawPath = $regKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
if ($rawPath) {
|
|
$entries = $rawPath -split ';'
|
|
$kept = New-Object System.Collections.ArrayList
|
|
$removedAny = $false
|
|
# Only remove PATH entries that live inside a Studio root we
|
|
# actually own (default or env-mode). A literal substring
|
|
# match on `unsloth_studio` would clobber unrelated user
|
|
# virtualenvs that happen to share the name.
|
|
foreach ($e in $entries) {
|
|
if ([string]::IsNullOrWhiteSpace($e)) { continue }
|
|
$expanded = [Environment]::ExpandEnvironmentVariables($e).TrimEnd('\','/')
|
|
$isStudio = $false
|
|
foreach ($r in $knownRoots) {
|
|
if (-not $r) { continue }
|
|
$rNorm = $r.TrimEnd('\','/')
|
|
if ($expanded -ieq $rNorm -or $expanded -ilike "$rNorm\*") {
|
|
$isStudio = $true; break
|
|
}
|
|
}
|
|
if ($isStudio) {
|
|
_Substep "removed PATH entry: $e" "Green"
|
|
$removedAny = $true
|
|
continue
|
|
}
|
|
[void]$kept.Add($e)
|
|
}
|
|
if ($removedAny) {
|
|
$newPath = ($kept -join ';')
|
|
$regKey.SetValue('Path', $newPath, [Microsoft.Win32.RegistryValueKind]::ExpandString)
|
|
try {
|
|
$d = "UnslothPathRefresh_" + ([guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
[Environment]::SetEnvironmentVariable($d, '1', 'User')
|
|
[Environment]::SetEnvironmentVariable($d, [NullString]::Value, 'User')
|
|
} catch { }
|
|
}
|
|
}
|
|
} finally {
|
|
$regKey.Close()
|
|
}
|
|
}
|
|
} catch {
|
|
_Substep "could not update user PATH: $($_.Exception.Message)" "Yellow"
|
|
}
|
|
# Remove HKCU\Software\Unsloth (PathBackup lives here; install.ps1 owns it).
|
|
try {
|
|
Remove-Item -LiteralPath 'HKCU:\Software\Unsloth' -Recurse -Force -ErrorAction SilentlyContinue
|
|
} catch { }
|
|
|
|
Write-Host ""
|
|
Write-Host "Unsloth Studio uninstalled."
|
|
Write-Host "Note: Hugging Face model cache at %USERPROFILE%\.cache\huggingface was left in place."
|
|
Write-Host "Remove it manually with 'Remove-Item -Recurse -Force `"$env:USERPROFILE\.cache\huggingface\hub`"' if desired."
|
|
if (-not $env:UNSLOTH_STUDIO_HOME -and -not $env:STUDIO_HOME) {
|
|
Write-Host ""
|
|
Write-Host "If you installed Unsloth Studio with UNSLOTH_STUDIO_HOME or STUDIO_HOME"
|
|
Write-Host "pointing at a custom directory, re-run this script with the same variable"
|
|
Write-Host "set to also remove that install tree, e.g.:"
|
|
Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex"
|
|
}
|
|
}
|
|
|
|
Uninstall-UnslothStudio @args
|