Files
claude-desktop-debian/scripts/packaging/deb.sh
Aaddrick 3ddfb7353c launcher: log session/IME env block at startup (#570)
* launcher: log session/IME env block at startup (#548)

Adds log_session_env, called once per launch from each packaging
target (deb, rpm, AppImage, Nix). Emits a single env={ ... } block
covering display (XDG_SESSION_TYPE, WAYLAND_DISPLAY, DISPLAY,
XDG_CURRENT_DESKTOP), IME (GTK_IM_MODULE, XMODIFIERS, QT_IM_MODULE),
and Claude-specific overrides (CLAUDE_USE_WAYLAND,
CLAUDE_TITLEBAR_STYLE, CLAUDE_GTK_IM_MODULE).

Empty/unset values are emitted as `KEY=` (rather than omitted) so
absence is unambiguous in bug reports. Pure observability — no
behavior change.

Closes #548

Co-Authored-By: Claude <claude@anthropic.com>

* test: consolidate log_session_env BATS coverage (#548)

Collapse the four log_session_env cases into two, and tighten the
assertions in both:

  - Old test 1 (substring match per key) + old test 4 (block braces
    on their own lines) → one test using exact-line equality on the
    `lines` array. Locks block structure and per-key formatting in
    a single pass; substring matching could not catch a regression
    that re-ordered keys, dropped indentation, or merged lines.
  - Old test 2 (unset values are KEY=) + old test 3 (empty-string
    is KEY=) → one test covering both code paths. Exact-line match
    proves the value after `=` is truly empty; the previous
    `*'KEY='*` substring would have matched `KEY=value` and the
    old test-3 regex was fragile (depended on trailing newline
    being literal `$'\n'` vs end-of-string `$`).

Net: 77 → 42 lines, 4 → 2 cases, stronger guarantees. No change to
the helper itself or the call sites — issue #548 acceptance criteria
still hold.

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-05-05 06:45:42 -04:00

256 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# 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
package_name="$5"
maintainer="$6"
description="$7"
echo '--- Starting Debian Package Build ---'
echo "Version: $version"
echo "Architecture: $architecture"
echo "Work Directory: $work_dir"
echo "App Staging Directory: $app_staging_dir"
echo "Package Name: $package_name"
package_root="$work_dir/package"
install_dir="$package_root/usr"
# Clean previous package structure if it exists
rm -rf "$package_root"
# Create Debian package structure
echo "Creating package structure in $package_root..."
mkdir -p "$package_root/DEBIAN" || exit 1
mkdir -p "$install_dir/lib/$package_name" || exit 1
mkdir -p "$install_dir/share/applications" || exit 1
mkdir -p "$install_dir/share/icons" || exit 1
mkdir -p "$install_dir/bin" || exit 1
# --- Icon Installation ---
echo 'Installing icons...'
# Map: size -> filename suffix
declare -A icon_files=(
[16]=13 [24]=11 [32]=10 [48]=8 [64]=7 [256]=6
)
for size in "${!icon_files[@]}"; do
icon_dir="$install_dir/share/icons/hicolor/${size}x${size}/apps"
mkdir -p "$icon_dir" || exit 1
icon_source_path="$work_dir/claude_${icon_files[$size]}_${size}x${size}x32.png"
if [[ -f $icon_source_path ]]; then
echo "Installing ${size}x${size} icon..."
install -Dm 644 "$icon_source_path" "$icon_dir/claude-desktop.png" || exit 1
else
echo "Warning: Missing ${size}x${size} icon at $icon_source_path"
fi
done
echo 'Icons installed'
# --- Copy Application Files ---
echo "Copying application files from $app_staging_dir..."
# Copy local electron first if it was packaged (check if node_modules exists in staging)
if [[ -d $app_staging_dir/node_modules ]]; then
echo 'Copying packaged electron...'
cp -r "$app_staging_dir/node_modules" "$install_dir/lib/$package_name/" || exit 1
fi
# Install app.asar in Electron's resources directory where process.resourcesPath points
resources_dir="$install_dir/lib/$package_name/node_modules/electron/dist/resources"
mkdir -p "$resources_dir" || exit 1
cp "$app_staging_dir/app.asar" "$resources_dir/" || exit 1
cp -r "$app_staging_dir/app.asar.unpacked" "$resources_dir/" || exit 1
echo 'Application files copied to Electron resources directory'
# Copy shared launcher library (launcher-common.sh sources doctor.sh
# at runtime, so both must live in the same directory)
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$(dirname "$script_dir")/launcher-common.sh" "$install_dir/lib/$package_name/" || exit 1
cp "$(dirname "$script_dir")/doctor.sh" "$install_dir/lib/$package_name/" || exit 1
echo 'Shared launcher library + doctor copied'
# --- Create Desktop Entry ---
echo 'Creating desktop entry...'
cat > "$install_dir/share/applications/claude-desktop.desktop" << EOF
[Desktop Entry]
Name=Claude
Exec=/usr/bin/claude-desktop %u
Icon=claude-desktop
Type=Application
Terminal=false
Categories=Office;Utility;
MimeType=x-scheme-handler/claude;
StartupWMClass=Claude
EOF
echo 'Desktop entry created'
# --- Create Launcher Script ---
echo 'Creating launcher script...'
cat > "$install_dir/bin/claude-desktop" << EOF
#!/usr/bin/env bash
# Source shared launcher library
source "/usr/lib/$package_name/launcher-common.sh"
# Handle --doctor flag before anything else
if [[ "\${1:-}" == '--doctor' ]]; then
local_electron_path="/usr/lib/$package_name/node_modules/electron/dist/electron"
run_doctor "\$local_electron_path"
exit \$?
fi
# Setup logging and environment
setup_logging || exit 1
setup_electron_env
cleanup_orphaned_cowork_daemon
cleanup_stale_lock
cleanup_stale_cowork_socket
# Log startup info
log_message '--- Claude Desktop Launcher Start ---'
log_message "Timestamp: \$(date)"
log_message "Arguments: \$@"
log_session_env
# Check for display
if ! check_display; then
log_message 'No display detected (TTY session)'
echo 'Error: Claude Desktop requires a graphical desktop environment.' >&2
echo 'Please run from within an X11 or Wayland session, not from a TTY.' >&2
exit 1
fi
# Detect display backend
detect_display_backend
if [[ \$is_wayland == true ]]; then
log_message 'Wayland detected'
fi
# Determine Electron executable path
electron_exec='electron'
local_electron_path="/usr/lib/$package_name/node_modules/electron/dist/electron"
if [[ -f \$local_electron_path ]]; then
electron_exec="\$local_electron_path"
log_message "Using local Electron: \$electron_exec"
else
if command -v electron &> /dev/null; then
log_message "Using global Electron: \$electron_exec"
else
log_message 'Error: Electron executable not found'
if command -v zenity &> /dev/null; then
zenity --error \
--text='Claude Desktop cannot start because the Electron framework is missing.'
elif command -v kdialog &> /dev/null; then
kdialog --error \
'Claude Desktop cannot start because the Electron framework is missing.'
fi
exit 1
fi
fi
# App path
app_path="/usr/lib/$package_name/node_modules/electron/dist/resources/app.asar"
# Build electron args
build_electron_args 'deb'
# Add app path LAST
electron_args+=("\$app_path")
# Change to application directory
app_dir="/usr/lib/$package_name"
log_message "Changing directory to \$app_dir"
cd "\$app_dir" || { log_message "Failed to cd to \$app_dir"; exit 1; }
# Execute Electron
log_message "Executing: \$electron_exec \${electron_args[*]} \$*"
"\$electron_exec" "\${electron_args[@]}" "\$@" >> "\$log_file" 2>&1
exit_code=\$?
log_message "Electron exited with code: \$exit_code"
log_message '--- Claude Desktop Launcher End ---'
exit \$exit_code
EOF
chmod +x "$install_dir/bin/claude-desktop" || exit 1
echo 'Launcher script created'
# --- Create Control File ---
echo 'Creating control file...'
# Electron is bundled with its own Node.js runtime, so nodejs/npm are not
# runtime dependencies. p7zip is only used at build time to extract the
# installer. No external dependencies are required at runtime.
cat > "$package_root/DEBIAN/control" << EOF
Package: $package_name
Version: $version
Section: utils
Priority: optional
Architecture: $architecture
Maintainer: $maintainer
Description: $description
Claude is an AI assistant from Anthropic.
This package provides the desktop interface for Claude.
.
Supported on Debian-based Linux distributions (Debian, Ubuntu, Linux Mint, MX Linux, etc.)
EOF
echo 'Control file created'
# --- Create Postinst Script ---
echo 'Creating postinst script...'
cat > "$package_root/DEBIAN/postinst" << EOF
#!/bin/sh
set -e
# Update desktop database for MIME types
echo "Updating desktop database..."
update-desktop-database /usr/share/applications &> /dev/null || true
# Set correct permissions for chrome-sandbox if electron is installed globally
# or locally packaged
echo "Setting chrome-sandbox permissions..."
SANDBOX_PATH=""
# Electron is always packaged locally now, so only check the local path.
LOCAL_SANDBOX_PATH="/usr/lib/$package_name/node_modules/electron/dist/chrome-sandbox"
if [ -f "\$LOCAL_SANDBOX_PATH" ]; then
SANDBOX_PATH="\$LOCAL_SANDBOX_PATH"
fi
if [ -n "\$SANDBOX_PATH" ] && [ -f "\$SANDBOX_PATH" ]; then
echo "Found chrome-sandbox at: \$SANDBOX_PATH"
chown root:root "\$SANDBOX_PATH" || echo "Warning: Failed to chown chrome-sandbox"
chmod 4755 "\$SANDBOX_PATH" || echo "Warning: Failed to chmod chrome-sandbox"
echo "Permissions set for \$SANDBOX_PATH"
else
echo "Warning: chrome-sandbox binary not found in local package at \$LOCAL_SANDBOX_PATH. Sandbox may not function correctly."
fi
exit 0
EOF
chmod +x "$package_root/DEBIAN/postinst" || exit 1
echo 'Postinst script created'
# --- Build .deb Package ---
echo 'Building .deb package...'
deb_file="$work_dir/${package_name}_${version}_${architecture}.deb"
# Fix DEBIAN directory permissions (must be 755 for dpkg-deb)
echo 'Setting DEBIAN directory permissions...'
chmod 755 "$package_root/DEBIAN" || exit 1
# Fix script permissions in DEBIAN directory
echo 'Setting script permissions...'
chmod 755 "$package_root/DEBIAN/postinst" || exit 1
if ! dpkg-deb --build "$package_root" "$deb_file"; then
echo 'Failed to build .deb package' >&2
exit 1
fi
echo "Deb package built successfully: $deb_file"
echo '--- Debian Package Build Finished ---'
exit 0