From 6f129a214be9b33a455aae706e9d85e06bee3fa9 Mon Sep 17 00:00:00 2001 From: Manan Shah <52329525+Manan17@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:09:09 -0500 Subject: [PATCH] Fix Install commands for Windows + 1 line installs (#4447) * One liner setup for unsloth studio * Fix install scripts: system deps, activation bugs, curl/wget support - install.sh: detect platform (macOS/Linux/WSL) and check for missing system dependencies (cmake, git, build-essential, libcurl4-openssl-dev). Prompt user once for permission to install all missing packages via brew (macOS) or sudo apt-get (Linux/WSL). Add wget fallback via download() helper since curl is not always present on minimal Linux installs. Fix nested curl|sh stdin stealing by downloading uv installer to a tempfile first. Replace venv activation (no-op in a pipe subshell) with explicit --python flag for uv pip install and direct venv binary invocation. Add idempotency guard for venv creation. Redirect stdin on unsloth studio setup to prevent pipe consumption. On macOS, check for Xcode Command Line Tools and trigger install if missing. - install.ps1: wrap script body in Install-UnslothStudio function so that errors use return instead of exit (exit kills the terminal when run via irm|iex). Remove activate.ps1 invocation entirely -- use explicit --python path for uv pip install and & $UnslothExe for studio setup. This avoids both the child-scope activation bug (& vs dot-source) and the execution policy error on default Windows systems. Add winget availability check with clear error message. Fix PATH refresh to append registry paths instead of replacing the session PATH. Add uv installer fallback via astral.sh PowerShell script if winget install does not put uv on PATH. Broaden Python version check to accept 3.11-3.13. Add idempotency guard for venv creation. - README.md: add wget one-liner alternative for systems without curl. * Fix Tailwind CSS v4 .gitignore bug on Windows (#4444) - Add .gitignore hiding workaround to setup.ps1 (matching existing setup.sh logic) so venv .gitignore files containing "*" don't prevent Tailwind's oxide scanner from finding .tsx source files - Add CSS size validation to setup.sh, setup.ps1, and build.sh to catch truncated Tailwind builds early - Remove stray force-rebuild overrides that made the "skip build if current" cache check dead code in both setup scripts - Add rm -rf dist to build.sh to force clean rebuilds for wheel packaging * Change default port 8000 to 8888, fix installer bugs, improve UX - Change default Studio port from 8000 to 8888 across all entry points (run.py, studio.py, ui.py, colab.py, vite.config.ts, setup scripts) - Update launch banner: "Launching with studio venv..." to "Launching Unsloth Studio... Please wait..." - Add "Open your web browser" banner and rename labels (Local -> Local Access, External -> Worldwide Web Address) - Fix venv idempotency: check for bin/python instead of just directory existence, clean up partial venvs on retry - Fix build.sh CSS validation: handle empty CSS case that silently bypassed the check with "integer expression expected" - Fix install.sh sudo handling: try apt-get without sudo first (works when root), then escalate with per-package tracking and user prompt - Fix install.ps1: check exit code from studio setup, fail on error - Add pciutils to WSL GGUF build dependencies - Apply same smart apt-get escalation pattern to studio/setup.sh * Use detected Python version for venv, abort on non-apt Linux - install.ps1: detect existing Python 3.11/3.12/3.13 and use that version for venv creation instead of always forcing 3.13 - install.sh: exit with error on non-apt Linux distros when required packages cannot be auto-installed, instead of silently continuing * Make sudo permission prompt more prominent with warning banner * Add Accept [Y/n] sudo prompt to studio/setup.sh for consistency * Fix native command exit code handling and sudo decline flow install.ps1: Add $LASTEXITCODE checks after winget (Python), uv venv, and uv pip install calls. $ErrorActionPreference only catches PowerShell cmdlet errors, not native executable failures. The Python check also handles winget returning non-zero for "already installed". setup.sh: Skip llama-server build when user declines sudo or sudo is unavailable. Previously the script continued to section 8 which would fail with confusing errors (e.g. "gcc: command not found") since build-essential was never installed. * Move rm -rf llama.cpp inside build branch to preserve existing install When _SKIP_GGUF_BUILD is set (user declined sudo or sudo unavailable), the previous rm -rf would destroy an already-working llama-server before the skip check ran. Move it inside the else branch so existing builds are preserved when the rebuild is skipped. --------- Co-authored-by: Daniel Han Co-authored-by: Daniel Han --- README.md | 19 ++- build.sh | 44 +++++++ install.ps1 | 119 ++++++++++++++++++ install.sh | 212 +++++++++++++++++++++++++++++++++ studio/backend/colab.py | 10 +- studio/backend/run.py | 16 ++- studio/frontend/vite.config.ts | 4 +- studio/setup.ps1 | 43 ++++++- studio/setup.sh | 76 +++++++++++- unsloth_cli/commands/studio.py | 4 +- unsloth_cli/commands/ui.py | 4 +- 11 files changed, 521 insertions(+), 30 deletions(-) create mode 100644 install.ps1 create mode 100755 install.sh diff --git a/README.md b/README.md index 6bf612df4a..583e26eef3 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,14 @@ Unsloth Studio (Beta) works on **Windows, Linux, WSL** and **macOS**. #### MacOS, Linux, WSL: For MacOS, ensure you have `cmake` installed. If not, run `brew install cmake`. ```bash +curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh +``` +If you don't have `curl`, use `wget`: +```bash +wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh +``` +Or manually: +```bash curl -LsSf https://astral.sh/uv/install.sh | sh uv venv unsloth_studio --python 3.13 source unsloth_studio/bin/activate @@ -67,9 +75,12 @@ source unsloth_studio/bin/activate unsloth studio -H 0.0.0.0 -p 8888 ``` -#### Windows: -Run in Windows Powershell: -```bash +#### Windows PowerShell (One time): +```powershell +irm https://raw.githubusercontent.com/unslothai/unsloth/main/install.ps1 | iex +``` +Or manually: +```powershell winget install -e --id Python.Python.3.13 winget install --id=astral-sh.uv -e uv venv unsloth_studio --python 3.13 @@ -79,7 +90,7 @@ unsloth studio setup unsloth studio -H 0.0.0.0 -p 8888 ``` Then to launch every time: -```bash +```powershell .\unsloth_studio\Scripts\activate unsloth studio -H 0.0.0.0 -p 8888 ``` diff --git a/build.sh b/build.sh index 4948a73ae4..3118e8810a 100644 --- a/build.sh +++ b/build.sh @@ -4,8 +4,52 @@ set -euo pipefail # 1. Build frontend (Vite outputs to dist/) cd studio/frontend + +# Clean stale dist to force a full rebuild +rm -rf dist + +# Tailwind v4's oxide scanner respects .gitignore in parent directories. +# Python venvs create a .gitignore with "*" (ignore everything), which +# prevents Tailwind from scanning .tsx source files for class names. +# Temporarily hide any such .gitignore during the build, then restore it. +_HIDDEN_GITIGNORES=() +_dir="$(pwd)" +while [ "$_dir" != "/" ]; do + _dir="$(dirname "$_dir")" + if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then + mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild" + _HIDDEN_GITIGNORES+=("$_dir/.gitignore") + fi +done + +_restore_gitignores() { + for _gi in "${_HIDDEN_GITIGNORES[@]+"${_HIDDEN_GITIGNORES[@]}"}"; do + mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true + done +} +trap _restore_gitignores EXIT + npm install npm run build # outputs to studio/frontend/dist/ + +_restore_gitignores +trap - EXIT + +# Validate CSS output -- catch truncated Tailwind builds before packaging +MAX_CSS_SIZE=$(find dist/assets -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}') +if [ -z "$MAX_CSS_SIZE" ]; then + echo "❌ ERROR: No CSS files were emitted into dist/assets." + echo " The frontend build may have failed silently." + exit 1 +fi +if [ "$MAX_CSS_SIZE" -lt 100000 ]; then + echo "❌ ERROR: Largest CSS file is only $((MAX_CSS_SIZE / 1024))KB (expected >100KB)." + echo " Tailwind may not have scanned all source files." + echo " Check for .gitignore files blocking the Tailwind oxide scanner." + exit 1 +fi +echo "✅ Frontend CSS validated (${MAX_CSS_SIZE} bytes)" + cd ../.. # 2. Clean old artifacts diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000000..0d65ef4e06 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,119 @@ +# Unsloth Studio Installer for Windows PowerShell +# Usage: irm https://raw.githubusercontent.com/unslothai/unsloth/main/install.ps1 | iex +# Local: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass; .\install.ps1 + +function Install-UnslothStudio { + $ErrorActionPreference = "Stop" + + $VenvName = "unsloth_studio" + $PythonVersion = "3.13" + + Write-Host "" + Write-Host "=========================================" + Write-Host " Unsloth Studio Installer (Windows)" + Write-Host "=========================================" + Write-Host "" + + # ── Helper: refresh PATH from registry (preserving current session entries) ── + function Refresh-SessionPath { + $machine = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + $user = [System.Environment]::GetEnvironmentVariable("Path", "User") + $env:Path = "$machine;$user;$env:Path" + } + + # ── Check winget ── + if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + Write-Host "Error: winget is not available." -ForegroundColor Red + Write-Host " Install it from https://aka.ms/getwinget" -ForegroundColor Yellow + Write-Host " or install Python $PythonVersion and uv manually, then re-run." -ForegroundColor Yellow + return + } + + # ── Install Python if no compatible version (3.11-3.13) found ── + $DetectedPythonVersion = "" + if (Get-Command python -ErrorAction SilentlyContinue) { + $pyVer = python --version 2>&1 + if ($pyVer -match "Python (3\.1[1-3])\.\d+") { + Write-Host "==> Python already installed: $pyVer" + $DetectedPythonVersion = $Matches[1] + } + } + if (-not $DetectedPythonVersion) { + Write-Host "==> Installing Python ${PythonVersion}..." + winget install -e --id Python.Python.3.13 --accept-package-agreements --accept-source-agreements + Refresh-SessionPath + if ($LASTEXITCODE -ne 0) { + # winget returns non-zero for "already installed" -- only fail if python is truly missing + if (-not (Get-Command python -ErrorAction SilentlyContinue)) { + Write-Host "[ERROR] Python installation failed (exit code $LASTEXITCODE)" -ForegroundColor Red + return + } + } + $DetectedPythonVersion = $PythonVersion + } + + # ── Install uv if not present ── + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Host "==> Installing uv package manager..." + winget install --id=astral-sh.uv -e --accept-package-agreements --accept-source-agreements + Refresh-SessionPath + # Fallback: if winget didn't put uv on PATH, try the PowerShell installer + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Host " Trying alternative uv installer..." + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + Refresh-SessionPath + } + } + + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Host "Error: uv could not be installed." -ForegroundColor Red + Write-Host " Install it from https://docs.astral.sh/uv/" -ForegroundColor Yellow + return + } + + # ── Create venv (skip if it already exists and has a valid interpreter) ── + $VenvPython = Join-Path $VenvName "Scripts\python.exe" + if (-not (Test-Path $VenvPython)) { + if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName } + Write-Host "==> Creating Python ${DetectedPythonVersion} virtual environment (${VenvName})..." + uv venv $VenvName --python $DetectedPythonVersion + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red + return + } + } else { + Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation." + } + + # ── Install unsloth directly into the venv (no activation needed) ── + Write-Host "==> Installing unsloth (this may take a few minutes)..." + uv pip install --python $VenvPython unsloth --torch-backend=auto + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red + return + } + + # ── Run studio setup ── + # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, + # CUDA Toolkit, Node.js, and other dependencies automatically via winget. + Write-Host "==> Running unsloth studio setup..." + $UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe" + & $UnslothExe studio setup + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red + return + } + + Write-Host "" + Write-Host "=========================================" + Write-Host " Unsloth Studio installed!" + Write-Host "=========================================" + Write-Host "" + Write-Host " To launch, run:" + Write-Host "" + Write-Host " .\${VenvName}\Scripts\activate" + Write-Host " unsloth studio -H 0.0.0.0 -p 8888" + Write-Host "" +} + +Install-UnslothStudio diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000..5e846a2feb --- /dev/null +++ b/install.sh @@ -0,0 +1,212 @@ +#!/bin/sh +# Unsloth Studio Installer +# Usage (curl): curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh +# Usage (wget): wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh +set -e + +VENV_NAME="unsloth_studio" +PYTHON_VERSION="3.13" + +# ── Helper: download a URL to a file (supports curl and wget) ── +download() { + if command -v curl >/dev/null 2>&1; then + curl -LsSf "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then + wget -qO "$2" "$1" + else + echo "Error: neither curl nor wget found. Install one and re-run." + exit 1 + fi +} + +# ── Helper: check if a single package is available on the system ── +_is_pkg_installed() { + case "$1" in + build-essential) command -v gcc >/dev/null 2>&1 ;; + libcurl4-openssl-dev) + command -v dpkg >/dev/null 2>&1 && dpkg -s "$1" >/dev/null 2>&1 ;; + pciutils) + command -v lspci >/dev/null 2>&1 ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac +} + +# ── Helper: install packages via apt, escalating to sudo only if needed ── +# Usage: _smart_apt_install pkg1 pkg2 pkg3 ... +_smart_apt_install() { + _PKGS="$*" + + # Step 1: Try installing without sudo (works when already root) + apt-get update -y >/dev/null 2>&1 || true + apt-get install -y $_PKGS >/dev/null 2>&1 || true + + # Step 2: Check which packages are still missing + _STILL_MISSING="" + for _pkg in $_PKGS; do + if ! _is_pkg_installed "$_pkg"; then + _STILL_MISSING="$_STILL_MISSING $_pkg" + fi + done + _STILL_MISSING=$(echo "$_STILL_MISSING" | sed 's/^ *//') + + if [ -z "$_STILL_MISSING" ]; then + return 0 + fi + + # Step 3: Escalate -- need elevated permissions for remaining packages + if command -v sudo >/dev/null 2>&1; then + echo "" + echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo " WARNING: We require sudo elevated permissions to install:" + echo " $_STILL_MISSING" + echo " If you accept, we'll run sudo now, and it'll prompt your password." + echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "" + printf " Accept? [Y/n] " + if [ -r /dev/tty ]; then + read -r REPLY /dev/null; then + OS="wsl" +fi +echo "==> Platform: $OS" + +# ── Check system dependencies ── +# cmake and git are needed by unsloth studio setup to build the GGUF inference +# engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux. +MISSING="" + +command -v cmake >/dev/null 2>&1 || MISSING="$MISSING cmake" +command -v git >/dev/null 2>&1 || MISSING="$MISSING git" + +case "$OS" in + macos) + # Xcode Command Line Tools provide the C/C++ compiler + if ! xcode-select -p >/dev/null 2>&1; then + echo "" + echo "==> Xcode Command Line Tools are required." + echo " Installing (a system dialog will appear)..." + xcode-select --install 2>/dev/null || true + echo " After the installation completes, please re-run this script." + exit 1 + fi + ;; + linux|wsl) + # curl or wget is needed for downloads; check both + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + MISSING="$MISSING curl" + fi + command -v gcc >/dev/null 2>&1 || MISSING="$MISSING build-essential" + # libcurl dev headers for llama.cpp HTTPS support + if command -v dpkg >/dev/null 2>&1; then + dpkg -s libcurl4-openssl-dev >/dev/null 2>&1 || MISSING="$MISSING libcurl4-openssl-dev" + fi + ;; +esac + +MISSING=$(echo "$MISSING" | sed 's/^ *//') + +if [ -n "$MISSING" ]; then + echo "" + echo "==> Unsloth Studio needs these packages: $MISSING" + echo " These are needed to build the GGUF inference engine." + + case "$OS" in + macos) + if ! command -v brew >/dev/null 2>&1; then + echo "" + echo " Homebrew is required to install them." + echo " Install Homebrew from https://brew.sh then re-run this script." + exit 1 + fi + brew install $MISSING + ;; + linux|wsl) + if command -v apt-get >/dev/null 2>&1; then + _smart_apt_install $MISSING + else + echo " apt-get is not available. Please install with your package manager:" + echo " $MISSING" + echo " Then re-run Unsloth Studio setup." + exit 1 + fi + ;; + esac + echo "" +else + echo "==> All system dependencies found." +fi + +# ── Install uv ── +if ! command -v uv >/dev/null 2>&1; then + echo "==> Installing uv package manager..." + _uv_tmp=$(mktemp) + download "https://astral.sh/uv/install.sh" "$_uv_tmp" + sh "$_uv_tmp" + rm -f "$_uv_tmp" + if [ -f "$HOME/.local/bin/env" ]; then + . "$HOME/.local/bin/env" + fi + export PATH="$HOME/.local/bin:$PATH" +fi + +# ── Create venv (skip if it already exists and has a valid interpreter) ── +if [ ! -x "$VENV_NAME/bin/python" ]; then + [ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME" + echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..." + uv venv "$VENV_NAME" --python "$PYTHON_VERSION" +else + echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation." +fi + +# ── Install unsloth directly into the venv (no activation needed) ── +echo "==> Installing unsloth (this may take a few minutes)..." +uv pip install --python "$VENV_NAME/bin/python" unsloth --torch-backend=auto + +# ── Run studio setup ── +echo "==> Running unsloth studio setup..." +"$VENV_NAME/bin/unsloth" studio setup str: +def get_colab_url(port: int = 8888) -> str: """ Get the actual Colab proxy URL for a port. """ @@ -60,7 +60,7 @@ def get_colab_url(port: int = 8000) -> str: return f"http://localhost:{port}" -def show_link(port: int = 8000): +def show_link(port: int = 8888): """Display a styled clickable link to the UI.""" from IPython.display import display, HTML @@ -68,8 +68,8 @@ def show_link(port: int = 8000): url = get_colab_url(port) short_url = ( - url[: url.index("-", url.index("8000-") + 5) + 1] + "..." - if "8000-" in url + url[: url.index("-", url.index(f"{port}-") + len(str(port)) + 1) + 1] + "..." + if f"{port}-" in url else url ) html = f""" @@ -96,7 +96,7 @@ def show_link(port: int = 8000): display(HTML(html)) -def start(port: int = 8000): +def start(port: int = 8888): """ Start Unsloth Studio server in Colab and display the URL. diff --git a/studio/backend/run.py b/studio/backend/run.py index 2f064ab92e..5388d3148b 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -155,7 +155,7 @@ _shutdown_event = None def run_server( host: str = "0.0.0.0", - port: int = 8000, + port: int = 8888, frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist", silent: bool = False, ): @@ -224,13 +224,17 @@ def run_server( if not silent: display_host = _resolve_external_ip() if host == "0.0.0.0" else host + print("") + print("=" * 50) + print(f"🦥 Open your web browser, and enter http://localhost:{port}") + print("=" * 50) print("") print("=" * 50) print(f"🦥 Unsloth Studio is running on port {port}") - print(f" Local: http://localhost:{port}") - print(f" External: http://{display_host}:{port}") - print(f" API: http://{display_host}:{port}/api") - print(f" Health: http://{display_host}:{port}/api/health") + print(f" Local Access: http://localhost:{port}") + print(f" Worldwide Web Address: http://{display_host}:{port}") + print(f" API: http://{display_host}:{port}/api") + print(f" Health: http://{display_host}:{port}/api/health") print("=" * 50) return app @@ -243,7 +247,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description = "Run Unsloth UI Backend server") parser.add_argument("--host", default = "0.0.0.0", help = "Host to bind to") - parser.add_argument("--port", type = int, default = 8000, help = "Port to bind to") + parser.add_argument("--port", type = int, default = 8888, help = "Port to bind to") parser.add_argument( "--frontend", type = str, diff --git a/studio/frontend/vite.config.ts b/studio/frontend/vite.config.ts index 6e9a03df95..8d34383a48 100644 --- a/studio/frontend/vite.config.ts +++ b/studio/frontend/vite.config.ts @@ -17,11 +17,11 @@ export default defineConfig({ allowedHosts: true, proxy: { "/api": { - target: "http://127.0.0.1:8000", + target: "http://127.0.0.1:8888", changeOrigin: true, }, "/v1": { - target: "http://127.0.0.1:8000", + target: "http://127.0.0.1:8888", changeOrigin: true, }, "/seed/inspect": { diff --git a/studio/setup.ps1 b/studio/setup.ps1 index c768c066af..7091920ab9 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -855,10 +855,31 @@ if ($IsPipInstall) { Write-Host "[INFO] Frontend source changed since last build -- rebuilding..." -ForegroundColor Yellow } } -$NeedFrontendBuild = $true if ($NeedFrontendBuild -and -not $IsPipInstall) { Write-Host "" Write-Host "Building frontend..." -ForegroundColor Cyan + + # ── Tailwind v4 .gitignore workaround ── + # Tailwind v4's oxide scanner respects .gitignore in parent directories. + # Python venvs create a .gitignore with "*" (ignore everything), which + # prevents Tailwind from scanning .tsx source files for class names. + # Temporarily hide any such .gitignore during the build, then restore it. + $HiddenGitignores = @() + $WalkDir = (Get-Item $FrontendDir).Parent.FullName + while ($WalkDir -and $WalkDir -ne [System.IO.Path]::GetPathRoot($WalkDir)) { + $gi = Join-Path $WalkDir ".gitignore" + if (Test-Path $gi) { + $content = Get-Content $gi -Raw -ErrorAction SilentlyContinue + if ($content -and ($content.Trim() -match '^\*$')) { + $hidden = "$gi._twbuild" + Rename-Item -Path $gi -NewName (Split-Path $hidden -Leaf) -Force + $HiddenGitignores += $gi + Write-Host " [INFO] Temporarily hiding $gi (venv .gitignore blocks Tailwind scanner)" -ForegroundColor DarkGray + } + } + $WalkDir = Split-Path $WalkDir -Parent + } + # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't # treat them as terminating errors (same pattern as the pip section below). $prevEAP_npm = $ErrorActionPreference @@ -868,6 +889,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { if ($LASTEXITCODE -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm + foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow exit 1 @@ -876,12 +898,27 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { if ($LASTEXITCODE -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm + foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red exit 1 } Pop-Location $ErrorActionPreference = $prevEAP_npm - Write-Host "[OK] Frontend built to frontend/dist" -ForegroundColor Green + + # ── Restore hidden .gitignore files ── + foreach ($gi in $HiddenGitignores) { + Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue + } + + # ── Validate CSS output ── + $CssFiles = Get-ChildItem (Join-Path $DistDir "assets") -Filter "*.css" -ErrorAction SilentlyContinue + $MaxCssSize = ($CssFiles | Measure-Object -Property Length -Maximum).Maximum + if ($MaxCssSize -lt 100000) { + Write-Host "[WARN] Largest CSS file is only $([math]::Round($MaxCssSize / 1024))KB -- Tailwind may not have scanned all source files." -ForegroundColor Yellow + Write-Host " Expected >100KB. Check for .gitignore files blocking the Tailwind oxide scanner." -ForegroundColor Yellow + } else { + Write-Host "[OK] Frontend built to frontend/dist (CSS: $([math]::Round($MaxCssSize / 1024))KB)" -ForegroundColor Green + } } if (Test-Path $OxcValidatorDir) { @@ -1374,6 +1411,6 @@ Write-Host "+===============================================+" -ForegroundColor Write-Host "| Setup Complete! |" -ForegroundColor Green Write-Host "| |" -ForegroundColor Green Write-Host "| Launch with: |" -ForegroundColor Green -Write-Host "| unsloth studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green +Write-Host "| unsloth studio -H 0.0.0.0 -p 8888 |" -ForegroundColor Green Write-Host "| |" -ForegroundColor Green Write-Host "+===============================================+" -ForegroundColor Green diff --git a/studio/setup.sh b/studio/setup.sh index 02c49afc15..3e56aebf38 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -59,7 +59,6 @@ if [ -d "$SCRIPT_DIR/frontend/dist" ]; then _NEED_FRONTEND_BUILD=false fi fi -_NEED_FRONTEND_BUILD=true if [ "$_NEED_FRONTEND_BUILD" = false ]; then echo "✅ Frontend already built and up to date -- skipping Node/npm check." else @@ -160,6 +159,16 @@ run_quiet "npm run build" npm run build _restore_gitignores trap - EXIT + +# Validate CSS output -- catch truncated Tailwind builds +_MAX_CSS=$(find "$SCRIPT_DIR/frontend/dist/assets" -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}') +if [ -z "$_MAX_CSS" ]; then + echo "⚠️ WARNING: No CSS files were emitted. The frontend build may have failed." +elif [ "$_MAX_CSS" -lt 100000 ]; then + echo "⚠️ WARNING: Largest CSS file is only $((_MAX_CSS / 1024))KB (expected >100KB)." + echo " Tailwind may not have scanned all source files. Check for .gitignore interference." +fi + cd "$SCRIPT_DIR" echo "✅ Frontend built to frontend/dist" @@ -308,10 +317,59 @@ echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/" if grep -qi microsoft /proc/version 2>/dev/null; then echo "" echo "⚠️ WSL detected -- installing build dependencies for GGUF export..." - echo " You may be prompted for your password." - sudo apt-get update -y - sudo apt-get install -y build-essential cmake curl git libcurl4-openssl-dev - echo "✅ GGUF build dependencies installed" + _GGUF_DEPS="pciutils build-essential cmake curl git libcurl4-openssl-dev" + + # Try without sudo first (works when already root) + apt-get update -y >/dev/null 2>&1 || true + apt-get install -y $_GGUF_DEPS >/dev/null 2>&1 || true + + # Check which packages are still missing + _STILL_MISSING="" + for _pkg in $_GGUF_DEPS; do + case "$_pkg" in + build-essential) command -v gcc >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;; + pciutils) command -v lspci >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;; + libcurl4-openssl-dev) dpkg -s "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;; + *) command -v "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;; + esac + done + _STILL_MISSING=$(echo "$_STILL_MISSING" | sed 's/^ *//') + + if [ -z "$_STILL_MISSING" ]; then + echo "✅ GGUF build dependencies installed" + elif command -v sudo >/dev/null 2>&1; then + echo "" + echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo " WARNING: We require sudo elevated permissions to install:" + echo " $_STILL_MISSING" + echo " If you accept, we'll run sudo now, and it'll prompt your password." + echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "" + printf " Accept? [Y/n] " + if [ -r /dev/tty ]; then + read -r REPLY Optional[Path]: @studio_app.callback(invoke_without_command = True) def studio_default( ctx: typer.Context, - port: int = typer.Option(8000, "--port", "-p"), + port: int = typer.Option(8888, "--port", "-p"), host: str = typer.Option("0.0.0.0", "--host", "-H"), frontend: Optional[Path] = typer.Option(None, "--frontend", "-f"), silent: bool = typer.Option(False, "--silent", "-q"), @@ -93,7 +93,7 @@ def studio_default( run_py = _find_run_py() if studio_python and run_py: if not silent: - typer.echo("Launching with studio venv...") + typer.echo("Launching Unsloth Studio... Please wait...") args = [ str(studio_python), str(run_py), diff --git a/unsloth_cli/commands/ui.py b/unsloth_cli/commands/ui.py index eb6a8a69e8..4457465e96 100644 --- a/unsloth_cli/commands/ui.py +++ b/unsloth_cli/commands/ui.py @@ -12,7 +12,7 @@ import typer def ui( port: int = typer.Option( - 8000, "--port", "-p", help = "Port to run the UI server on." + 8888, "--port", "-p", help = "Port to run the UI server on." ), host: str = typer.Option( "0.0.0.0", "--host", "-H", help = "Host address to bind to." @@ -40,7 +40,7 @@ def ui( run_py = _find_run_py() if studio_python and run_py: if not silent: - typer.echo("Launching with studio venv...") + typer.echo("Launching Unsloth Studio... Please wait...") args = [ str(studio_python), str(run_py),