From 6872c6e8505305e8fb6f69c0cf0e9ad4dbafd5d6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 25 Mar 2026 03:34:21 -0700 Subject: [PATCH 1/2] Remove advanced CodeQL workflow in favor of default setup (#4584) The repo has both the CodeQL "default setup" (configured in repo settings) and this advanced workflow file enabled. GitHub does not allow both simultaneously, causing all PR CI runs to fail with: "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled" Since the default setup already covers the same languages (Python, JavaScript/TypeScript) with the same build-mode (none), remove the redundant advanced workflow file. --- .github/workflows/codeql.yml | 46 ------------------------------------ 1 file changed, 46 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 61c1b96d3b..0000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: "CodeQL" - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - schedule: - - cron: "25 14 * * 3" - workflow_dispatch: - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: python - build-mode: none - - language: javascript-typescript - build-mode: none - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{ matrix.language }}" -... From be2cd7087a8b083465866b60f8359a86eaa1bab7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 25 Mar 2026 03:37:37 -0700 Subject: [PATCH 2/2] Add macOS and Linux desktop shortcuts to install.sh (#4568) * Add macOS and Linux desktop shortcuts to install.sh Adds create_studio_shortcuts() function that creates platform-native shortcuts after `unsloth studio setup` completes, mirroring the Windows shortcut behavior from PR #4558. Linux: .desktop file in ~/.local/share/applications/ and ~/Desktop/ macOS: .app bundle in ~/Applications/ with Info.plist, exec stub, and optional .icns icon built from unsloth-gem.png via sips+iconutil Both platforms share a Bash launcher script at ~/.local/share/unsloth/launch-studio.sh that provides: - Health check with service fingerprint verification - Port scanning (8888-8908) via ss/lsof - PID-file single-instance guard (no flock dependency) - Terminal spawning (macOS: Terminal.app; Linux: gnome-terminal etc.) - Browser open after health poll with 60s timeout WSL is skipped (no native desktop environment). * Fix 6 issues found by 10 parallel reviewers 1. [10/10] Health check now supports wget as fallback to curl via _http_get() helper, matching the installer's own download() pattern. Previously wget-only systems would time out on every launch. 2. [9/10] Exe path substitution now escapes sed metacharacters (&, \, |) and shell single-quotes before injection, preventing launcher corruption for paths like /opt/R&D/bin/unsloth. 3. [4/10] Linux .desktop Exec= field now quotes the launcher path, fixing launches from home directories containing spaces. 4. [3/10] macOS AppleScript command now escapes backslashes and double-quotes before interpolation into do script "...", fixing Terminal.app launch failures. 5. [3/10] Single-instance guard now uses atomic mkdir instead of racy check-then-write PID file, preventing duplicate concurrent launches on rapid double-click. 6. [1/10] Launcher now scans for a free port via _find_launch_port() instead of always hardcoding -p 8888, so Studio starts correctly when another service already occupies port 8888. Also fixed: `open` command on Linux (openvt) no longer incorrectly triggers the macOS browser-open path -- now gated on uname=Darwin. * Fix mktemp guard and exe path escaping from PR review comments Two real issues identified from automated review comments: 1. Guard mktemp -d failure in macOS icns generation. If mktemp -d returned empty, dirname would resolve to / and rm -rf would attempt to delete the root directory. Now checks that the temp dir was actually created before proceeding. 2. Replace sed-based exe path substitution with a conf file approach. The previous sed escaping broke paths containing apostrophes (e.g. /home/O'Connor/) because the '\'' escape introduced backslashes that were then double-escaped by the metacharacter pass. Now writes UNSLOTH_EXE to a separate studio.conf file that the launcher sources at runtime, eliminating all sed metacharacter and shell quoting interaction issues. This also addresses the sed -i.bak portability concern (now moot since sed is no longer used on the launcher file). * Fix unbound variable crash and per-user lock in launcher - Use ${UNSLOTH_EXE:-} so set -u does not crash before the friendly error message when studio.conf is missing or empty. - Append $(id -u) to the fallback lock path so each user gets their own lock directory when XDG_RUNTIME_DIR is unset. * Mark desktop shortcut as trusted for GNOME/Nautilus On modern GNOME desktops, chmod +x alone is not sufficient to make a .desktop file launchable by double-click on ~/Desktop. Nautilus requires the metadata::trusted attribute to be set via gio, otherwise it shows a warning dialog instead of launching the application. --- install.sh | 437 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) diff --git a/install.sh b/install.sh index 3d8e08612c..0893955939 100755 --- a/install.sh +++ b/install.sh @@ -89,6 +89,441 @@ _smart_apt_install() { fi } +# ── Helper: create desktop shortcuts and launcher script ── +# Usage: create_studio_shortcuts +# Creates ~/.local/share/unsloth/launch-studio.sh (shared launcher), +# plus platform-specific shortcuts (Linux .desktop / macOS .app bundle). +# Skipped on WSL (no native desktop). +create_studio_shortcuts() { + _css_exe="$1" + _css_os="$2" + + # Skip on WSL -- no native desktop environment + if [ "$_css_os" = "wsl" ]; then + return 0 + fi + + # Validate exe + if [ ! -x "$_css_exe" ]; then + echo "[WARN] Cannot create shortcuts: unsloth not found at $_css_exe" + return 0 + fi + + # Resolve absolute path + _css_exe_dir=$(cd "$(dirname "$_css_exe")" && pwd) + _css_exe="$_css_exe_dir/$(basename "$_css_exe")" + + _css_data_dir="$HOME/.local/share/unsloth" + _css_launcher="$_css_data_dir/launch-studio.sh" + _css_icon_png="$_css_data_dir/unsloth-studio.png" + _css_gem_png="$_css_data_dir/unsloth-gem.png" + + mkdir -p "$_css_data_dir" + + # ── Write launcher script ── + # The launcher is Bash (not POSIX sh). + # We write it with a placeholder and substitute the exe path via sed. + cat > "$_css_launcher" << 'LAUNCHER_EOF' +#!/usr/bin/env bash +# Unsloth Studio Launcher +# Auto-generated by install.sh -- do not edit manually. +set -euo pipefail + +DATA_DIR="$HOME/.local/share/unsloth" + +# Read exe path from config written at install time. +# Sourcing is safe: the config file is written by install.sh, not user input. +if [ -f "$DATA_DIR/studio.conf" ]; then + . "$DATA_DIR/studio.conf" +fi +if [ -z "${UNSLOTH_EXE:-}" ] || [ ! -x "${UNSLOTH_EXE:-}" ]; then + echo "Error: UNSLOTH_EXE not set or not executable. Re-run the installer." >&2 + exit 1 +fi + +BASE_PORT=8888 +MAX_PORT_OFFSET=20 +TIMEOUT_SEC=60 +POLL_INTERVAL_SEC=1 +LOG_FILE="$DATA_DIR/studio.log" +LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/unsloth-studio-launcher-$(id -u).lock" + +# ── HTTP GET helper (supports curl and wget) ── +_http_get() { + _url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 1 "$_url" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -qO- --timeout=1 "$_url" 2>/dev/null + else + return 1 + fi +} + +# ── Health check ── +_check_health() { + _port=$1 + _resp=$(_http_get "http://127.0.0.1:$_port/api/health") || return 1 + case "$_resp" in + *'"status"'*'"healthy"'*'"service"'*'"Unsloth UI Backend"'*) return 0 ;; + *'"service"'*'"Unsloth UI Backend"'*'"status"'*'"healthy"'*) return 0 ;; + esac + return 1 +} + +# ── Port scanning ── +_candidate_ports() { + echo "$BASE_PORT" + _max_port=$((BASE_PORT + MAX_PORT_OFFSET)) + if command -v ss >/dev/null 2>&1; then + ss -tlnH 2>/dev/null | awk '{print $4}' | grep -oE '[0-9]+$' | \ + awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true + elif command -v lsof >/dev/null 2>&1; then + lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | awk '{print $9}' | grep -oE '[0-9]+$' | \ + awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true + else + _offset=1 + while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do + echo $((BASE_PORT + _offset)) + _offset=$((_offset + 1)) + done + fi +} + +_find_healthy_port() { + for _p in $(_candidate_ports | sort -un); do + if _check_health "$_p"; then + echo "$_p" + return 0 + fi + done + return 1 +} + +# ── Check if a port is busy ── +_is_port_busy() { + _port=$1 + if command -v ss >/dev/null 2>&1; then + ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE "[.:]$_port$" + elif command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"$_port" -sTCP:LISTEN -nP >/dev/null 2>&1 + else + return 1 + fi +} + +# ── Find a free port in range ── +_find_launch_port() { + _offset=0 + while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do + _candidate=$((BASE_PORT + _offset)) + if ! _is_port_busy "$_candidate"; then + echo "$_candidate" + return 0 + fi + _offset=$((_offset + 1)) + done + return 1 +} + +# ── Open browser ── +_open_browser() { + _url="$1" + if [ "$(uname)" = "Darwin" ] && command -v open >/dev/null 2>&1; then + open "$_url" + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$_url" >/dev/null 2>&1 & + else + echo "Open in your browser: $_url" >&2 + fi +} + +# ── Spawn terminal with studio command ── +_spawn_terminal() { + _cmd="$1" + _os=$(uname) + if [ "$_os" = "Darwin" ]; then + # Escape backslashes and double-quotes for AppleScript string + _cmd_escaped=$(printf '%s' "$_cmd" | sed 's/\\/\\\\/g; s/"/\\"/g') + osascript -e "tell application \"Terminal\" to do script \"$_cmd_escaped\"" >/dev/null 2>&1 && return 0 + else + for _term in gnome-terminal konsole xfce4-terminal mate-terminal lxterminal xterm; do + if command -v "$_term" >/dev/null 2>&1; then + case "$_term" in + gnome-terminal) "$_term" -- sh -c "$_cmd" & return 0 ;; + konsole) "$_term" -e sh -c "$_cmd" & return 0 ;; + xterm) "$_term" -e sh -c "$_cmd" & return 0 ;; + *) "$_term" -e sh -c "$_cmd" & return 0 ;; + esac + fi + done + fi + # Fallback: background with log + echo "No terminal emulator found; running in background. Logs: $LOG_FILE" >&2 + nohup sh -c "$_cmd" >> "$LOG_FILE" 2>&1 & + return 0 +} + +# ── Atomic directory-based single-instance guard ── +_acquire_lock() { + if mkdir "$LOCK_DIR" 2>/dev/null; then + echo "$$" > "$LOCK_DIR/pid" + return 0 + fi + + # Lock dir exists -- check if owner is still alive + _old_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || true) + if [ -n "$_old_pid" ] && kill -0 "$_old_pid" 2>/dev/null; then + # Another launcher is running; wait for it to bring Studio up + _deadline=$(($(date +%s) + TIMEOUT_SEC)) + while [ "$(date +%s)" -lt "$_deadline" ]; do + _port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 + } + sleep "$POLL_INTERVAL_SEC" + done + echo "Timed out waiting for other launcher (PID $_old_pid)" >&2 + exit 0 + fi + + # Stale lock -- reclaim + rm -rf "$LOCK_DIR" + mkdir "$LOCK_DIR" 2>/dev/null || return 1 + echo "$$" > "$LOCK_DIR/pid" +} + +_release_lock() { + rm -rf "$LOCK_DIR" +} + +# ── Main ── +# Fast path: already healthy +_port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 +} + +_acquire_lock +trap '_release_lock' EXIT INT TERM + +# Post-lock re-check (handles race with another launcher) +_port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 +} + +# Find a free port in range +_launch_port=$(_find_launch_port) || { + echo "No free port found in range ${BASE_PORT}-$((BASE_PORT + MAX_PORT_OFFSET))" >&2 + exit 1 +} + +# Launch studio in a terminal +_launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port") +_launch_cmd=${_launch_cmd% } +_spawn_terminal "$_launch_cmd" + +# Poll for health +_deadline=$(($(date +%s) + TIMEOUT_SEC)) +while [ "$(date +%s)" -lt "$_deadline" ]; do + _port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 + } + sleep "$POLL_INTERVAL_SEC" +done + +echo "Unsloth Studio did not become healthy within ${TIMEOUT_SEC}s." >&2 +echo "Check logs at: $LOG_FILE" >&2 +exit 1 +LAUNCHER_EOF + + chmod +x "$_css_launcher" + + # Write the exe path to a separate conf file sourced by the launcher. + # Using single-quote wrapping with the standard '\'' escape for any + # embedded apostrophes. This avoids all sed metacharacter issues. + _css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g") + printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'" > "$_css_data_dir/studio.conf" + + # ── Icon: try bundled, then download ── + # favicon.png (small, for Linux) and unsloth-gem.png (large, for macOS icns) + _css_script_dir="" + if [ -n "${0:-}" ] && [ -f "$0" ]; then + _css_script_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || true + fi + + # Try to find favicon.png from installed package (site-packages) or local repo + _css_found_favicon="" + _css_found_gem="" + _css_venv_dir=$(dirname "$(dirname "$_css_exe")") + # Check site-packages + for _sp in "$_css_venv_dir"/lib/python*/site-packages/unsloth/studio/frontend/public; do + if [ -f "$_sp/favicon.png" ]; then + _css_found_favicon="$_sp/favicon.png" + fi + if [ -f "$_sp/unsloth-gem.png" ]; then + _css_found_gem="$_sp/unsloth-gem.png" + fi + done + # Check local repo (when running from clone) + if [ -z "$_css_found_favicon" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/favicon.png" ]; then + _css_found_favicon="$_css_script_dir/studio/frontend/public/favicon.png" + fi + if [ -z "$_css_found_gem" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/unsloth-gem.png" ]; then + _css_found_gem="$_css_script_dir/studio/frontend/public/unsloth-gem.png" + fi + + # Copy or download favicon.png + if [ -n "$_css_found_favicon" ]; then + cp "$_css_found_favicon" "$_css_icon_png" 2>/dev/null || true + elif [ ! -f "$_css_icon_png" ]; then + download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/favicon.png" "$_css_icon_png" 2>/dev/null || true + fi + # Copy or download unsloth-gem.png (for macOS icns) + if [ -n "$_css_found_gem" ]; then + cp "$_css_found_gem" "$_css_gem_png" 2>/dev/null || true + elif [ ! -f "$_css_gem_png" ]; then + download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth-gem.png" "$_css_gem_png" 2>/dev/null || true + fi + + # Validate PNG header (first 4 bytes: \x89PNG) + _css_validate_png() { + [ -f "$1" ] || return 1 + _hdr=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' ') + [ "$_hdr" = "89504e47" ] + } + if [ -f "$_css_icon_png" ] && ! _css_validate_png "$_css_icon_png"; then + rm -f "$_css_icon_png" + fi + if [ -f "$_css_gem_png" ] && ! _css_validate_png "$_css_gem_png"; then + rm -f "$_css_gem_png" + fi + + # ── Platform-specific shortcuts ── + _css_created=0 + + if [ "$_css_os" = "linux" ]; then + # ── Linux: .desktop file ── + _css_app_dir="$HOME/.local/share/applications" + mkdir -p "$_css_app_dir" + + _css_desktop="$_css_app_dir/unsloth-studio.desktop" + # Escape backslashes and double-quotes for .desktop Exec= field + _css_exec_escaped=$(printf '%s' "$_css_launcher" | sed 's/\\/\\\\/g; s/"/\\"/g') + _css_icon_escaped=$(printf '%s' "$_css_icon_png" | sed 's/\\/\\\\/g; s/"/\\"/g') + cat > "$_css_desktop" << DESKTOP_EOF +[Desktop Entry] +Version=1.0 +Type=Application +Name=Unsloth Studio +Comment=Launch Unsloth Studio +Exec="$_css_exec_escaped" +Icon=$_css_icon_escaped +Terminal=false +StartupNotify=true +Categories=Development;Science; +DESKTOP_EOF + chmod +x "$_css_desktop" + + # Copy to ~/Desktop if it exists + if [ -d "$HOME/Desktop" ]; then + cp "$_css_desktop" "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true + chmod +x "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true + # Mark as trusted so GNOME/Nautilus allows launching via double-click + if command -v gio >/dev/null 2>&1; then + gio set "$HOME/Desktop/unsloth-studio.desktop" metadata::trusted true 2>/dev/null || true + fi + fi + + # Best-effort update database + update-desktop-database "$_css_app_dir" 2>/dev/null || true + _css_created=1 + + elif [ "$_css_os" = "macos" ]; then + # ── macOS: .app bundle ── + _css_app="$HOME/Applications/Unsloth Studio.app" + _css_contents="$_css_app/Contents" + _css_macos_dir="$_css_contents/MacOS" + _css_res_dir="$_css_contents/Resources" + mkdir -p "$_css_macos_dir" "$_css_res_dir" + + # Info.plist + cat > "$_css_contents/Info.plist" << 'PLIST_EOF' + + + + + CFBundleIdentifier + ai.unsloth.studio + CFBundleName + Unsloth Studio + CFBundleDisplayName + Unsloth Studio + CFBundleExecutable + launch-studio + CFBundleIconFile + AppIcon + CFBundlePackageType + APPL + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + LSMinimumSystemVersion + 10.15 + NSHighResolutionCapable + + + +PLIST_EOF + + # Executable stub + cat > "$_css_macos_dir/launch-studio" << STUB_EOF +#!/bin/sh +exec "$HOME/.local/share/unsloth/launch-studio.sh" "\$@" +STUB_EOF + chmod +x "$_css_macos_dir/launch-studio" + + # Build AppIcon.icns from unsloth-gem.png (2240x2240) + if [ -f "$_css_gem_png" ] && command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then + _css_tmpdir=$(mktemp -d 2>/dev/null) + if [ -d "$_css_tmpdir" ]; then + _css_iconset="$_css_tmpdir/AppIcon.iconset" + mkdir -p "$_css_iconset" + _css_icon_ok=true + for _sz in 16 32 128 256 512; do + _sz2=$((_sz * 2)) + sips -z "$_sz" "$_sz" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}.png" >/dev/null 2>&1 || _css_icon_ok=false + sips -z "$_sz2" "$_sz2" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}@2x.png" >/dev/null 2>&1 || _css_icon_ok=false + done + if [ "$_css_icon_ok" = "true" ]; then + iconutil -c icns "$_css_iconset" -o "$_css_res_dir/AppIcon.icns" 2>/dev/null || true + fi + rm -rf "$_css_tmpdir" + fi + fi + # Fallback: copy PNG as icon + if [ ! -f "$_css_res_dir/AppIcon.icns" ] && [ -f "$_css_icon_png" ]; then + cp "$_css_icon_png" "$_css_res_dir/AppIcon.icns" 2>/dev/null || true + fi + + # Touch so Finder indexes it + touch "$_css_app" + + # Symlink on Desktop + if [ -d "$HOME/Desktop" ]; then + ln -sf "$_css_app" "$HOME/Desktop/Unsloth Studio" 2>/dev/null || true + fi + _css_created=1 + fi + + if [ "$_css_created" -eq 1 ]; then + echo "[OK] Created Unsloth Studio shortcut(s)" + fi +} + echo "" echo "=========================================" echo " Unsloth Studio Installer" @@ -251,6 +686,8 @@ echo "==> Running unsloth studio setup..." REQUESTED_PYTHON_VERSION="$(cd "$VENV_NAME/bin" && pwd)/python" \ "$VENV_NAME/bin/unsloth" studio setup