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.
This commit is contained in:
parent
6872c6e850
commit
be2cd7087a
1 changed files with 437 additions and 0 deletions
437
install.sh
437
install.sh
|
|
@ -89,6 +89,441 @@ _smart_apt_install() {
|
|||
fi
|
||||
}
|
||||
|
||||
# ── Helper: create desktop shortcuts and launcher script ──
|
||||
# Usage: create_studio_shortcuts <unsloth_exe> <os>
|
||||
# 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'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ai.unsloth.studio</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Unsloth Studio</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Unsloth Studio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launch-studio</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
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 </dev/null
|
||||
|
||||
create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Unsloth Studio installed!"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue