336 lines
13 KiB
Bash
Executable File
336 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
# Arguments passed from the main script
|
|
VERSION="$1"
|
|
ARCHITECTURE="$2"
|
|
WORK_DIR="$3" # The top-level build directory (e.g., ./build)
|
|
APP_STAGING_DIR="$4" # Directory containing the prepared app files (e.g., ./build/electron-app)
|
|
PACKAGE_NAME="$5"
|
|
# MAINTAINER and DESCRIPTION might not be directly used by AppImage tools but passed for consistency
|
|
|
|
echo "--- Starting AppImage Build ---"
|
|
echo "Version: $VERSION"
|
|
echo "Architecture: $ARCHITECTURE"
|
|
echo "Work Directory: $WORK_DIR"
|
|
echo "App Staging Directory: $APP_STAGING_DIR"
|
|
echo "Package Name: $PACKAGE_NAME"
|
|
|
|
COMPONENT_ID="io.github.aaddrick.claude-desktop-debian"
|
|
# Define AppDir structure path
|
|
APPDIR_PATH="$WORK_DIR/${COMPONENT_ID}.AppDir"
|
|
rm -rf "$APPDIR_PATH"
|
|
mkdir -p "$APPDIR_PATH/usr/bin"
|
|
mkdir -p "$APPDIR_PATH/usr/lib"
|
|
mkdir -p "$APPDIR_PATH/usr/share/icons/hicolor/256x256/apps"
|
|
mkdir -p "$APPDIR_PATH/usr/share/applications"
|
|
|
|
echo "📦 Staging application files into AppDir..."
|
|
# Copy the core application files (asar, unpacked resources, node_modules if present)
|
|
# Explicitly copy required components to ensure hidden files/dirs like .bin are included
|
|
if [ -f "$APP_STAGING_DIR/app.asar" ]; then
|
|
cp -a "$APP_STAGING_DIR/app.asar" "$APPDIR_PATH/usr/lib/"
|
|
fi
|
|
if [ -d "$APP_STAGING_DIR/app.asar.unpacked" ]; then
|
|
cp -a "$APP_STAGING_DIR/app.asar.unpacked" "$APPDIR_PATH/usr/lib/"
|
|
fi
|
|
if [ -d "$APP_STAGING_DIR/node_modules" ]; then
|
|
echo "Copying node_modules from staging to AppDir..."
|
|
cp -a "$APP_STAGING_DIR/node_modules" "$APPDIR_PATH/usr/lib/"
|
|
fi
|
|
|
|
# Ensure Electron is bundled within the AppDir for portability
|
|
# Check if electron was copied into the staging dir's node_modules
|
|
# The actual executable is usually inside the 'dist' directory
|
|
BUNDLED_ELECTRON_PATH="$APPDIR_PATH/usr/lib/node_modules/electron/dist/electron"
|
|
echo "Checking for executable at: $BUNDLED_ELECTRON_PATH"
|
|
if [ ! -x "$BUNDLED_ELECTRON_PATH" ]; then # Check if it exists and is executable
|
|
echo "❌ Electron executable not found or not executable in staging area ($BUNDLED_ELECTRON_PATH)."
|
|
echo " AppImage requires Electron to be bundled. Ensure the main script copies it correctly."
|
|
exit 1
|
|
fi
|
|
# Ensure the bundled electron is executable (redundant check, but safe)
|
|
chmod +x "$BUNDLED_ELECTRON_PATH"
|
|
|
|
# --- Create AppRun Script ---
|
|
echo "🚀 Creating AppRun script..."
|
|
# Note: We use $VERSION and $PACKAGE_NAME from the build script environment here
|
|
# They will be embedded into the AppRun script.
|
|
cat > "$APPDIR_PATH/AppRun" << EOF
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
# Find the location of the AppRun script and the AppImage file itself
|
|
APPDIR=\$(dirname "\$0")
|
|
# Try to get the absolute path of the AppImage file being run
|
|
# $APPIMAGE is often set by the AppImage runtime, otherwise try readlink
|
|
APPIMAGE_PATH="\${APPIMAGE:-}"
|
|
if [ -z "\$APPIMAGE_PATH" ]; then
|
|
# Find the AppRun script itself, which should be $0
|
|
# Use readlink -f to get the absolute path, handling symlinks
|
|
# Go up one level from AppRun's dir to get the AppImage path (usually)
|
|
# This might be fragile if AppRun is not at the root, but it's standard.
|
|
APPIMAGE_PATH=\$(readlink -f "\$APPDIR/../$(basename "$APPDIR" .AppDir).AppImage" 2>/dev/null || readlink -f "\$0" 2>/dev/null)
|
|
# As a final fallback, just use $0, hoping it's the AppImage path
|
|
if [ -z "\$APPIMAGE_PATH" ] || [ ! -f "\$APPIMAGE_PATH" ]; then
|
|
APPIMAGE_PATH="\$0"
|
|
fi
|
|
fi
|
|
|
|
# --- Desktop Integration (Handled by Gear Lever) ---
|
|
# The bundled .desktop file (claude-desktop-appimage.desktop) inside the AppImage
|
|
# contains the necessary MimeType=x-scheme-handler/claude; entry.
|
|
# Gear Lever (or similar tools) will use this file to integrate
|
|
# the AppImage with the system, including setting up the URI scheme handler,
|
|
# if the user chooses to integrate. No manual registration is needed here.
|
|
# --- End Desktop Integration ---
|
|
|
|
|
|
# Set up environment variables if needed (e.g., LD_LIBRARY_PATH)
|
|
# export LD_LIBRARY_PATH="\$APPDIR/usr/lib:\$LD_LIBRARY_PATH"
|
|
|
|
export ELECTRON_FORCE_IS_PACKAGED=true
|
|
|
|
# Detect if Wayland is likely running
|
|
IS_WAYLAND=false
|
|
if [ ! -z "\$WAYLAND_DISPLAY" ]; then
|
|
IS_WAYLAND=true
|
|
fi
|
|
|
|
# Path to the bundled Electron executable
|
|
# Use the path relative to AppRun within the 'electron/dist' module directory
|
|
ELECTRON_EXEC="\$APPDIR/usr/lib/node_modules/electron/dist/electron"
|
|
APP_PATH="\$APPDIR/usr/lib/app.asar"
|
|
|
|
# Base command arguments array
|
|
# Add --no-sandbox flag to avoid sandbox issues within AppImage
|
|
ELECTRON_ARGS=("--no-sandbox" "\$APP_PATH")
|
|
|
|
# Add Wayland flags if Wayland is detected
|
|
if [ "\$IS_WAYLAND" = true ]; then
|
|
echo "AppRun: Wayland detected, adding flags."
|
|
ELECTRON_ARGS+=("--enable-features=UseOzonePlatform,WaylandWindowDecorations,GlobalShortcutsPortal")
|
|
ELECTRON_ARGS+=("--ozone-platform=wayland")
|
|
ELECTRON_ARGS+=("--enable-wayland-ime")
|
|
ELECTRON_ARGS+=("--wayland-text-input-version=3")
|
|
fi
|
|
|
|
# Change to the application resources directory (where app.asar is)
|
|
cd "\$APPDIR/usr/lib" || exit 1
|
|
|
|
# Define log file path in user's home directory
|
|
LOG_FILE="\$HOME/claude-desktop-launcher.log"
|
|
|
|
# Change to HOME directory before exec'ing Electron to avoid CWD permission issues
|
|
cd "\$HOME" || exit 1
|
|
|
|
# Execute Electron with app path, flags, and script arguments passed to AppRun
|
|
# Redirect stdout and stderr to the log file (append)
|
|
echo "AppRun: Executing \$ELECTRON_EXEC \${ELECTRON_ARGS[@]} \$@ >> \$LOG_FILE 2>&1"
|
|
exec "\$ELECTRON_EXEC" "\${ELECTRON_ARGS[@]}" "\$@" >> "\$LOG_FILE" 2>&1
|
|
EOF
|
|
chmod +x "$APPDIR_PATH/AppRun"
|
|
echo "✓ AppRun script created (with logging to \$HOME/claude-desktop-launcher.log, --no-sandbox, and CWD set to \$HOME)"
|
|
|
|
# --- Create Desktop Entry (Bundled inside AppDir) ---
|
|
echo "📝 Creating bundled desktop entry..."
|
|
# This is the desktop file *inside* the AppImage, used by tools like appimaged
|
|
cat > "$APPDIR_PATH/$COMPONENT_ID.desktop" << EOF
|
|
[Desktop Entry]
|
|
Name=Claude
|
|
Exec=AppRun %u
|
|
Icon=$COMPONENT_ID
|
|
Type=Application
|
|
Terminal=false
|
|
Categories=Network;Utility;
|
|
Comment=Claude Desktop for Linux
|
|
MimeType=x-scheme-handler/claude;
|
|
StartupWMClass=Claude
|
|
X-AppImage-Version=$VERSION
|
|
X-AppImage-Name=Claude Desktop
|
|
EOF
|
|
# Also place it in the standard location for tools like appimaged and validation
|
|
mkdir -p "$APPDIR_PATH/usr/share/applications"
|
|
cp "$APPDIR_PATH/$COMPONENT_ID.desktop" "$APPDIR_PATH/usr/share/applications/"
|
|
echo "✓ Bundled desktop entry created and copied to usr/share/applications/"
|
|
|
|
# --- Copy Icons ---
|
|
echo "🎨 Copying icons..."
|
|
# Use the 256x256 icon as the main AppImage icon
|
|
ICON_SOURCE_PATH="$WORK_DIR/claude_6_256x256x32.png"
|
|
if [ -f "$ICON_SOURCE_PATH" ]; then
|
|
# Standard location within AppDir
|
|
cp "$ICON_SOURCE_PATH" "$APPDIR_PATH/usr/share/icons/hicolor/256x256/apps/${COMPONENT_ID}.png"
|
|
# Top-level icon (used by appimagetool) - Should match the Icon field in the .desktop file
|
|
cp "$ICON_SOURCE_PATH" "$APPDIR_PATH/${COMPONENT_ID}.png"
|
|
# Top-level icon without extension (fallback for some tools)
|
|
cp "$ICON_SOURCE_PATH" "$APPDIR_PATH/${COMPONENT_ID}"
|
|
# Hidden .DirIcon (fallback for some systems/tools)
|
|
cp "$ICON_SOURCE_PATH" "$APPDIR_PATH/.DirIcon"
|
|
echo "✓ Icon copied to standard path, top-level (.png and no ext), and .DirIcon"
|
|
else
|
|
echo "Warning: Missing 256x256 icon at $ICON_SOURCE_PATH. AppImage icon might be missing."
|
|
fi
|
|
|
|
# --- Create AppStream Metadata ---
|
|
echo "📄 Creating AppStream metadata..."
|
|
METADATA_DIR="$APPDIR_PATH/usr/share/metainfo"
|
|
mkdir -p "$METADATA_DIR"
|
|
|
|
# Use the package name for the appdata file name (seems required by appimagetool warning)
|
|
# Use reverse-DNS for component ID and filename, following common practice
|
|
APPDATA_FILE="$METADATA_DIR/${COMPONENT_ID}.appdata.xml" # Filename matches component ID
|
|
|
|
# Generate the AppStream XML file
|
|
# Use MIT license based on LICENSE-MIT file in repo
|
|
# ID follows reverse DNS convention
|
|
cat > "$APPDATA_FILE" << EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<component type="desktop-application">
|
|
<id>$COMPONENT_ID</id>
|
|
<metadata_license>CC0-1.0</metadata_license>
|
|
<project_license>MIT</project_license>
|
|
<developer id="io.github.aaddrick">
|
|
<name>aaddrick</name>
|
|
</developer>
|
|
|
|
<name>Claude Desktop</name>
|
|
<summary>Unofficial desktop client for Claude AI</summary>
|
|
|
|
<description>
|
|
<p>
|
|
Provides a desktop experience for interacting with Claude AI, wrapping the web interface.
|
|
</p>
|
|
</description>
|
|
|
|
<launchable type="desktop-id">${COMPONENT_ID}.desktop</launchable> <!-- Reference the actual .desktop file -->
|
|
|
|
<icon type="stock">${COMPONENT_ID}</icon> <!-- Use the icon name from .desktop -->
|
|
<url type="homepage">https://github.com/aaddrick/claude-desktop-debian</url>
|
|
<screenshots>
|
|
<screenshot type="default">
|
|
<image>https://github.com/user-attachments/assets/93080028-6f71-48bd-8e59-5149d148cd45</image>
|
|
</screenshot>
|
|
</screenshots>
|
|
<provides>
|
|
<binary>AppRun</binary> <!-- Provide the actual binary -->
|
|
</provides>
|
|
|
|
<categories>
|
|
<category>Network</category>
|
|
<category>Utility</category>
|
|
</categories>
|
|
|
|
<content_rating type="oars-1.1" />
|
|
|
|
<releases>
|
|
<release version="$VERSION" date="$(date +%Y-%m-%d)">
|
|
<description>
|
|
<p>Version $VERSION.</p>
|
|
</description>
|
|
</release>
|
|
</releases>
|
|
|
|
</component>
|
|
EOF
|
|
echo "✓ AppStream metadata created at $APPDATA_FILE"
|
|
|
|
|
|
# --- Get appimagetool ---
|
|
APPIMAGETOOL_PATH=""
|
|
if command -v appimagetool &> /dev/null; then
|
|
APPIMAGETOOL_PATH=$(command -v appimagetool)
|
|
echo "✓ Found appimagetool in PATH: $APPIMAGETOOL_PATH"
|
|
elif [ -f "$WORK_DIR/appimagetool-x86_64.AppImage" ]; then # Check for specific arch first
|
|
APPIMAGETOOL_PATH="$WORK_DIR/appimagetool-x86_64.AppImage"
|
|
echo "✓ Found downloaded x86_64 appimagetool: $APPIMAGETOOL_PATH"
|
|
elif [ -f "$WORK_DIR/appimagetool-aarch64.AppImage" ]; then # Check for other arch
|
|
APPIMAGETOOL_PATH="$WORK_DIR/appimagetool-aarch64.AppImage"
|
|
echo "✓ Found downloaded aarch64 appimagetool: $APPIMAGETOOL_PATH"
|
|
else
|
|
echo "🛠️ Downloading appimagetool..."
|
|
# Determine architecture for download URL
|
|
TOOL_ARCH=""
|
|
case "$ARCHITECTURE" in # Use target ARCHITECTURE passed to script
|
|
"amd64") TOOL_ARCH="x86_64" ;;
|
|
"arm64") TOOL_ARCH="aarch64" ;;
|
|
*) echo "❌ Unsupported architecture for appimagetool download: $ARCHITECTURE"; exit 1 ;;
|
|
esac
|
|
|
|
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${TOOL_ARCH}.AppImage"
|
|
APPIMAGETOOL_PATH="$WORK_DIR/appimagetool-${TOOL_ARCH}.AppImage"
|
|
|
|
if wget -q -O "$APPIMAGETOOL_PATH" "$APPIMAGETOOL_URL"; then
|
|
chmod +x "$APPIMAGETOOL_PATH"
|
|
echo "✓ Downloaded appimagetool to $APPIMAGETOOL_PATH"
|
|
else
|
|
echo "❌ Failed to download appimagetool from $APPIMAGETOOL_URL"
|
|
rm -f "$APPIMAGETOOL_PATH" # Clean up partial download
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# --- Build AppImage ---
|
|
echo "📦 Building AppImage..."
|
|
OUTPUT_FILENAME="${PACKAGE_NAME}-${VERSION}-${ARCHITECTURE}.AppImage"
|
|
OUTPUT_PATH="$WORK_DIR/$OUTPUT_FILENAME"
|
|
|
|
# --- Prepare Update Information (GitHub Actions only) ---
|
|
# Check if running in GitHub Actions workflow
|
|
if [ "$GITHUB_ACTIONS" = "true" ]; then
|
|
echo "🔄 Running in GitHub Actions - embedding update information for automatic updates..."
|
|
|
|
# Check if zsyncmake is available (required for generating .zsync files)
|
|
if ! command -v zsyncmake &> /dev/null; then
|
|
echo "⚠️ zsyncmake not found. Installing zsync package for .zsync file generation..."
|
|
if command -v apt-get &> /dev/null; then
|
|
sudo apt-get update && sudo apt-get install -y zsync
|
|
elif command -v dnf &> /dev/null; then
|
|
sudo dnf install -y zsync
|
|
elif command -v zypper &> /dev/null; then
|
|
sudo zypper install -y zsync
|
|
else
|
|
echo "⚠️ Cannot install zsync automatically. .zsync files may not be generated."
|
|
fi
|
|
fi
|
|
|
|
# Format: gh-releases-zsync|<username>|<repository>|<tag>|<filename-pattern>
|
|
# Using 'latest' tag to always point to the most recent release
|
|
UPDATE_INFO="gh-releases-zsync|aaddrick|claude-desktop-debian|latest|claude-desktop-*-${ARCHITECTURE}.AppImage.zsync"
|
|
echo "Update info: $UPDATE_INFO"
|
|
|
|
# Execute appimagetool with update information
|
|
export ARCH="$ARCHITECTURE"
|
|
echo "Using ARCH=$ARCH" # Debug output
|
|
if "$APPIMAGETOOL_PATH" --updateinformation "$UPDATE_INFO" "$APPDIR_PATH" "$OUTPUT_PATH"; then
|
|
echo "✓ AppImage built successfully with embedded update info: $OUTPUT_PATH"
|
|
# Check if zsync file was generated
|
|
ZSYNC_FILE="${OUTPUT_PATH}.zsync"
|
|
if [ -f "$ZSYNC_FILE" ]; then
|
|
echo "✓ zsync file generated: $ZSYNC_FILE"
|
|
echo "📤 zsync file will be included in release artifacts"
|
|
else
|
|
echo "⚠️ zsync file not generated (zsyncmake may not be installed)"
|
|
fi
|
|
else
|
|
echo "❌ Failed to build AppImage using $APPIMAGETOOL_PATH"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "🏠 Running locally - building AppImage without update information"
|
|
echo " (Update info and zsync files are only generated in GitHub Actions for releases)"
|
|
|
|
# Execute appimagetool without update information
|
|
export ARCH="$ARCHITECTURE"
|
|
echo "Using ARCH=$ARCH" # Debug output
|
|
if "$APPIMAGETOOL_PATH" "$APPDIR_PATH" "$OUTPUT_PATH"; then
|
|
echo "✓ AppImage built successfully: $OUTPUT_PATH"
|
|
else
|
|
echo "❌ Failed to build AppImage using $APPIMAGETOOL_PATH"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo "--- AppImage Build Finished ---"
|
|
|
|
exit 0 |