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 <danielhanchen@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
6c2bfebb20
commit
6f129a214b
11 changed files with 521 additions and 30 deletions
19
README.md
19
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
|
||||
```
|
||||
|
|
|
|||
44
build.sh
44
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
|
||||
|
|
|
|||
119
install.ps1
Normal file
119
install.ps1
Normal file
|
|
@ -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
|
||||
212
install.sh
Executable file
212
install.sh
Executable file
|
|
@ -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/tty || REPLY="y"
|
||||
else
|
||||
REPLY="y"
|
||||
fi
|
||||
case "$REPLY" in
|
||||
[nN]*)
|
||||
echo ""
|
||||
echo " Please install these packages first, then re-run Unsloth Studio setup:"
|
||||
echo " sudo apt-get update -y && sudo apt-get install -y $_STILL_MISSING"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y $_STILL_MISSING
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo ""
|
||||
echo " sudo is not available on this system."
|
||||
echo " Please install these packages as root, then re-run Unsloth Studio setup:"
|
||||
echo " apt-get update -y && apt-get install -y $_STILL_MISSING"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Unsloth Studio Installer"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# ── Detect platform ──
|
||||
OS="linux"
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
OS="macos"
|
||||
elif grep -qi microsoft /proc/version 2>/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 </dev/null
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Unsloth Studio installed!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo " To launch, run:"
|
||||
echo ""
|
||||
echo " source ${VENV_NAME}/bin/activate"
|
||||
echo " unsloth studio -H 0.0.0.0 -p 8888"
|
||||
echo ""
|
||||
|
|
@ -45,7 +45,7 @@ from loggers import get_logger
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_colab_url(port: int = 8000) -> 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 </dev/tty || REPLY="y"
|
||||
else
|
||||
REPLY="y"
|
||||
fi
|
||||
case "$REPLY" in
|
||||
[nN]*)
|
||||
echo ""
|
||||
echo " Please install these packages first, then re-run Unsloth Studio setup:"
|
||||
echo " sudo apt-get update -y && sudo apt-get install -y $_STILL_MISSING"
|
||||
_SKIP_GGUF_BUILD=true
|
||||
;;
|
||||
*)
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y $_STILL_MISSING
|
||||
echo "✅ GGUF build dependencies installed"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo " sudo is not available on this system."
|
||||
echo " Please install as root, then re-run setup:"
|
||||
echo " apt-get install -y $_STILL_MISSING"
|
||||
_SKIP_GGUF_BUILD=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 8. Build llama.cpp binaries for GGUF inference + export ──
|
||||
|
|
@ -324,6 +382,11 @@ UNSLOTH_HOME="$HOME/.unsloth"
|
|||
mkdir -p "$UNSLOTH_HOME"
|
||||
LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
|
||||
LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
|
||||
if [ "${_SKIP_GGUF_BUILD:-}" = true ]; then
|
||||
echo ""
|
||||
echo "Skipping llama-server build (missing dependencies)"
|
||||
echo " Install the missing packages and re-run setup to enable GGUF inference."
|
||||
else
|
||||
rm -rf "$LLAMA_CPP_DIR"
|
||||
{
|
||||
# Check prerequisites
|
||||
|
|
@ -442,6 +505,7 @@ rm -rf "$LLAMA_CPP_DIR"
|
|||
fi
|
||||
fi
|
||||
}
|
||||
fi # end _SKIP_GGUF_BUILD check
|
||||
|
||||
echo ""
|
||||
if [ "$IS_COLAB" = true ]; then
|
||||
|
|
@ -460,6 +524,6 @@ else
|
|||
echo "╠══════════════════════════════════════╣"
|
||||
echo "║ Launch with: ║"
|
||||
echo "║ ║"
|
||||
echo "║ unsloth studio -H 0.0.0.0 -p 8000 ║"
|
||||
echo "║ unsloth studio -H 0.0.0.0 -p 8888 ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ def _find_setup_script() -> 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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue