Files
claude-desktop-debian/build.sh

319 lines
10 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
#===============================================================================
# Claude Desktop Debian Build Script
# Repackages Claude Desktop (Electron app) for Debian/Ubuntu Linux
#===============================================================================
# Global variables (set by functions, used throughout)
architecture=''
distro_family='' # debian, rpm, nix, or unknown
claude_download_url=''
claude_exe_sha256=''
claude_exe_filename=''
version=''
release_tag='' # Optional release tag (e.g., v1.3.2+claude1.1.799) for unique package versions
build_format='' # Will be set based on distro if not specified
cleanup_action='yes'
perform_cleanup=false
test_flags_mode=false
local_exe_path=''
node_pty_dir=''
source_dir=''
original_user=''
original_home=''
project_root=''
work_dir=''
app_staging_dir=''
chosen_electron_module_path=''
electron_var=''
electron_var_re=''
asar_exec=''
claude_extract_dir=''
electron_resources_dest=''
node_pty_build_dir=''
final_output_path=''
# Package metadata (constants)
readonly PACKAGE_NAME='claude-desktop'
readonly MAINTAINER='Claude Desktop Linux Maintainers'
readonly DESCRIPTION='Claude Desktop for Linux'
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/_common.sh
source "$script_dir/scripts/_common.sh"
# shellcheck source=scripts/setup/detect-host.sh
source "$script_dir/scripts/setup/detect-host.sh"
# shellcheck source=scripts/setup/dependencies.sh
source "$script_dir/scripts/setup/dependencies.sh"
# shellcheck source=scripts/setup/download.sh
source "$script_dir/scripts/setup/download.sh"
# shellcheck source=scripts/patches/_common.sh
source "$script_dir/scripts/patches/_common.sh"
# shellcheck source=scripts/patches/app-asar.sh
source "$script_dir/scripts/patches/app-asar.sh"
# shellcheck source=scripts/patches/tray.sh
source "$script_dir/scripts/patches/tray.sh"
# shellcheck source=scripts/patches/quick-window.sh
source "$script_dir/scripts/patches/quick-window.sh"
# shellcheck source=scripts/patches/claude-code.sh
source "$script_dir/scripts/patches/claude-code.sh"
# shellcheck source=scripts/patches/cowork.sh
source "$script_dir/scripts/patches/cowork.sh"
feat(linux): hybrid titlebar mode for clickable in-app topbar (#538) * feat(linux): hybrid titlebar mode for clickable in-app topbar Default `CLAUDE_TITLEBAR_STYLE` is now `hybrid`: native OS frame plus a BrowserView preload shim that convinces claude.ai's bundle to render its in-app topbar (hamburger / sidebar / search / nav / Cowork ghost). Stacked layout instead of Windows's combined bar, but every button is clickable. Why not the upstream `frame:false` + WCO config: investigation (see docs/learnings/linux-topbar-shim.md) ruled out `titleBarOverlay`, `titleBarStyle:'hidden'`, and the `.draggable` CSS class as the source of the topbar click-eating drag region. The remaining cause is a Chromium-level implicit drag region for `frame:false` windows that exists on both X11 and Wayland and has no Electron-API knob. With `frame:true` the OS handles dragging and Chromium pushes no drag-region map, so the buttons receive mouse events normally. Modes: - `hybrid` (default) — system frame + shim, topbar visible and clickable - `native` — system frame, no shim, no in-app topbar - `hidden` — frameless + WCO config, matches Windows/macOS upstream; topbar visible but not clickable on Linux. Kept for Wayland comparison and future investigation Tests: tests/launcher-common.bats grew 16 cases covering `_resolve_titlebar_style`, `build_electron_args` flag selection per mode, and `setup_electron_env` env-var wiring per mode. `claude-desktop --doctor` now reports the resolved mode and warns when `hidden` is set. Co-Authored-By: Claude <claude@anthropic.com> * docs(learnings): add hybrid-mode screenshot Visual reference of the stacked layout: DE-drawn titlebar on top with native window controls, claude.ai's in-app topbar (hamburger / search / back-forward) immediately below it. Co-Authored-By: Claude <claude@anthropic.com> * docs(learnings): fix codespell hit (Pre-emptive → Preemptive) Codespell flags hyphenated "Pre-emptive" as a misspelling of "Preemptive". Drops the hyphen to clear the spellcheck CI gate on PR #538. Co-Authored-By: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com>
2026-05-01 02:47:16 -04:00
# shellcheck source=scripts/patches/wco-shim.sh
source "$script_dir/scripts/patches/wco-shim.sh"
# shellcheck source=scripts/staging/electron.sh
source "$script_dir/scripts/staging/electron.sh"
# shellcheck source=scripts/staging/icons.sh
source "$script_dir/scripts/staging/icons.sh"
# shellcheck source=scripts/staging/locales.sh
source "$script_dir/scripts/staging/locales.sh"
# shellcheck source=scripts/staging/ssh-helpers.sh
source "$script_dir/scripts/staging/ssh-helpers.sh"
# shellcheck source=scripts/staging/cowork-resources.sh
source "$script_dir/scripts/staging/cowork-resources.sh"
#===============================================================================
# Packaging Functions
#===============================================================================
run_packaging() {
section_header 'Call Packaging Script'
if [[ $build_format == 'nix' ]]; then
echo 'Nix build mode - skipping packaging (Nix derivation handles installation)'
section_footer 'Call Packaging Script'
return 0
fi
local output_path=''
local script_name file_pattern pkg_file
case "$build_format" in
deb)
script_name='deb.sh'
file_pattern="${PACKAGE_NAME}_${version}_${architecture}.deb"
;;
rpm)
script_name='rpm.sh'
file_pattern="${PACKAGE_NAME}-${version}*.rpm"
;;
appimage)
script_name='appimage.sh'
file_pattern="${PACKAGE_NAME}-${version}-${architecture}.AppImage"
;;
esac
if [[ $build_format == 'deb' || $build_format == 'rpm' ]]; then
echo "Calling ${build_format^^} packaging script for $architecture..."
chmod +x "scripts/packaging/$script_name" || exit 1
if ! "scripts/packaging/$script_name" \
"$version" "$architecture" "$work_dir" "$app_staging_dir" \
"$PACKAGE_NAME" "$MAINTAINER" "$DESCRIPTION"; then
echo "${build_format^^} packaging script failed." >&2
exit 1
fi
pkg_file=$(find "$work_dir" -maxdepth 1 -name "$file_pattern" | head -n 1)
echo "${build_format^^} Build complete!"
if [[ -n $pkg_file && -f $pkg_file ]]; then
output_path="./$(basename "$pkg_file")"
mv "$pkg_file" "$output_path" || exit 1
echo "Package created at: $output_path"
else
echo "Warning: Could not determine final .${build_format} file path."
output_path='Not Found'
fi
elif [[ $build_format == 'appimage' ]]; then
echo "Calling AppImage packaging script for $architecture..."
chmod +x "scripts/packaging/$script_name" || exit 1
if ! "scripts/packaging/$script_name" \
"$version" "$architecture" "$work_dir" "$app_staging_dir" "$PACKAGE_NAME"; then
echo 'AppImage packaging script failed.' >&2
exit 1
fi
local appimage_file
appimage_file=$(find "$work_dir" -maxdepth 1 -name "${PACKAGE_NAME}-${version}-${architecture}.AppImage" | head -n 1)
echo 'AppImage Build complete!'
if [[ -n $appimage_file && -f $appimage_file ]]; then
output_path="./$(basename "$appimage_file")"
mv "$appimage_file" "$output_path" || exit 1
echo "Package created at: $output_path"
section_header 'Generate .desktop file for AppImage'
local desktop_file="./${PACKAGE_NAME}-appimage.desktop"
echo "Generating .desktop file for AppImage at $desktop_file..."
cat > "$desktop_file" << EOF
[Desktop Entry]
Name=Claude (AppImage)
Comment=Claude Desktop (AppImage Version $version)
Exec=$(basename "$output_path") %u
Icon=claude-desktop
Type=Application
Terminal=false
Categories=Office;Utility;Network;
MimeType=x-scheme-handler/claude;
StartupWMClass=Claude
X-AppImage-Version=$version
X-AppImage-Name=Claude Desktop (AppImage)
EOF
echo '.desktop file generated.'
else
echo 'Warning: Could not determine final .AppImage file path.'
output_path='Not Found'
fi
fi
# Store for print_next_steps
final_output_path="$output_path"
}
cleanup_build() {
section_header 'Cleanup'
if [[ $perform_cleanup != true ]]; then
echo "Skipping cleanup of intermediate build files in $work_dir."
return
fi
echo "Cleaning up intermediate build files in $work_dir..."
if rm -rf "$work_dir"; then
echo "Cleanup complete ($work_dir removed)."
else
echo 'Cleanup command failed.'
fi
}
print_next_steps() {
echo -e '\n\033[1;34m====== Next Steps ======\033[0m'
case "$build_format" in
deb|rpm)
if [[ $final_output_path != 'Not Found' && -e $final_output_path ]]; then
local pkg_type install_cmd alt_cmd
if [[ $build_format == 'deb' ]]; then
pkg_type='Debian'
install_cmd="sudo apt install $final_output_path"
alt_cmd="sudo dpkg -i $final_output_path"
else
pkg_type='RPM'
install_cmd="sudo dnf install $final_output_path"
alt_cmd="sudo rpm -i $final_output_path"
fi
echo -e "To install the $pkg_type package, run:"
echo -e " \033[1;32m$install_cmd\033[0m"
echo -e " (or \`$alt_cmd\`)"
else
echo -e "${build_format^^} package file not found. Cannot provide installation instructions."
fi
;;
appimage)
if [[ $final_output_path != 'Not Found' && -e $final_output_path ]]; then
echo -e "AppImage created at: \033[1;36m$final_output_path\033[0m"
echo -e '\n\033[1;33mIMPORTANT:\033[0m This AppImage requires \033[1;36mGear Lever\033[0m for proper desktop integration'
# shellcheck disable=SC2016 # backticks intentional for display
echo -e 'and to handle the `claude://` login process correctly.'
echo -e '\nTo install Gear Lever:'
echo -e ' 1. Install via Flatpak:'
echo -e ' \033[1;32mflatpak install flathub it.mijorus.gearlever\033[0m'
echo -e ' 2. Integrate your AppImage with just one click:'
echo -e ' - Open Gear Lever'
echo -e " - Drag and drop \033[1;36m$final_output_path\033[0m into Gear Lever"
echo -e " - Click 'Integrate' to add it to your app menu"
if [[ ${GITHUB_ACTIONS:-} == 'true' ]]; then
echo -e '\n This AppImage includes embedded update information!'
else
echo -e '\n This locally-built AppImage does not include update information.'
echo -e ' For automatic updates, download release versions: https://github.com/aaddrick/claude-desktop-debian/releases'
fi
else
echo -e 'AppImage file not found. Cannot provide usage instructions.'
fi
;;
esac
echo -e '\033[1;34m======================\033[0m'
}
#===============================================================================
# Main Execution
#===============================================================================
main() {
# Phase 1: Setup
detect_architecture
detect_distro
check_system_requirements
parse_arguments "$@"
# Early exit for test mode
if [[ $test_flags_mode == true ]]; then
echo '--- Test Flags Mode Enabled ---'
echo "Build Format: $build_format"
echo "Clean Action: $cleanup_action"
echo 'Exiting without build.'
exit 0
fi
if [[ $build_format != 'nix' ]]; then
check_dependencies
fi
setup_work_directory
if [[ $build_format != 'nix' ]]; then
setup_nodejs
setup_electron_asar
else
# Nix provides node and asar in PATH
asar_exec=$(command -v asar)
if [[ -z $asar_exec ]]; then
echo 'Error: asar not found in PATH (expected Nix to provide it)' >&2
exit 1
fi
fi
# Phase 2: Download and extract
if [[ $build_format == 'nix' && -z $local_exe_path ]]; then
echo 'Error: --exe is required when --build nix is specified' >&2
exit 1
fi
download_claude_installer
# Phase 3: Patch and prepare
patch_app_asar
install_node_pty
finalize_app_asar
if [[ $build_format != 'nix' ]]; then
stage_electron
copy_locale_files
else
# Nix installPhase handles Electron staging and locale files.
# Set a resources destination so process_icons and copy_ssh_helpers
# have somewhere to write; the Nix installPhase picks them up.
electron_resources_dest="$app_staging_dir/nix-resources"
mkdir -p "$electron_resources_dest" || exit 1
fi
process_icons
copy_ssh_helpers
copy_cowork_resources
cd "$project_root" || exit 1
# Phase 4: Package
run_packaging
# Phase 5: Cleanup and finish
cleanup_build
echo 'Build process finished.'
if [[ $build_format != 'nix' ]]; then
print_next_steps
fi
}
# Run main with all script arguments
main "$@"
2025-04-02 23:24:46 -04:00
exit 0