Created
January 3, 2026 03:20
-
-
Save 22Pizzas/b024028868fb936499db00b936bc3b6d to your computer and use it in GitHub Desktop.
Secure LM Studio Linux Installer/Updater – extracts AppImage, fixes chrome-sandbox (SUID), creates desktop entry & symlinks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| # ------------------------------- | |
| # Configuration | |
| # ------------------------------- | |
| readonly INSTALL_DIR="${HOME}/.local/share/lm-studio" | |
| readonly BIN_DIR="${HOME}/.local/bin" | |
| readonly DESKTOP_DIR="${HOME}/.local/share/applications" | |
| readonly EXTRACT_DIR="squashfs-root" | |
| # Colors for output | |
| readonly RED='\033[0;31m' | |
| readonly GREEN='\033[0;32m' | |
| readonly YELLOW='\033[1;33m' | |
| readonly NC='\033[0m' # No Color | |
| # ------------------------------- | |
| # Cleanup handler | |
| # ------------------------------- | |
| TEMP_FILES=() | |
| cleanup() { | |
| local exit_code=$? | |
| if [ ${#TEMP_FILES[@]} -gt 0 ]; then | |
| echo "Cleaning up temporary files..." >&2 | |
| for file in "${TEMP_FILES[@]}"; do | |
| rm -rf "$file" 2>/dev/null || true | |
| done | |
| fi | |
| if [ $exit_code -ne 0 ]; then | |
| echo -e "${RED}Installation failed. Temporary files have been cleaned up.${NC}" >&2 | |
| fi | |
| exit $exit_code | |
| } | |
| trap cleanup EXIT INT TERM | |
| # ------------------------------- | |
| # Utility functions | |
| # ------------------------------- | |
| log_info() { | |
| echo -e "${GREEN}ℹ${NC} $*" >&2 | |
| } | |
| log_warn() { | |
| echo -e "${YELLOW}⚠${NC} $*" >&2 | |
| } | |
| log_error() { | |
| echo -e "${RED}✗${NC} $*" >&2 | |
| } | |
| log_success() { | |
| echo -e "${GREEN}✓${NC} $*" >&2 | |
| } | |
| # ------------------------------- | |
| # Dependencies check | |
| # ------------------------------- | |
| check_dependencies() { | |
| log_info "Checking dependencies..." | |
| local missing_deps=() | |
| for cmd in curl file sudo wget; do | |
| if ! command -v "$cmd" >/dev/null 2>&1; then | |
| missing_deps+=("$cmd") | |
| fi | |
| done | |
| if [ ${#missing_deps[@]} -gt 0 ]; then | |
| log_error "Missing required dependencies: ${missing_deps[*]}" | |
| echo "Please install them using your package manager." >&2 | |
| exit 1 | |
| fi | |
| # Check if aria2c is available for faster downloads | |
| if command -v aria2c >/dev/null 2>&1; then | |
| USE_ARIA2=true | |
| log_info "aria2c detected - will use for faster downloads" | |
| else | |
| USE_ARIA2=false | |
| fi | |
| } | |
| # ------------------------------- | |
| # Detect architecture | |
| # ------------------------------- | |
| detect_architecture() { | |
| local arch | |
| arch=$(uname -m) | |
| case "$arch" in | |
| x86_64) | |
| echo "x64" | |
| ;; | |
| aarch64) | |
| echo "arm64" | |
| ;; | |
| *) | |
| log_error "Unsupported architecture: $arch" | |
| echo "Only x86_64 and aarch64 are supported." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| # ------------------------------- | |
| # Validate version format | |
| # ------------------------------- | |
| validate_version() { | |
| local version="$1" | |
| # Version should match pattern like: 0.3.24-6 or 0.3.24 | |
| if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then | |
| log_error "Invalid version format: $version" | |
| echo "Expected format: X.Y.Z or X.Y.Z-N (e.g., 0.3.24 or 0.3.24-6)" >&2 | |
| return 1 | |
| fi | |
| # Additional sanitization: ensure no path traversal attempts | |
| if [[ "$version" == *".."* ]] || [[ "$version" == *"/"* ]]; then | |
| log_error "Version contains invalid characters" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # ------------------------------- | |
| # Prompt for version | |
| # ------------------------------- | |
| prompt_version() { | |
| echo "" >&2 | |
| echo "Enter the LM Studio version you want to install (e.g., 0.3.24-6):" >&2 | |
| echo "You can find available versions at: https://github.com/lmstudio-ai" >&2 | |
| echo "" >&2 | |
| read -r version | |
| if [ -z "$version" ]; then | |
| log_error "No version entered." | |
| exit 1 | |
| fi | |
| if ! validate_version "$version"; then | |
| exit 1 | |
| fi | |
| echo "$version" | |
| } | |
| # ------------------------------- | |
| # Enhanced download validation | |
| # ------------------------------- | |
| validate_download() { | |
| local file="$1" | |
| # Check if file exists and is not empty | |
| if [ ! -s "$file" ]; then | |
| log_error "Downloaded file is empty or does not exist" | |
| return 1 | |
| fi | |
| # Check if it's an HTML error page | |
| if file "$file" | grep -q "HTML"; then | |
| log_error "Download failed - received HTML instead of AppImage" | |
| log_error "The version may not exist or the URL may be incorrect" | |
| return 1 | |
| fi | |
| # Check for ELF magic bytes (AppImages are ELF executables) | |
| local magic | |
| magic=$(head -c 4 "$file" | od -An -tx1 | tr -d ' \n') | |
| if [[ ! "$magic" =~ ^7f454c46 ]]; then | |
| log_error "Downloaded file is not a valid ELF executable" | |
| log_error "Expected AppImage format (ELF), got magic bytes: $magic" | |
| return 1 | |
| fi | |
| log_success "Download validation passed" | |
| return 0 | |
| } | |
| # ------------------------------- | |
| # Display security warning | |
| # ------------------------------- | |
| show_security_warning() { | |
| echo "" >&2 | |
| log_warn "═══════════════════════════════════════════════════════════════" | |
| log_warn " SECURITY NOTICE" | |
| log_warn "═══════════════════════════════════════════════════════════════" | |
| echo "" >&2 | |
| echo "This script will:" >&2 | |
| echo " 1. Download LM Studio from installers.lmstudio.ai (unverified)" >&2 | |
| echo " 2. Set SUID bit on chrome-sandbox (requires sudo password)" >&2 | |
| echo "" >&2 | |
| echo "⚠ LM Studio does NOT provide checksums or GPG signatures" >&2 | |
| echo "⚠ You are trusting the download source and granting root privileges" >&2 | |
| echo "" >&2 | |
| echo "The SUID sandbox is required for Electron security isolation." >&2 | |
| echo "Without it, LM Studio would run with --no-sandbox (LESS secure)." >&2 | |
| echo "" >&2 | |
| log_warn "═══════════════════════════════════════════════════════════════" | |
| echo "" >&2 | |
| read -p "Do you understand and want to continue? (yes/no): " -r response | |
| if [[ ! "$response" =~ ^[Yy][Ee][Ss]$ ]]; then | |
| log_info "Installation cancelled by user" | |
| exit 0 | |
| fi | |
| } | |
| # ------------------------------- | |
| # Check existing installation | |
| # ------------------------------- | |
| check_existing_installation() { | |
| local version="$1" | |
| local version_file="${INSTALL_DIR}/.installed_version" | |
| if [ -d "$INSTALL_DIR" ]; then | |
| if [ -f "$version_file" ]; then | |
| local installed_version | |
| installed_version=$(cat "$version_file") | |
| if [ "$installed_version" = "$version" ]; then | |
| log_warn "Version $version is already installed." | |
| read -p "Do you want to reinstall it? (y/n): " -r reinstall | |
| if [[ ! "$reinstall" =~ ^[Yy]$ ]]; then | |
| log_info "Installation cancelled" | |
| exit 0 | |
| fi | |
| log_info "Reinstalling version $version..." | |
| else | |
| log_info "Upgrading from version $installed_version to $version" | |
| fi | |
| else | |
| log_info "Existing installation found without version info" | |
| fi | |
| log_info "Removing old installation..." | |
| rm -rf "${INSTALL_DIR:?}" | |
| fi | |
| } | |
| # ------------------------------- | |
| # Download AppImage | |
| # ------------------------------- | |
| download_appimage() { | |
| local version="$1" | |
| local arch="$2" | |
| local appimage_file="LM-Studio-${version}-${arch}.AppImage" | |
| local download_url="https://installers.lmstudio.ai/linux/${arch}/${version}/${appimage_file}" | |
| log_info "Downloading LM Studio v${version} for ${arch}..." | |
| echo "URL: $download_url" >&2 | |
| echo "" >&2 | |
| # Download to temporary file | |
| local temp_file | |
| temp_file=$(mktemp) | |
| TEMP_FILES+=("$temp_file") | |
| local download_success=false | |
| # Try aria2c first if available | |
| if $USE_ARIA2; then | |
| log_info "Attempting download with aria2c..." | |
| if aria2c -x 8 -s 8 --allow-overwrite=true -d "$(dirname "$temp_file")" -o "$(basename "$temp_file")" "$download_url" >&2; then | |
| download_success=true | |
| log_success "Download completed with aria2c" | |
| else | |
| log_warn "aria2c download failed, falling back to wget..." | |
| # Clean up any partial download | |
| rm -f "$temp_file" | |
| temp_file=$(mktemp) | |
| TEMP_FILES+=("$temp_file") | |
| fi | |
| fi | |
| # Fall back to wget if aria2c failed or wasn't available | |
| if ! $download_success; then | |
| log_info "Downloading with wget..." | |
| if wget --show-progress -O "$temp_file" "$download_url" >&2; then | |
| download_success=true | |
| log_success "Download completed with wget" | |
| else | |
| log_error "Download failed with wget" | |
| return 1 | |
| fi | |
| fi | |
| # Validate download | |
| if ! validate_download "$temp_file"; then | |
| return 1 | |
| fi | |
| # Move to final location | |
| mv "$temp_file" "$appimage_file" | |
| TEMP_FILES=("${TEMP_FILES[@]/$temp_file}") # Remove from cleanup list | |
| echo "$appimage_file" | |
| } | |
| # ------------------------------- | |
| # Extract and install | |
| # ------------------------------- | |
| extract_and_install() { | |
| local appimage_file="$1" | |
| local version="$2" | |
| log_info "Extracting AppImage..." | |
| chmod +x "$appimage_file" | |
| TEMP_FILES+=("$appimage_file") | |
| # Extract in current directory | |
| ./"$appimage_file" --appimage-extract >/dev/null 2>&1 || { | |
| log_error "Failed to extract AppImage" | |
| return 1 | |
| } | |
| TEMP_FILES+=("$EXTRACT_DIR") | |
| # Move to install directory | |
| log_info "Installing to ${INSTALL_DIR}..." | |
| mkdir -p "$(dirname "$INSTALL_DIR")" | |
| mv "$EXTRACT_DIR" "$INSTALL_DIR" | |
| TEMP_FILES=("${TEMP_FILES[@]/$EXTRACT_DIR}") # Remove from cleanup | |
| # Fix chrome-sandbox permissions | |
| log_info "Configuring chrome-sandbox (requires sudo)..." | |
| echo "This sets SUID bit for Electron's security sandbox." >&2 | |
| local sandbox="${INSTALL_DIR}/chrome-sandbox" | |
| if [ -f "$sandbox" ]; then | |
| sudo chown root:root "$sandbox" || { | |
| log_error "Failed to set chrome-sandbox ownership" | |
| return 1 | |
| } | |
| sudo chmod 4755 "$sandbox" || { | |
| log_error "Failed to set chrome-sandbox permissions" | |
| return 1 | |
| } | |
| log_success "Chrome-sandbox configured successfully" | |
| else | |
| log_warn "chrome-sandbox not found - this may cause issues" | |
| fi | |
| # Store installed version | |
| echo "$version" > "${INSTALL_DIR}/.installed_version" | |
| } | |
| # ------------------------------- | |
| # Create symlinks | |
| # ------------------------------- | |
| create_symlinks() { | |
| log_info "Creating symlinks in ${BIN_DIR}..." | |
| # Ensure ~/.local/bin exists and is in PATH | |
| mkdir -p "$BIN_DIR" | |
| # Create symlink for main executable | |
| ln -sf "${INSTALL_DIR}/lm-studio" "${BIN_DIR}/lm-studio" | |
| log_success "Created: lm-studio" | |
| # Create symlink for CLI if it exists | |
| if [ -f "${INSTALL_DIR}/lms" ]; then | |
| ln -sf "${INSTALL_DIR}/lms" "${BIN_DIR}/lms" | |
| log_success "Created: lms (CLI)" | |
| fi | |
| # Check if ~/.local/bin is in PATH | |
| if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then | |
| log_warn "~/.local/bin is not in your PATH" | |
| echo "" >&2 | |
| echo "Add this line to your ~/.bashrc or ~/.zshrc:" >&2 | |
| echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2 | |
| echo "" >&2 | |
| fi | |
| } | |
| # ------------------------------- | |
| # Create desktop entry | |
| # ------------------------------- | |
| create_desktop_entry() { | |
| log_info "Creating desktop entry..." | |
| mkdir -p "$DESKTOP_DIR" | |
| local desktop_file="${DESKTOP_DIR}/lm-studio.desktop" | |
| # Find the actual icon location (it varies between versions) | |
| local icon_path | |
| if [ -f "${INSTALL_DIR}/lm-studio.png" ]; then | |
| # Icon in root directory (symlink) | |
| icon_path="${INSTALL_DIR}/lm-studio.png" | |
| elif [ -f "${INSTALL_DIR}/usr/share/icons/hicolor/0x0/apps/lm-studio.png" ]; then | |
| # Icon in 0x0 directory | |
| icon_path="${INSTALL_DIR}/usr/share/icons/hicolor/0x0/apps/lm-studio.png" | |
| elif [ -f "${INSTALL_DIR}/usr/share/icons/hicolor/256x256/apps/lm-studio.png" ]; then | |
| # Icon in 256x256 directory | |
| icon_path="${INSTALL_DIR}/usr/share/icons/hicolor/256x256/apps/lm-studio.png" | |
| else | |
| # Fallback: search for any lm-studio icon | |
| icon_path=$(find "${INSTALL_DIR}" -name "lm-studio.png" -type f 2>/dev/null | head -1) | |
| if [ -z "$icon_path" ]; then | |
| log_warn "Icon file not found - using icon name fallback" | |
| icon_path="lm-studio" | |
| else | |
| log_success "Found icon at: $icon_path" | |
| fi | |
| fi | |
| cat > "$desktop_file" <<EOF | |
| [Desktop Entry] | |
| Version=1.0 | |
| Type=Application | |
| Name=LM Studio | |
| Comment=Run LLMs locally on your computer | |
| Exec=${BIN_DIR}/lm-studio | |
| Icon=${icon_path} | |
| Terminal=false | |
| Categories=Development;AI;Science; | |
| StartupWMClass=lm-studio | |
| Keywords=AI;LLM;ML; | |
| EOF | |
| chmod +x "$desktop_file" | |
| # Update desktop database if available | |
| if command -v update-desktop-database >/dev/null 2>&1; then | |
| update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true | |
| fi | |
| log_success "Desktop entry created" | |
| } | |
| # ------------------------------- | |
| # Main installation flow | |
| # ------------------------------- | |
| main() { | |
| echo "" >&2 | |
| echo "╔════════════════════════════════════════════════════════════╗" >&2 | |
| echo "║ LM Studio Linux Installer/Updater (Enhanced) ║" >&2 | |
| echo "╚════════════════════════════════════════════════════════════╝" >&2 | |
| echo "" >&2 | |
| # Check dependencies | |
| check_dependencies | |
| # Detect architecture | |
| local arch | |
| arch=$(detect_architecture) | |
| log_success "Detected architecture: $arch" | |
| # Get version from user | |
| local version | |
| version=$(prompt_version) | |
| log_success "Version: $version" | |
| # Show security warning | |
| show_security_warning | |
| # Check existing installation | |
| check_existing_installation "$version" | |
| # Download AppImage | |
| local appimage_file | |
| appimage_file=$(download_appimage "$version" "$arch") | |
| # Extract and install | |
| extract_and_install "$appimage_file" "$version" | |
| # Create symlinks | |
| create_symlinks | |
| # Create desktop entry | |
| create_desktop_entry | |
| # Success message | |
| echo "" >&2 | |
| echo "╔════════════════════════════════════════════════════════════╗" >&2 | |
| echo "║ Installation Complete! ║" >&2 | |
| echo "╚════════════════════════════════════════════════════════════╝" >&2 | |
| echo "" >&2 | |
| log_success "LM Studio v${version} installed successfully!" | |
| echo "" >&2 | |
| echo "Installation location: ${INSTALL_DIR}" >&2 | |
| echo "" >&2 | |
| echo "To run LM Studio:" >&2 | |
| echo " • Type: lm-studio" >&2 | |
| echo " • Or launch from your application menu" >&2 | |
| if [ -f "${BIN_DIR}/lms" ]; then | |
| echo " • Use CLI: lms" >&2 | |
| fi | |
| echo "" >&2 | |
| } | |
| # ------------------------------- | |
| # Run main function | |
| # ------------------------------- | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment