Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
```
Expand Down
44 changes: 44 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude wc totals when validating frontend CSS size

The new CSS guard computes MAX_CSS_SIZE with find ... -exec wc -c {} + | sort -n | tail -1, but wc adds a total row when multiple files are passed at once. That means this check compares against aggregate CSS bytes, not the largest single file as intended, so a truncated Tailwind build split across several small files can still pass packaging if their sum exceeds 100KB.

Useful? React with 👍 / 👎.

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
Expand Down
119 changes: 119 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +25 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exit PowerShell installer non-zero on fatal failures

This failure path returns from Install-UnslothStudio instead of terminating with a non-zero status, so callers can observe a successful command completion even when install prerequisites are missing. That matters for scripted installs (CI/bootstrap scripts) that rely on exit codes to detect failure. Use throw/exit 1 (or propagate an explicit non-zero return code) for fatal branches to avoid silent false-success outcomes.

Useful? React with 👍 / 👎.

}

# ── 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail install when studio setup returns non-zero

This script relies on $ErrorActionPreference = "Stop", but native executables do not automatically stop the script on non-zero exit in standard PowerShell behavior, and this call does not check $LASTEXITCODE. If unsloth studio setup fails (for example due to a dependency install failure), the script can still continue to the success banner, giving users a false-positive "installed" result; add an explicit exit-code check immediately after this command.

Useful? React with 👍 / 👎.

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
Loading
Loading