Compare commits
3 commits
main
...
fix/uninst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9082357414 | ||
|
|
f6b341bc70 | ||
|
|
2e3d0c151c |
6 changed files with 211 additions and 20 deletions
|
|
@ -3,8 +3,10 @@
|
|||
#
|
||||
# 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
|
||||
# desktop and Start Menu shortcuts, the user PATH entry, the PathBackup
|
||||
# registry key, and WebView2 runtime data keyed by the app bundle id
|
||||
# (EBWebView caches created at first desktop-app launch, not by install.ps1).
|
||||
# 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
|
||||
|
|
@ -398,6 +400,32 @@ function Uninstall-UnslothStudio {
|
|||
_RemovePath $defaultUnslothHome
|
||||
}
|
||||
|
||||
# WebView2/app runtime data keyed by the Tauri bundle id. Created at first
|
||||
# desktop-app launch, not by install.ps1. LOCALAPPDATA holds the EBWebView
|
||||
# profile (a leftover copy serves a stale frontend to the next install);
|
||||
# APPDATA holds the app config dir.
|
||||
_Step "Removing WebView caches and app data (ai.unsloth.studio)..."
|
||||
$bundleId = "ai.unsloth.studio"
|
||||
$webviewDataDirs = @()
|
||||
if ($env:LOCALAPPDATA) { $webviewDataDirs += Join-Path $env:LOCALAPPDATA $bundleId }
|
||||
if ($env:APPDATA) { $webviewDataDirs += Join-Path $env:APPDATA $bundleId }
|
||||
# Stop the desktop app and any msedgewebview2.exe helper using these dirs
|
||||
# first; WebView2 keeps open handles that make the delete fail.
|
||||
try { Stop-Process -Name "unsloth-studio" -Force -ErrorAction SilentlyContinue } catch { }
|
||||
try {
|
||||
foreach ($proc in (Get-CimInstance Win32_Process -Filter "Name = 'msedgewebview2.exe'" -ErrorAction SilentlyContinue)) {
|
||||
$cl = $proc.CommandLine
|
||||
if (-not $cl) { continue }
|
||||
foreach ($d in $webviewDataDirs) {
|
||||
if ($cl -ilike "*$d*") {
|
||||
try { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } catch { }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
foreach ($d in $webviewDataDirs) { _RemovePath $d }
|
||||
|
||||
# ── Remove desktop and Start Menu shortcuts ──
|
||||
_Step "Removing desktop and Start Menu shortcuts..."
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
#
|
||||
# Unsloth Studio uninstaller (macOS / Linux / WSL).
|
||||
# Stops running servers and removes install dir, launcher data,
|
||||
# CLI shim, desktop shortcut, .app bundle, and Launch Services entry.
|
||||
# CLI shim, desktop shortcut, .app bundle, Launch Services entry, and
|
||||
# WebView runtime data keyed by the app bundle id (WebKit/webkit2gtk
|
||||
# caches created at first app launch, not by install.sh).
|
||||
# Honors custom roots set via UNSLOTH_STUDIO_HOME / STUDIO_HOME at
|
||||
# install time (read back from studio.conf).
|
||||
#
|
||||
|
|
@ -80,6 +82,13 @@ $_roots_from_conf"
|
|||
pkill -KILL -f "$_pat" 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
|
||||
# Tauri desktop app (binary "unsloth-studio"): stop it so its WebView
|
||||
# helpers can't hold or re-create the runtime caches removed below.
|
||||
# -x is an exact name match, so the "unsloth" CLI shim is never touched.
|
||||
pkill -TERM -x unsloth-studio 2>/dev/null || true
|
||||
sleep 0.5
|
||||
pkill -KILL -x unsloth-studio 2>/dev/null || true
|
||||
}
|
||||
|
||||
_remove_path() {
|
||||
|
|
@ -261,6 +270,25 @@ case "$_os" in
|
|||
if [ -x "$_lsr" ]; then
|
||||
"$_lsr" -u "$HOME/Applications/Unsloth Studio.app" 2>/dev/null || true
|
||||
fi
|
||||
# WebView/app runtime data keyed by the bundle id. Created by macOS +
|
||||
# WKWebView at first app launch, not by install.sh, so it survives an
|
||||
# uninstall and a leftover WebKit cache then serves a stale frontend
|
||||
# to the next install.
|
||||
_bid="ai.unsloth.studio"
|
||||
echo "Removing WebView caches and app data ($_bid)..."
|
||||
_remove_path "$HOME/Library/Caches/$_bid"
|
||||
_remove_path "$HOME/Library/WebKit/$_bid"
|
||||
_remove_path "$HOME/Library/Application Support/$_bid"
|
||||
_remove_path "$HOME/Library/HTTPStorages/$_bid"
|
||||
_remove_path "$HOME/Library/HTTPStorages/$_bid.binarycookies"
|
||||
_remove_path "$HOME/Library/Cookies/$_bid.binarycookies"
|
||||
_remove_path "$HOME/Library/Saved Application State/$_bid.savedState"
|
||||
# Delete via defaults first: cfprefsd caches plists in memory and can
|
||||
# re-write the file after a bare rm.
|
||||
if command -v defaults >/dev/null 2>&1; then
|
||||
defaults delete "$_bid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
_remove_path "$HOME/Library/Preferences/$_bid.plist"
|
||||
;;
|
||||
Linux)
|
||||
if [ "$_is_wsl" = "1" ]; then
|
||||
|
|
@ -403,6 +431,15 @@ case "$_os" in
|
|||
echo " sudo rm -rf /opt/rocm /opt/rocm-* && sudo ldconfig"
|
||||
fi
|
||||
fi
|
||||
# WebView/app runtime data keyed by the Tauri bundle id. Created by
|
||||
# webkit2gtk at first desktop-app launch, not by install.sh; a
|
||||
# leftover cache serves a stale frontend to the next install.
|
||||
_bid="ai.unsloth.studio"
|
||||
echo "Removing WebView caches and app data ($_bid)..."
|
||||
_remove_path "${XDG_CACHE_HOME:-$HOME/.cache}/$_bid"
|
||||
_remove_path "${XDG_DATA_HOME:-$HOME/.local/share}/$_bid"
|
||||
_remove_path "${XDG_CONFIG_HOME:-$HOME/.config}/$_bid"
|
||||
_remove_path "${XDG_STATE_HOME:-$HOME/.local/state}/$_bid"
|
||||
echo "Removing Linux .desktop entry..."
|
||||
_remove_path "$HOME/.local/share/applications/unsloth-studio.desktop"
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh"
|
|||
sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh"
|
||||
sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh"
|
||||
sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh"
|
||||
sh "$TESTS_DIR/sh/test_uninstall_webview_data.sh"
|
||||
sh "$TESTS_DIR/sh/test_torch_flavor.sh"
|
||||
sh "$TESTS_DIR/sh/test_redact_install_output.sh"
|
||||
sh "$TESTS_DIR/sh/test_install_uv_override_space.sh"
|
||||
|
|
|
|||
129
tests/sh/test_uninstall_webview_data.sh
Executable file
129
tests/sh/test_uninstall_webview_data.sh
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
# Regression tests for WebView runtime-data cleanup in scripts/uninstall.sh.
|
||||
#
|
||||
# WKWebView (macOS) and webkit2gtk (Linux) create data keyed by the bundle id
|
||||
# at first app launch, not at install time, so the uninstaller used to miss it
|
||||
# and a leftover cache served a stale frontend to the next install. Runs the
|
||||
# full script against a fixture HOME (pkill/defaults stubbed via PATH, OS
|
||||
# branch picked by a stubbed uname, /proc/version WSL probe force-failed) and
|
||||
# asserts bundle-id paths are removed while unrelated app data survives.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UNINSTALL_SH="$SCRIPT_DIR/../../scripts/uninstall.sh"
|
||||
BID="ai.unsloth.studio"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_TMP_ROOT=$(mktemp -d)
|
||||
trap 'rm -rf "$_TMP_ROOT"' EXIT
|
||||
|
||||
assert_gone() { _l="$1"; if [ -e "$2" ]; then echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); else echo " PASS: $_l"; PASS=$((PASS+1)); fi; }
|
||||
assert_present() { _l="$1"; if [ -e "$2" ]; then echo " PASS: $_l"; PASS=$((PASS+1)); else echo " FAIL: $_l (missing: $2)"; FAIL=$((FAIL+1)); fi; }
|
||||
|
||||
# Stub out process kills and macOS pref/LaunchServices tools so the script can
|
||||
# run against a fixture HOME without touching the real system.
|
||||
STUB_BIN="$_TMP_ROOT/stubbin"
|
||||
mkdir -p "$STUB_BIN"
|
||||
for _tool in pkill defaults; do
|
||||
printf '#!/bin/sh\nexit 0\n' > "$STUB_BIN/$_tool"
|
||||
chmod +x "$STUB_BIN/$_tool"
|
||||
done
|
||||
# Force the non-WSL path: with uname stubbed to Linux on a WSL host, the
|
||||
# script's `grep -qi microsoft /proc/version` probe would still fire and the
|
||||
# real WSL cleanup would touch the host's /mnt/* shortcuts and /etc profile.
|
||||
# Fail that one probe; delegate every other grep call to the real grep.
|
||||
# REAL_GREP must be an absolute path: a bare "grep" (e.g. from an alias-shaped
|
||||
# `command -v` result) would resolve back to this stub and self-exec forever.
|
||||
REAL_GREP=$(command -v grep)
|
||||
case "$REAL_GREP" in /*) ;; *) REAL_GREP=/usr/bin/grep ;; esac
|
||||
cat > "$STUB_BIN/grep" <<EOF
|
||||
#!/bin/sh
|
||||
for _a in "\$@"; do
|
||||
[ "\$_a" = "/proc/version" ] && exit 1
|
||||
done
|
||||
exec "$REAL_GREP" "\$@"
|
||||
EOF
|
||||
chmod +x "$STUB_BIN/grep"
|
||||
# Belt and braces should the WSL branch ever be entered anyway: powershell.exe
|
||||
# exiting 0 makes its no-op path taken (and skips the /mnt/* drvfs fallback);
|
||||
# sudo exiting 0 without running its argv keeps /etc untouched.
|
||||
for _tool in powershell.exe sudo; do
|
||||
printf '#!/bin/sh\nexit 0\n' > "$STUB_BIN/$_tool"
|
||||
chmod +x "$STUB_BIN/$_tool"
|
||||
done
|
||||
|
||||
# run_uninstall <home> <uname_output> : run the full script with a stubbed OS.
|
||||
run_uninstall() {
|
||||
printf '#!/bin/sh\necho %s\n' "$2" > "$STUB_BIN/uname"
|
||||
chmod +x "$STUB_BIN/uname"
|
||||
env -u UNSLOTH_STUDIO_HOME -u STUDIO_HOME \
|
||||
-u XDG_CACHE_HOME -u XDG_DATA_HOME -u XDG_CONFIG_HOME -u XDG_STATE_HOME \
|
||||
HOME="$1" PATH="$STUB_BIN:$PATH" sh "$UNINSTALL_SH" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ── 1. macOS: every bundle-id-keyed ~/Library path is removed ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$H/Library/Caches/$BID/WebKit/NetworkCache" \
|
||||
"$H/Library/WebKit/$BID/WebsiteData/CacheStorage" \
|
||||
"$H/Library/Application Support/$BID" \
|
||||
"$H/Library/HTTPStorages/$BID" \
|
||||
"$H/Library/Saved Application State/$BID.savedState" \
|
||||
"$H/Library/Preferences" \
|
||||
"$H/Library/Cookies" \
|
||||
"$H/Library/Caches/com.other.app"
|
||||
: > "$H/Library/HTTPStorages/$BID.binarycookies"
|
||||
: > "$H/Library/Cookies/$BID.binarycookies"
|
||||
: > "$H/Library/Preferences/$BID.plist"
|
||||
: > "$H/Library/Caches/$BID/stale-frontend.js"
|
||||
: > "$H/Library/Caches/com.other.app/keepme"
|
||||
run_uninstall "$H" Darwin
|
||||
assert_gone "macOS: Caches/$BID removed" "$H/Library/Caches/$BID"
|
||||
assert_gone "macOS: WebKit/$BID removed" "$H/Library/WebKit/$BID"
|
||||
assert_gone "macOS: Application Support/$BID removed" "$H/Library/Application Support/$BID"
|
||||
assert_gone "macOS: HTTPStorages/$BID removed" "$H/Library/HTTPStorages/$BID"
|
||||
assert_gone "macOS: HTTPStorages/$BID.binarycookies removed" "$H/Library/HTTPStorages/$BID.binarycookies"
|
||||
assert_gone "macOS: Cookies/$BID.binarycookies removed" "$H/Library/Cookies/$BID.binarycookies"
|
||||
assert_gone "macOS: Saved Application State removed" "$H/Library/Saved Application State/$BID.savedState"
|
||||
assert_gone "macOS: Preferences/$BID.plist removed" "$H/Library/Preferences/$BID.plist"
|
||||
assert_present "macOS: unrelated app cache kept" "$H/Library/Caches/com.other.app/keepme"
|
||||
|
||||
# ── 2. Linux: bundle-id-keyed XDG default paths are removed ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$H/.cache/$BID" "$H/.local/share/$BID" "$H/.config/$BID" \
|
||||
"$H/.local/state/$BID" "$H/.cache/other.app"
|
||||
run_uninstall "$H" Linux
|
||||
assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID"
|
||||
assert_gone "linux: ~/.local/share/$BID removed" "$H/.local/share/$BID"
|
||||
assert_gone "linux: ~/.config/$BID removed" "$H/.config/$BID"
|
||||
assert_gone "linux: ~/.local/state/$BID removed" "$H/.local/state/$BID"
|
||||
assert_present "linux: unrelated app cache kept" "$H/.cache/other.app"
|
||||
|
||||
# ── 3. Linux: XDG_*_HOME overrides are honored ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
XDG=$(mktemp -d -p "$_TMP_ROOT")
|
||||
mkdir -p "$XDG/cache/$BID" "$XDG/data/$BID" "$XDG/config/$BID" "$XDG/state/$BID"
|
||||
printf '#!/bin/sh\necho Linux\n' > "$STUB_BIN/uname"
|
||||
chmod +x "$STUB_BIN/uname"
|
||||
env -u UNSLOTH_STUDIO_HOME -u STUDIO_HOME \
|
||||
XDG_CACHE_HOME="$XDG/cache" XDG_DATA_HOME="$XDG/data" \
|
||||
XDG_CONFIG_HOME="$XDG/config" XDG_STATE_HOME="$XDG/state" \
|
||||
HOME="$H" PATH="$STUB_BIN:$PATH" sh "$UNINSTALL_SH" >/dev/null 2>&1
|
||||
assert_gone "linux: XDG_CACHE_HOME override honored" "$XDG/cache/$BID"
|
||||
assert_gone "linux: XDG_DATA_HOME override honored" "$XDG/data/$BID"
|
||||
assert_gone "linux: XDG_CONFIG_HOME override honored" "$XDG/config/$BID"
|
||||
assert_gone "linux: XDG_STATE_HOME override honored" "$XDG/state/$BID"
|
||||
|
||||
# ── 4. Nothing to remove is a clean no-op (fresh HOME, exit 0) ──
|
||||
H=$(mktemp -d -p "$_TMP_ROOT")
|
||||
if run_uninstall "$H" Darwin; then
|
||||
echo " PASS: empty HOME -> no-op exit 0"; PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: empty HOME -> nonzero exit"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" = 0 ]
|
||||
|
|
@ -38,7 +38,11 @@ def fail(m):
|
|||
raise AssertionError(f"[font-scale] FAIL: {m}")
|
||||
|
||||
|
||||
def near(a, b, tol = 0.35):
|
||||
def near(
|
||||
a,
|
||||
b,
|
||||
tol = 0.35,
|
||||
):
|
||||
return a is not None and b is not None and abs(a - b) <= tol
|
||||
|
||||
|
||||
|
|
@ -86,9 +90,7 @@ def open_appearance(page):
|
|||
page.wait_for_timeout(700)
|
||||
if page.get_by_role("dialog").count() == 0:
|
||||
fail("settings dialog did not open")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Appearance"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
|
||||
|
||||
|
|
@ -153,9 +155,7 @@ def main():
|
|||
page.wait_for_timeout(400)
|
||||
|
||||
step("overflowing select scrolls its Radix viewport")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Voice"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
page.set_viewport_size({"width": 1440, "height": 480})
|
||||
page.locator("[aria-label='Dictation language']").click()
|
||||
|
|
@ -194,9 +194,7 @@ def main():
|
|||
page.wait_for_timeout(400)
|
||||
|
||||
step("default restores exactly")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(
|
||||
has_text = "Appearance"
|
||||
).first.click()
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
page.wait_for_timeout(500)
|
||||
set_input(page, "UI font size", DEFAULT)
|
||||
final = measure(page)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ from pathlib import Path
|
|||
REPO = Path(__file__).resolve().parents[2]
|
||||
SRC = REPO / "studio/frontend/src"
|
||||
INDEX_CSS = (SRC / "index.css").read_text(encoding = "utf-8")
|
||||
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(
|
||||
encoding = "utf-8"
|
||||
)
|
||||
STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(encoding = "utf-8")
|
||||
SELECT = (SRC / "components/ui/select.tsx").read_text(encoding = "utf-8")
|
||||
|
||||
# Raw numeric fontSize props are only allowed where a scaled stylesheet rule
|
||||
|
|
@ -70,9 +68,7 @@ def test_ui_token_families_exist():
|
|||
|
||||
|
||||
def test_explicit_code_font_size_is_never_multiplied():
|
||||
match = re.search(
|
||||
r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS
|
||||
)
|
||||
match = re.search(r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS)
|
||||
assert match is not None
|
||||
body = match.group(1)
|
||||
assert "var(--custom-code-font-size)" in body
|
||||
|
|
@ -83,7 +79,9 @@ def test_radix_select_viewport_owns_the_scroll_state():
|
|||
viewport = SELECT[SELECT.index("SelectPrimitive.Viewport") :]
|
||||
assert "overflow-y-auto" in viewport.split("</SelectPrimitive.Viewport>")[0]
|
||||
# The rounded surface itself must not scroll (WebKit squares its corners).
|
||||
content_cls = re.search(r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT)
|
||||
content_cls = re.search(
|
||||
r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT
|
||||
)
|
||||
assert content_cls is not None
|
||||
assert "overflow-hidden" in content_cls.group(1)
|
||||
assert "overflow-y-auto" not in content_cls.group(1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue