studio: add uninstall.sh and document it in README (#5497)

* studio: add uninstall.sh and document it in README

The current uninstall guidance in README.md is `rm -rf ~/.unsloth/studio`,
which leaves behind everything that lives outside that path:

  - ~/.local/share/unsloth/ (launcher script, studio.conf, studio.log,
    icon assets)
  - ~/Applications/Unsloth Studio.app (macOS bundle, orphaned and
    pointing nowhere on next reinstall)
  - ~/Desktop/Unsloth Studio (broken symlink after the bundle is gone)
  - ~/Desktop/unsloth-studio.desktop (Linux)
  - ~/.local/share/applications/unsloth-studio.desktop (Linux)
  - /tmp/unsloth-studio-launcher-<uid>*.lock (lock dir, possibly stale)
  - Launch Services cache entry for ai.unsloth.studio on macOS
  - Any running `unsloth studio -p N` processes

Users who follow the documented uninstall and reinstall end up with the
new launcher layered on top of stale state from the previous install,
which has produced concrete bugs (e.g. self-referential symlink inside
the .app bundle after a reinstall over leftover state).

Add uninstall.sh at the repo root that handles all of the above, and
update README.md to point at it as the recommended path. The plain
`rm -rf ~/.unsloth/studio` line is kept as a "partial uninstall, keep
launcher for a later reinstall" alternative. The model cache at
~/.cache/huggingface is intentionally left untouched, with a note in
the script suggesting how to remove it if desired.

Script is POSIX sh, idempotent (every removal is gated on existence
and uses `2>/dev/null || true`), and handles macOS, Linux, and WSL.
Windows is intentionally not covered here; the existing PowerShell
Remove-Item line in README is kept for that.

* studio: trim uninstall.sh header

* studio: address PR review feedback on uninstall.sh

Four findings from automated review, all verified real:

1. pkill pattern only matched `-p N`, not `--port N`. Studio
   instances launched with the long option form survived the
   uninstall. Fix: run two pkill passes, one for each form, with
   `[ =]` covering both space and `=` separators.

2. CLI shim at ~/.local/bin/unsloth (symlink into the venv created
   by install.sh:2167) was left behind, becoming a broken symlink
   after the venv directory is removed. Fix: add it to the removals.

3. Custom install roots via UNSLOTH_STUDIO_HOME / STUDIO_HOME were
   not removed. install.sh records the install location in
   ~/.local/share/unsloth/studio.conf as UNSLOTH_EXE; parse it,
   derive the root as three dirnames up, and remove the root if it
   is non-default.

4. On WSL the installer creates 'Unsloth Studio.lnk' on the Windows
   Desktop and Start Menu Programs folder via powershell.exe.
   Mirror that path on uninstall by invoking powershell.exe to
   Remove-Item the same two locations. Best-effort, gated on
   powershell.exe being available.

Tests (T2.8b, T2.15, T2.16, T2.17, T2.18, T2.5b) added behind the
scenes; all pass on macOS Darwin 25.3 with `dash -n`, `sh -n`,
shellcheck-clean (SC2016 suppressed on the PowerShell single-quoted
heredoc since the $env: expansions must remain literal to the
shell so PowerShell receives them verbatim).

* studio: harden uninstall.sh against env-mode and shim collisions

- Honor UNSLOTH_STUDIO_HOME / STUDIO_HOME at uninstall time and read
  env-mode studio.conf at $<root>/share/studio.conf, not just the
  default-mode conf under $HOME/.local/share/unsloth/. Without this,
  installs done with a custom STUDIO_HOME leak the install tree even
  when the env var is re-exported.
- Guard the custom-root resolver against "/" and empty so a corrupted
  studio.conf (UNSLOTH_EXE='/etc/passwd' or similar) or an
  UNSLOTH_STUDIO_HOME=/ cannot trick the script into rm -rf'ing root.
- Only remove $HOME/.local/bin/unsloth when it is a symlink resolving
  to a Studio venv. pyproject.toml declares unsloth as a console
  script, so pip install --user unsloth places a regular file at the
  same path; the previous unconditional rm wiped that unrelated CLI.
- When neither env var is set, print a tail hint so users with custom
  install roots know to re-run with the variable.

Verified with a sandboxed harness covering 24 scenarios (default and
env-mode installs across macOS / Linux / WSL, idempotency, hostile
lockfile names, path-traversal attempts, malformed conf, pkill long
and short forms, pip-conflict shim, broken-symlink bundle path).
Script remains POSIX (shellcheck -s sh clean, runs under /bin/dash).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refuse non-Studio uninstall roots and tighten process matching for PR #5497

Three issues found while testing custom-root paths and process cleanup:

1. UNSLOTH_STUDIO_HOME=$HOME sh uninstall.sh rm -rf'd $HOME (same for
   STUDIO_HOME and parent-of-$HOME). install.sh accepts any writable
   directory for STUDIO_HOME, so the uninstaller must validate ownership
   before deletion. _is_studio_root accepts a candidate root only if it
   contains share/studio.conf, an unsloth_studio/ directory, or a
   bin/unsloth shim pointing into unsloth_studio/bin. _is_unsafe_root is
   a defense-in-depth deny list (/, $HOME, $HOME's parent, system paths).

2. pkill -f patterns "unsloth studio.*-p[ =][0-9]" over-matched on argv
   substrings. A user running `less notes.md` whose filename contained
   "unsloth studio ... -p N" had their less killed. New patterns anchor
   on /unsloth_studio/bin/ so only processes whose actual exe lives in a
   Studio venv match.

3. pkill missed processes that exec into studio/backend/run.py --port N
   (the post-exec form when the unsloth CLI replaces itself). Added a
   third pattern for that shape, and prefer PID files written by
   install.sh's _spawn_terminal (studio-$port.pid in DATA_DIR) over
   argv matching for installs that have them.

* Tighten ownership guards from review round for PR #5497

Three findings from the second reviewer round:

1. _is_studio_root accepted any directory containing an unsloth_studio/
   subdir as Studio-owned. A user workspace that happens to contain a
   folder named unsloth_studio/ would be deleted. install.sh's env-mode
   guard at install.sh:1358-1361 already requires .unsloth-studio-owned
   before treating the venv as replaceable. Mirror that: require the
   owner marker, share/studio.conf, or the bin/unsloth shim target.

2. The pkill -f fallback patterns were global, so uninstalling install A
   would also kill install B's running server. Scope each pattern to the
   actual install root being removed by interpolating the root path into
   the regex. Also adds a third pattern shape for `unsloth studio` with
   no -p / --port flag (the CLI default-port form).

3. Desktop/Unsloth Studio is created by install.sh as a symlink to the
   .app bundle. If a user has a regular directory by that name (photos,
   notes, etc.), the previous _remove_path call rm -rf'd it. Now we only
   remove it when it is a symlink or does not exist.

* Canonicalize env roots and honor UNSLOTH_STUDIO_HOME precedence for PR #5497

Two findings from the latest review round:

1. Canonicalize env-derived roots before the safety check. The deny list
   only string-compares against $HOME, so a syntactic variant like
   UNSLOTH_STUDIO_HOME=$HOME/../$USER (or trailing slash, or relative
   path) bypassed _is_unsafe_root even though it resolves to $HOME. Now
   _emit runs CDPATH= cd -P -- + pwd -P first, so all variants normalize
   to the same canonical path before the deny check. Also added the same
   tilde expansion install.sh's _resolve_studio_destinations does.

2. Mirror install.sh's env-var precedence (install.sh:282-290). When
   both UNSLOTH_STUDIO_HOME and STUDIO_HOME are set, install.sh resolves
   only UNSLOTH_STUDIO_HOME and ignores STUDIO_HOME. Uninstall was
   emitting both, so running uninstall.sh for install A would also
   delete install B if the user had a stale STUDIO_HOME pointing at B.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel Han <info@unsloth.ai>
This commit is contained in:
Michael Han 2026-05-18 02:11:05 -07:00 committed by GitHub
commit c41ce170ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 287 additions and 2 deletions

View file

@ -218,11 +218,13 @@ unsloth studio -p 8888
```
#### Uninstall
You can uninstall Unsloth Studio by deleting its install folder usually located under `$HOME/.unsloth/studio` on Mac/Linux/WSL and `%USERPROFILE%\.unsloth\studio` on Windows. Using the `rm -rf` commands will **delete everything**, including your history, cache:
On Mac/Linux/WSL the recommended way to fully remove Unsloth Studio is the `uninstall.sh` script. It stops any running servers, removes the install dir, the launcher data dir, the desktop shortcut, the macOS `.app` bundle, and the Launch Services entry:
* **MacOS, WSL, Linux:** `rm -rf ~/.unsloth/studio`
* **MacOS, WSL, Linux:** `curl -fsSL https://unsloth.ai/uninstall.sh | sh`
* **Windows (PowerShell):** `Remove-Item -Recurse -Force "$HOME\.unsloth\studio"`
If you only want to drop the install dir and keep the launcher/shortcut for a later reinstall, you can instead run `rm -rf ~/.unsloth/studio`. The model cache at `~/.cache/huggingface` is not touched by either command.
For more info, [see our docs](https://unsloth.ai/docs/new/studio/install#uninstall).
#### Deleting model files