chore: import upstream snapshot with attribution
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
This commit is contained in:
Executable
+379
@@ -0,0 +1,379 @@
|
||||
#!/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 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" || exit 1
|
||||
mkdir -p "$appdir_path/usr/lib" || exit 1
|
||||
mkdir -p "$appdir_path/usr/share/icons/hicolor/256x256/apps" || exit 1
|
||||
mkdir -p "$appdir_path/usr/share/applications" || exit 1
|
||||
|
||||
echo 'Staging application files into AppDir...'
|
||||
# The staging dir is the extracted official usr/lib/claude-desktop tree
|
||||
# (Electron ELF, chrome-sandbox, resources/, locales/, ...); ship it as-is.
|
||||
mkdir -p "$appdir_path/usr/lib/claude-desktop" || exit 1
|
||||
cp -a "$app_staging_dir/." "$appdir_path/usr/lib/claude-desktop/" || exit 1
|
||||
echo 'Official application tree copied'
|
||||
|
||||
# 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" "$appdir_path/usr/lib/claude-desktop/" || exit 1
|
||||
sed -i "s/@@WM_CLASS@@/$WM_CLASS/" "$appdir_path/usr/lib/claude-desktop/launcher-common.sh"
|
||||
cp "$(dirname "$script_dir")/doctor.sh" "$appdir_path/usr/lib/claude-desktop/" || exit 1
|
||||
echo 'Shared launcher library + doctor copied'
|
||||
|
||||
# Ensure the official binary made it into the AppDir
|
||||
bundled_app_path="$appdir_path/usr/lib/claude-desktop/claude-desktop"
|
||||
echo "Checking for executable at: $bundled_app_path"
|
||||
if [[ ! -f $bundled_app_path ]]; then
|
||||
echo 'Claude Desktop binary not found in staging area.' >&2
|
||||
echo "Path checked: $bundled_app_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$bundled_app_path" || exit 1
|
||||
|
||||
# --- Create AppRun Script ---
|
||||
echo 'Creating AppRun script...'
|
||||
cat > "$appdir_path/AppRun" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Find the location of the AppRun script
|
||||
appdir=$(dirname "$(readlink -f "$0")")
|
||||
|
||||
# Source shared launcher library
|
||||
source "$appdir/usr/lib/claude-desktop/launcher-common.sh"
|
||||
|
||||
# The official Electron binary; it auto-loads the co-located
|
||||
# resources/app.asar, so no app path is ever passed (issue #696).
|
||||
app_exec="$appdir/usr/lib/claude-desktop/claude-desktop"
|
||||
|
||||
# Handle --doctor flag before anything else
|
||||
if [[ "${1:-}" == '--doctor' ]]; then
|
||||
run_doctor "$app_exec"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --version never reaches the terminal via Electron: the launcher
|
||||
# redirects all app output to the log (#772). Answer it here instead.
|
||||
if [[ "${1:-}" == '--version' ]]; then
|
||||
echo '@@PACKAGE_NAME@@ @@VERSION@@'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup logging and environment
|
||||
setup_logging || exit 1
|
||||
setup_electron_env
|
||||
|
||||
cleanup_orphaned_cowork_daemon
|
||||
cleanup_stale_desktop_helpers
|
||||
cleanup_stale_lock
|
||||
cleanup_stale_cowork_socket
|
||||
# APPIMAGE is set by the AppImage runtime to the persistent image path;
|
||||
# an extracted/direct run leaves it unset and the heal no-ops.
|
||||
heal_autostart_entry "${APPIMAGE:-}"
|
||||
backup_user_config
|
||||
|
||||
# Detect display backend
|
||||
detect_display_backend
|
||||
|
||||
# Log startup info
|
||||
log_message '--- Claude Desktop AppImage Start ---'
|
||||
log_message "Timestamp: $(date)"
|
||||
log_message "Arguments: $@"
|
||||
log_message "APPDIR: $appdir"
|
||||
log_session_env
|
||||
|
||||
# Build Chromium switches (appimage mode adds --no-sandbox for FUSE)
|
||||
build_electron_args 'appimage'
|
||||
|
||||
# Change to HOME directory before exec'ing the app to avoid CWD permission issues
|
||||
cd "$HOME" || exit 1
|
||||
|
||||
# Execute the official binary and keep AppRun alive so explicit quit can
|
||||
# clean up Desktop-owned helpers that outlive the main process.
|
||||
log_message "Executing: $app_exec ${electron_args[*]} $*"
|
||||
run_electron_and_cleanup "$app_exec" "${electron_args[@]}" "$@"
|
||||
exit $?
|
||||
EOF
|
||||
chmod +x "$appdir_path/AppRun" || exit 1
|
||||
# The AppRun heredoc is quoted (runtime expansion), so the build-time
|
||||
# values for the --version fast-path are stamped in afterwards.
|
||||
sed -i "s/@@PACKAGE_NAME@@/$package_name/; s/@@VERSION@@/$version/" \
|
||||
"$appdir_path/AppRun" || exit 1
|
||||
echo 'AppRun script created'
|
||||
|
||||
# --- 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=$WM_CLASS
|
||||
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" || exit 1
|
||||
cp "$appdir_path/$component_id.desktop" "$appdir_path/usr/share/applications/" || exit 1
|
||||
echo 'Bundled desktop entry created and copied to usr/share/applications/'
|
||||
|
||||
# --- Copy Icons ---
|
||||
echo 'Copying icons...'
|
||||
# Use the official 256x256 hicolor icon as the main AppImage icon
|
||||
icon_source_path="${CLAUDE_EXTRACT_DIR:?}/usr/share/icons/hicolor/256x256/apps/claude-desktop.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" || exit 1
|
||||
# Top-level icon (used by appimagetool) - Should match the Icon field in .desktop
|
||||
cp "$icon_source_path" "$appdir_path/${component_id}.png" || exit 1
|
||||
# Top-level icon without extension (fallback for some tools)
|
||||
cp "$icon_source_path" "$appdir_path/${component_id}" || exit 1
|
||||
# Hidden .DirIcon (fallback for some systems/tools)
|
||||
cp "$icon_source_path" "$appdir_path/.DirIcon" || exit 1
|
||||
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" || exit 1
|
||||
|
||||
# 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"
|
||||
|
||||
# Generate the AppStream XML file
|
||||
# project_license describes the app the user launches (the proprietary
|
||||
# Claude binary), not the MIT packaging scripts
|
||||
# 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>LicenseRef-proprietary</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>
|
||||
|
||||
<icon type="stock">${component_id}</icon>
|
||||
<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>
|
||||
</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 is a native binary that must run on the HOST machine, not
|
||||
# the package's target architecture: CI cross-builds (e.g. an arm64
|
||||
# package on an ubuntu-latest/x86_64 runner) need the x86_64 tool even
|
||||
# though $architecture says arm64. Select strictly by uname -m here;
|
||||
# the target architecture is only used later for the embedded ARCH.
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in
|
||||
x86_64|aarch64) ;;
|
||||
*)
|
||||
echo "Unsupported host architecture for appimagetool: $host_arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
appimagetool_path=''
|
||||
|
||||
# Check system PATH first
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
appimagetool_path=$(command -v appimagetool)
|
||||
echo "Found appimagetool in PATH: $appimagetool_path"
|
||||
fi
|
||||
|
||||
# Check for a previously downloaded HOST-arch tool
|
||||
if [[ -z $appimagetool_path ]]; then
|
||||
local_path="$work_dir/appimagetool-${host_arch}.AppImage"
|
||||
if [[ -f $local_path ]]; then
|
||||
appimagetool_path="$local_path"
|
||||
echo "Found downloaded ${host_arch} appimagetool: $appimagetool_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Download if not found
|
||||
if [[ -z $appimagetool_path ]]; then
|
||||
echo 'Downloading appimagetool...'
|
||||
|
||||
appimagetool_url="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${host_arch}.AppImage"
|
||||
appimagetool_path="$work_dir/appimagetool-${host_arch}.AppImage"
|
||||
|
||||
if wget -q -O "$appimagetool_path" "$appimagetool_url"; then
|
||||
chmod +x "$appimagetool_path" || exit 1
|
||||
echo "Downloaded appimagetool to $appimagetool_path"
|
||||
else
|
||||
echo "Failed to download appimagetool from $appimagetool_url" >&2
|
||||
rm -f "$appimagetool_path"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalize AppDir permissions before squashing. The staging copy above
|
||||
# uses `cp -a`, which preserves source modes, and a restrictive build
|
||||
# umask can leave directories at 0700. mksquashfs records those verbatim,
|
||||
# so a user who later runs the AppImage can't traverse into
|
||||
# app.asar.unpacked/ — silently breaking Cowork's daemon auto-launch (the
|
||||
# fork is guarded by fs.existsSync(), false on a directory it can't read).
|
||||
# Canonical modes: dirs and already-executable files 755, the rest 644.
|
||||
echo 'Normalizing AppDir permissions...'
|
||||
find "$appdir_path" -type d -exec chmod 755 {} + || exit 1
|
||||
find "$appdir_path" -type f -exec chmod u=rwX,go=rX {} + || exit 1
|
||||
|
||||
# --- Build AppImage ---
|
||||
echo 'Building AppImage...'
|
||||
output_filename="${package_name}-${version}-${architecture}.AppImage"
|
||||
output_path="$work_dir/$output_filename"
|
||||
|
||||
# ARCH names the TARGET architecture (canonical uname-style), which can
|
||||
# differ from the host running the tool during a cross-build. It only
|
||||
# covers naming/validation: appimagetool ALWAYS embeds the runtime stub
|
||||
# bundled with the tool itself, which is host-arch. On a cross-build
|
||||
# that bakes an x86_64 stub into an arm64 AppImage, which then can't
|
||||
# start on target hardware (caught by test-artifacts on the first
|
||||
# native-arm64 run). Fetch the TARGET-arch runtime from the same
|
||||
# release as the tool and force it in with --runtime-file.
|
||||
case "$architecture" in
|
||||
amd64) export ARCH='x86_64' ;;
|
||||
arm64) export ARCH='aarch64' ;;
|
||||
*)
|
||||
echo "Unsupported target architecture for ARCH: $architecture" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "Using ARCH=$ARCH"
|
||||
|
||||
runtime_path="$work_dir/appimage-runtime-${ARCH}"
|
||||
if [[ ! -f $runtime_path ]]; then
|
||||
runtime_url="https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-${ARCH}"
|
||||
echo "Downloading AppImage runtime for ${ARCH}..."
|
||||
if ! wget -q -O "$runtime_path" "$runtime_url"; then
|
||||
echo "Failed to download AppImage runtime from $runtime_url" >&2
|
||||
rm -f "$runtime_path"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Local build - no update information
|
||||
if [[ $GITHUB_ACTIONS != 'true' ]]; then
|
||||
echo 'Running locally - building AppImage without update information'
|
||||
echo '(Update info and zsync files are only generated in GitHub Actions for releases)'
|
||||
|
||||
if ! "$appimagetool_path" --runtime-file "$runtime_path" \
|
||||
"$appdir_path" "$output_path"; then
|
||||
echo "Failed to build AppImage using $appimagetool_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "AppImage built successfully: $output_path"
|
||||
echo '--- AppImage Build Finished ---'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# GitHub Actions build - embed update information
|
||||
echo 'Running in GitHub Actions - embedding update information for automatic updates...'
|
||||
|
||||
# Install zsync if needed for .zsync file generation
|
||||
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>
|
||||
# The 'claude-desktop-*' wildcard is deliberately NOT renamed along with
|
||||
# $package_name: it matches both the old claude-desktop-* and the new
|
||||
# claude-desktop-unofficial-* artifact names, so AppImages installed
|
||||
# before the rename keep self-updating. Do not narrow it.
|
||||
update_info="gh-releases-zsync|aaddrick|claude-desktop-debian|latest|claude-desktop-*-${architecture}.AppImage.zsync"
|
||||
echo "Update info: $update_info"
|
||||
|
||||
if ! "$appimagetool_path" --runtime-file "$runtime_path" \
|
||||
--updateinformation "$update_info" "$appdir_path" "$output_path"; then
|
||||
echo "Failed to build AppImage using $appimagetool_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "AppImage built successfully with embedded update info: $output_path"
|
||||
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
|
||||
|
||||
echo '--- AppImage Build Finished ---'
|
||||
|
||||
exit 0
|
||||
Executable
+437
@@ -0,0 +1,437 @@
|
||||
#!/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...'
|
||||
# The official tree ships hicolor PNGs (16-256px). The source basename
|
||||
# stays upstream's claude-desktop.png, but the destination is renamed to
|
||||
# $package_name.png so our files never collide with the icons installed
|
||||
# by Anthropic's official claude-desktop package.
|
||||
official_hicolor="${CLAUDE_EXTRACT_DIR:?}/usr/share/icons/hicolor"
|
||||
found_icons=false
|
||||
for icon_source_path in "$official_hicolor"/*/apps/claude-desktop.png; do
|
||||
[[ -f $icon_source_path ]] || continue
|
||||
size_dir=$(basename "$(dirname "$(dirname "$icon_source_path")")")
|
||||
echo "Installing $size_dir icon..."
|
||||
install -Dm 644 "$icon_source_path" \
|
||||
"$install_dir/share/icons/hicolor/$size_dir/apps/$package_name.png" || exit 1
|
||||
found_icons=true
|
||||
done
|
||||
if [[ $found_icons == false ]]; then
|
||||
echo "Warning: no hicolor icons found under $official_hicolor"
|
||||
fi
|
||||
echo 'Icons installed'
|
||||
|
||||
# --- Copy Application Files ---
|
||||
echo "Copying application files from $app_staging_dir..."
|
||||
|
||||
# The staging dir is the extracted official usr/lib/claude-desktop tree
|
||||
# (Electron ELF, chrome-sandbox, resources/, locales/, ...); ship it as-is.
|
||||
cp -a "$app_staging_dir/." "$install_dir/lib/$package_name/" || exit 1
|
||||
echo 'Official application tree copied'
|
||||
|
||||
# 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
|
||||
sed -i "s/@@WM_CLASS@@/$WM_CLASS/" "$install_dir/lib/$package_name/launcher-common.sh"
|
||||
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/$package_name.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Name=Claude
|
||||
Exec=/usr/bin/$package_name %u
|
||||
Icon=$package_name
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Categories=Office;Utility;
|
||||
MimeType=x-scheme-handler/claude;
|
||||
StartupWMClass=$WM_CLASS
|
||||
EOF
|
||||
echo 'Desktop entry created'
|
||||
|
||||
# --- Install AppStream metainfo (App Center / GNOME Software / KDE Discover) ---
|
||||
echo 'Installing AppStream metainfo...'
|
||||
metainfo_name='io.github.aaddrick.claude-desktop-unofficial.metainfo.xml'
|
||||
install -Dm 644 "$script_dir/$metainfo_name" \
|
||||
"$install_dir/share/metainfo/$metainfo_name" || exit 1
|
||||
echo 'AppStream metainfo installed'
|
||||
|
||||
# --- Create Launcher Script ---
|
||||
echo 'Creating launcher script...'
|
||||
cat > "$install_dir/bin/$package_name" << EOF
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Source shared launcher library
|
||||
source "/usr/lib/$package_name/launcher-common.sh"
|
||||
|
||||
# The official Electron binary; it auto-loads the co-located
|
||||
# resources/app.asar, so no app path is ever passed (issue #696).
|
||||
app_exec="/usr/lib/$package_name/claude-desktop"
|
||||
|
||||
# Handle --doctor flag before anything else
|
||||
if [[ "\${1:-}" == '--doctor' ]]; then
|
||||
run_doctor "\$app_exec"
|
||||
exit \$?
|
||||
fi
|
||||
|
||||
# --version never reaches the terminal via Electron: the launcher
|
||||
# redirects all app output to the log (#772). Answer it here instead.
|
||||
if [[ "\${1:-}" == '--version' ]]; then
|
||||
echo "$package_name $version"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup logging and environment
|
||||
setup_logging || exit 1
|
||||
setup_electron_env
|
||||
|
||||
cleanup_orphaned_cowork_daemon
|
||||
cleanup_stale_desktop_helpers
|
||||
cleanup_stale_lock
|
||||
cleanup_stale_cowork_socket
|
||||
heal_autostart_entry "/usr/bin/$package_name"
|
||||
backup_user_config
|
||||
|
||||
# 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
|
||||
|
||||
if [[ ! -x \$app_exec ]]; then
|
||||
log_message "Error: Claude Desktop binary not found at \$app_exec"
|
||||
echo "Error: Claude Desktop binary not found at \$app_exec" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build Chromium switches for the official binary
|
||||
build_electron_args 'deb'
|
||||
|
||||
# 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 the official binary and keep the launcher alive so explicit
|
||||
# quit can clean up Desktop-owned helpers that outlive the main process.
|
||||
log_message "Executing: \$app_exec \${electron_args[*]} \$*"
|
||||
run_electron_and_cleanup "\$app_exec" "\${electron_args[@]}" "\$@"
|
||||
exit \$?
|
||||
EOF
|
||||
chmod +x "$install_dir/bin/$package_name" || exit 1
|
||||
echo 'Launcher script created'
|
||||
|
||||
# --- Create Control File ---
|
||||
echo 'Creating control file...'
|
||||
# Depends/Recommends are re-emitted verbatim from the extracted official
|
||||
# control file (exported by build.sh): the contract differs per arch
|
||||
# (arm64 recommends qemu-system-arm/qemu-efi-aarch64 instead of
|
||||
# qemu-system-x86/ovmf) and tracks upstream automatically this way.
|
||||
|
||||
{
|
||||
echo "Package: $package_name"
|
||||
echo "Version: $version"
|
||||
echo 'Section: utils'
|
||||
echo 'Priority: optional'
|
||||
echo "Architecture: $architecture"
|
||||
# The 'claude-desktop' name is shared by two other packages: our own
|
||||
# legacy repack (<= 1.15200.x) and Anthropic's official package
|
||||
# (>= 1.17377.1). The relation must stay version-scoped to hit only
|
||||
# the legacy repack — unscoped, apt would remove the official package
|
||||
# on install. The bound also sits below the 1.16000.0-1 transitional
|
||||
# dummy built at the end of this script, so the dummy and this
|
||||
# package can coexist during the rename upgrade.
|
||||
echo 'Conflicts: claude-desktop (<< 1.16000)'
|
||||
echo 'Replaces: claude-desktop (<< 1.16000)'
|
||||
if [[ -n ${OFFICIAL_DEB_DEPENDS:-} ]]; then
|
||||
echo "Depends: $OFFICIAL_DEB_DEPENDS"
|
||||
fi
|
||||
if [[ -n ${OFFICIAL_DEB_RECOMMENDS:-} ]]; then
|
||||
echo "Recommends: $OFFICIAL_DEB_RECOMMENDS"
|
||||
fi
|
||||
echo "Maintainer: $maintainer"
|
||||
echo "Description: $description"
|
||||
echo ' Claude is an AI assistant from Anthropic.'
|
||||
echo ' This package provides the desktop interface for Claude.'
|
||||
echo ' .'
|
||||
echo ' Supported on Debian-based Linux distributions (Debian, Ubuntu, Linux Mint, MX Linux, etc.)'
|
||||
} > "$package_root/DEBIAN/control"
|
||||
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 2>&1 || true
|
||||
|
||||
# Set correct permissions for chrome-sandbox. The official data.tar
|
||||
# records it SUID, but our non-root ar|tar extraction strips the bit, so
|
||||
# it must be re-asserted here.
|
||||
echo "Setting chrome-sandbox permissions..."
|
||||
SANDBOX_PATH=""
|
||||
LOCAL_SANDBOX_PATH="/usr/lib/$package_name/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
|
||||
|
||||
# --- AppArmor profile for Chromium's user-namespace sandbox ---
|
||||
# Ubuntu 24.04+ sets kernel.apparmor_restrict_unprivileged_userns=1, which
|
||||
# blocks the unprivileged user namespaces Chromium's sandbox relies on,
|
||||
# crashing the app on launch with a sandbox/.../credentials.cc FATAL.
|
||||
# Grant userns to our Electron binary via a scoped AppArmor profile, exactly
|
||||
# as the google-chrome, code, and slack packages do. Gate on the kernel knob
|
||||
# (not just apparmor_parser): only Ubuntu-family systems impose the
|
||||
# restriction, so on stock Debian/others the knob is absent and we skip the
|
||||
# profile entirely rather than installing one they never need. The knob may
|
||||
# read 0 now and flip to 1 later, so existence — not value — is the gate.
|
||||
APPARMOR_PROFILE="/etc/apparmor.d/$package_name"
|
||||
if command -v apparmor_parser >/dev/null 2>&1 \
|
||||
&& [ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
|
||||
echo "Configuring AppArmor profile for Chromium sandbox..."
|
||||
# Writing the profile is best-effort: a read-only or atypical /etc must
|
||||
# never abort the install (this postinst runs under set -e). Keeping the
|
||||
# grep / mkdir + heredoc in the if/elif conditions exempts them from
|
||||
# errexit. Debian Policy 10.7.3: a profile without our marker header was
|
||||
# hand-created or hand-edited by the admin — preserve it, never overwrite.
|
||||
if [ -e "\$APPARMOR_PROFILE" ] \
|
||||
&& ! grep -qF "managed by the $package_name package" \
|
||||
"\$APPARMOR_PROFILE" 2>/dev/null; then
|
||||
echo "Preserving locally modified \$APPARMOR_PROFILE (no marker header)"
|
||||
apparmor_parser -r "\$APPARMOR_PROFILE" >/dev/null 2>&1 || true
|
||||
elif mkdir -p /etc/apparmor.d 2>/dev/null && cat > "\$APPARMOR_PROFILE" <<'APPARMOR_EOF'
|
||||
# This profile is managed by the $package_name package (postinst); direct
|
||||
# edits will be overwritten on upgrade. Put local changes in
|
||||
# /etc/apparmor.d/local/$package_name instead.
|
||||
abi <abi/4.0>,
|
||||
include <tunables/global>
|
||||
|
||||
profile $package_name /usr/lib/$package_name/claude-desktop flags=(unconfined) {
|
||||
userns,
|
||||
|
||||
include if exists <local/$package_name>
|
||||
}
|
||||
APPARMOR_EOF
|
||||
then
|
||||
if apparmor_parser -Q "\$APPARMOR_PROFILE" >/dev/null 2>&1; then
|
||||
apparmor_parser -r "\$APPARMOR_PROFILE" >/dev/null 2>&1 || echo "Note: AppArmor profile staged but not loaded now; it will apply on the next AppArmor reload or reboot."
|
||||
echo "AppArmor profile installed at \$APPARMOR_PROFILE"
|
||||
else
|
||||
rm -f "\$APPARMOR_PROFILE"
|
||||
echo "AppArmor on this system does not support the userns rule; skipping profile (not required here)."
|
||||
fi
|
||||
else
|
||||
# A failed write may leave a truncated profile behind; clear it.
|
||||
# The || true is mandatory: this branch is errexit-live, and a bare
|
||||
# rm fails the upgrade on a read-only /etc.
|
||||
rm -f "\$APPARMOR_PROFILE" 2>/dev/null || true
|
||||
echo "Warning: could not write \$APPARMOR_PROFILE; skipping AppArmor profile."
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$package_root/DEBIAN/postinst" || exit 1
|
||||
echo 'Postinst script created'
|
||||
|
||||
# --- Create Postrm Script ---
|
||||
echo 'Creating postrm script...'
|
||||
# The AppArmor profiles are generated by postinst, not tracked by dpkg, so we
|
||||
# unload and delete them ourselves. Cleanup lives in postrm (not prerm) so it
|
||||
# also fires on purge and abort-install. Skip on upgrade — the incoming
|
||||
# postinst rewrites and reloads them. 'disappear' is deliberately not handled:
|
||||
# matching it would also clean during the overwrite-by-another-package flow.
|
||||
# Three profile names: the Electron one (Chromium sandbox, #687), the bwrap
|
||||
# one (Cowork sandbox helper, #694), and the legacy pre-rename names. The
|
||||
# bwrap profile is no longer installed since the official-deb rebase parked
|
||||
# the bwrap Cowork backend, but 2.x installs left it behind (as
|
||||
# 'claude-desktop-bwrap', before the rename) — keep removing it here.
|
||||
# The legacy 'claude-desktop' Electron profile needs care: Anthropic's
|
||||
# official package postinst also writes /etc/apparmor.d/claude-desktop
|
||||
# (untracked by dpkg, so ownership can't be queried) — only our marker
|
||||
# header proves the file came from our legacy 2.x postinst. Markerless
|
||||
# files (official's, admin-created, or pre-v2.0.19 ours) are preserved.
|
||||
# Per Debian Policy 10.7.3 the profiles are configuration: unload them
|
||||
# whenever the confined binaries go away, but delete the files only on
|
||||
# purge — a profile for an absent binary is a harmless no-op (google-chrome
|
||||
# leaves its profile behind the same way).
|
||||
cat > "$package_root/DEBIAN/postrm" << EOF
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "\$1" in
|
||||
remove|purge|abort-install)
|
||||
for _profile in "/etc/apparmor.d/$package_name" \
|
||||
"/etc/apparmor.d/${package_name}-bwrap" \
|
||||
"/etc/apparmor.d/claude-desktop-bwrap"; do
|
||||
if [ -e "\$_profile" ] \
|
||||
&& command -v apparmor_parser >/dev/null 2>&1; then
|
||||
apparmor_parser -R "\$_profile" >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Policy 10.7.3: config survives remove; delete on purge only.
|
||||
if [ "\$1" = purge ]; then
|
||||
rm -f "\$_profile" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# Legacy 2.x profile from before the rename to $package_name.
|
||||
# The official claude-desktop package writes the same path from
|
||||
# its postinst; touch it only when our marker header proves our
|
||||
# legacy postinst wrote it.
|
||||
_legacy='/etc/apparmor.d/claude-desktop'
|
||||
if [ -e "\$_legacy" ] \
|
||||
&& grep -qF 'managed by the claude-desktop package' \
|
||||
"\$_legacy" 2>/dev/null; then
|
||||
if command -v apparmor_parser >/dev/null 2>&1; then
|
||||
apparmor_parser -R "\$_legacy" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "\$1" = purge ]; then
|
||||
rm -f "\$_legacy" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$package_root/DEBIAN/postrm" || exit 1
|
||||
echo 'Postrm 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
|
||||
chmod 755 "$package_root/DEBIAN/postrm" || exit 1
|
||||
|
||||
# Normalize the installed tree before building. A restrictive build umask
|
||||
# can leave directories at 0700, and dpkg-deb records file ownership
|
||||
# verbatim unless told otherwise. Both bite at runtime: the launcher runs
|
||||
# as the desktop user, who then can't traverse into app.asar.unpacked/ —
|
||||
# silently breaking Cowork's daemon auto-launch (the fork is guarded by
|
||||
# fs.existsSync(), which returns false on a directory it can't read, so
|
||||
# the symptom is an endless connect ENOENT on the VM-service socket with
|
||||
# no daemon log and no [cowork-autolaunch] line). Canonical modes: dirs
|
||||
# and already-executable files 755, every other file 644. The blanket
|
||||
# pass clears chrome-sandbox's setuid bit, but postinst re-asserts 4755
|
||||
# after install, so the net result is unchanged.
|
||||
echo 'Normalizing installed tree permissions...'
|
||||
find "$install_dir" -type d -exec chmod 755 {} + || exit 1
|
||||
find "$install_dir" -type f -exec chmod u=rwX,go=rX {} + || exit 1
|
||||
|
||||
# --root-owner-group forces root:root in the archive so a leaked build
|
||||
# uid can't deny access on the installed system (the build does not run
|
||||
# under fakeroot).
|
||||
if ! dpkg-deb --root-owner-group --build "$package_root" "$deb_file"; then
|
||||
echo 'Failed to build .deb package' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deb package built successfully: $deb_file"
|
||||
|
||||
# --- Build Transitional Dummy Package ---
|
||||
# The package renamed from 'claude-desktop' to claude-desktop-unofficial
|
||||
# when Anthropic's official package claimed the old name. This
|
||||
# control-only dummy walks legacy repack installs (<= 1.15200.x) over
|
||||
# the rename: apt upgrades the old 'claude-desktop' to it, and its
|
||||
# Depends pulls in the real package. Version 1.16000.0-1 sits above
|
||||
# every legacy repack and below the first official build (1.17377.1),
|
||||
# so the official package still upgrades cleanly over the dummy.
|
||||
# Architecture: all — built on the amd64 leg only so the arm64 CI leg
|
||||
# doesn't emit a duplicate artifact. build.sh moves it next to the
|
||||
# main .deb (the filename is referenced there; keep them in sync).
|
||||
if [[ $architecture == 'amd64' ]]; then
|
||||
echo 'Building transitional dummy package...'
|
||||
transitional_root="$work_dir/transitional-package"
|
||||
transitional_deb="$work_dir/claude-desktop_1.16000.0-1_all.deb"
|
||||
rm -rf "$transitional_root"
|
||||
mkdir -p "$transitional_root/DEBIAN" || exit 1
|
||||
{
|
||||
echo 'Package: claude-desktop'
|
||||
echo 'Version: 1.16000.0-1'
|
||||
echo 'Architecture: all'
|
||||
echo "Depends: $package_name"
|
||||
echo 'Section: oldlibs'
|
||||
echo 'Priority: optional'
|
||||
echo "Maintainer: $maintainer"
|
||||
echo 'Description: Transitional package for the rename to claude-desktop-unofficial'
|
||||
echo " This dummy package eases upgrades from the legacy 'claude-desktop'"
|
||||
echo " community repack to its new name, $package_name."
|
||||
echo ' It can be safely removed after the upgrade.'
|
||||
} > "$transitional_root/DEBIAN/control"
|
||||
chmod 755 "$transitional_root/DEBIAN" || exit 1
|
||||
if ! dpkg-deb --root-owner-group --build \
|
||||
"$transitional_root" "$transitional_deb"; then
|
||||
echo 'Failed to build transitional .deb package' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Transitional package built successfully: $transitional_deb"
|
||||
fi
|
||||
|
||||
echo '--- Debian Package Build Finished ---'
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
AppStream metainfo for the claude-desktop-unofficial package.
|
||||
Indexed by GNOME Software / Ubuntu App Center / KDE Discover under the
|
||||
Installed tab so users can see the package with name, summary, icon, and
|
||||
release history rather than as an unidentified entry.
|
||||
|
||||
See: https://www.freedesktop.org/software/appstream/docs/
|
||||
-->
|
||||
<component type="desktop-application">
|
||||
<id>io.github.aaddrick.claude-desktop-debian</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>LicenseRef-proprietary</project_license>
|
||||
|
||||
<name>Claude Desktop</name>
|
||||
<summary>Unofficial desktop client for Claude AI</summary>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
Claude Desktop is an unofficial community repackaging of Anthropic's
|
||||
Claude Desktop client for Debian and Ubuntu. The upstream Windows
|
||||
binary is repacked and patched for Linux compatibility (frame, tray,
|
||||
Cowork mode, MCP stdio, Quick Entry, etc.).
|
||||
</p>
|
||||
<p>Features:</p>
|
||||
<ul>
|
||||
<li>Conversations with the Claude model family (Sonnet, Opus, Haiku)</li>
|
||||
<li>Projects with persistent context and file uploads</li>
|
||||
<li>Cowork mode — local agent VM for sandboxed code tasks</li>
|
||||
<li>MCP (Model Context Protocol) stdio servers for tool integration</li>
|
||||
<li>System tray, Quick Entry hotkey, and tab system</li>
|
||||
</ul>
|
||||
<p>
|
||||
This packaging is community-maintained and is not affiliated with or
|
||||
endorsed by Anthropic. See the packaging source and issue tracker
|
||||
linked below.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">claude-desktop-unofficial.desktop</launchable>
|
||||
|
||||
<url type="homepage">https://github.com/aaddrick/claude-desktop-debian</url>
|
||||
<url type="bugtracker">https://github.com/aaddrick/claude-desktop-debian/issues</url>
|
||||
<url type="vcs-browser">https://github.com/aaddrick/claude-desktop-debian</url>
|
||||
|
||||
<developer id="io.github.aaddrick">
|
||||
<name>aaddrick</name>
|
||||
</developer>
|
||||
|
||||
<categories>
|
||||
<category>Office</category>
|
||||
<category>Utility</category>
|
||||
</categories>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<provides>
|
||||
<binary>claude-desktop-unofficial</binary>
|
||||
</provides>
|
||||
</component>
|
||||
Executable
+376
@@ -0,0 +1,376 @@
|
||||
#!/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"
|
||||
# $6 is maintainer (unused in RPM spec, kept for parameter compatibility with deb)
|
||||
description="$7"
|
||||
|
||||
echo '--- Starting RPM Package Build ---'
|
||||
echo "Version: $version"
|
||||
|
||||
# RPM Version field cannot contain hyphens. If version contains a hyphen,
|
||||
# split into version (before hyphen) and release (after hyphen).
|
||||
# e.g., "1.1.799-1.3.3" -> rpm_version="1.1.799", rpm_release="1.3.3"
|
||||
if [[ $version == *-* ]]; then
|
||||
rpm_version="${version%%-*}"
|
||||
rpm_release="${version#*-}"
|
||||
# RPM Release field cannot contain hyphens either. The wrapper
|
||||
# suffix appended in official-deb.sh can itself carry an RC suffix
|
||||
# (e.g. "3.0.0-rc1"), which lands entirely in rpm_release here since
|
||||
# the split above only cuts on the first hyphen.
|
||||
rpm_release="${rpm_release//-/.}"
|
||||
echo "RPM Version: $rpm_version"
|
||||
echo "RPM Release: $rpm_release"
|
||||
else
|
||||
rpm_version="$version"
|
||||
rpm_release="1"
|
||||
fi
|
||||
echo "Architecture: $architecture"
|
||||
echo "Work Directory: $work_dir"
|
||||
echo "App Staging Directory: $app_staging_dir"
|
||||
echo "Package Name: $package_name"
|
||||
|
||||
# Map architecture to RPM naming
|
||||
case "$architecture" in
|
||||
amd64) rpm_arch='x86_64' ;;
|
||||
arm64) rpm_arch='aarch64' ;;
|
||||
*)
|
||||
echo "Unsupported architecture for RPM: $architecture" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# RPM build directories
|
||||
rpmbuild_dir="$work_dir/rpmbuild"
|
||||
|
||||
# Clean previous RPM build structure if it exists
|
||||
rm -rf "$rpmbuild_dir"
|
||||
|
||||
# Create RPM build directory structure
|
||||
echo "Creating RPM build structure in $rpmbuild_dir..."
|
||||
mkdir -p "$rpmbuild_dir"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} || exit 1
|
||||
|
||||
# Get script directory for accessing launcher-common.sh
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Create staging area for files to include
|
||||
staging_dir="$work_dir/rpm-staging"
|
||||
rm -rf "$staging_dir"
|
||||
mkdir -p "$staging_dir" || exit 1
|
||||
|
||||
# --- Create Desktop Entry ---
|
||||
echo 'Creating desktop entry...'
|
||||
cat > "$staging_dir/$package_name.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Name=Claude
|
||||
Exec=/usr/bin/$package_name %u
|
||||
Icon=$package_name
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Categories=Office;Utility;
|
||||
MimeType=x-scheme-handler/claude;
|
||||
StartupWMClass=$WM_CLASS
|
||||
EOF
|
||||
|
||||
# --- Stage AppStream metainfo (installed via %files block below) ---
|
||||
metainfo_name='io.github.aaddrick.claude-desktop-unofficial.metainfo.xml'
|
||||
cp "$script_dir/$metainfo_name" "$staging_dir/$metainfo_name" || exit 1
|
||||
|
||||
# --- Create Launcher Script ---
|
||||
echo 'Creating launcher script...'
|
||||
cat > "$staging_dir/$package_name" << EOF
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Source shared launcher library
|
||||
source "/usr/lib/$package_name/launcher-common.sh"
|
||||
|
||||
# The official Electron binary; it auto-loads the co-located
|
||||
# resources/app.asar, so no app path is ever passed (issue #696).
|
||||
app_exec="/usr/lib/$package_name/claude-desktop"
|
||||
|
||||
# Handle --doctor flag before anything else
|
||||
if [[ "\${1:-}" == '--doctor' ]]; then
|
||||
run_doctor "\$app_exec"
|
||||
exit \$?
|
||||
fi
|
||||
|
||||
# --version never reaches the terminal via Electron: the launcher
|
||||
# redirects all app output to the log (#772). Answer it here instead.
|
||||
if [[ "\${1:-}" == '--version' ]]; then
|
||||
echo "$package_name $version"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup logging and environment
|
||||
setup_logging || exit 1
|
||||
setup_electron_env
|
||||
|
||||
cleanup_orphaned_cowork_daemon
|
||||
cleanup_stale_desktop_helpers
|
||||
cleanup_stale_lock
|
||||
cleanup_stale_cowork_socket
|
||||
heal_autostart_entry "/usr/bin/$package_name"
|
||||
backup_user_config
|
||||
|
||||
# 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
|
||||
|
||||
if [[ ! -x \$app_exec ]]; then
|
||||
log_message "Error: Claude Desktop binary not found at \$app_exec"
|
||||
echo "Error: Claude Desktop binary not found at \$app_exec" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build Chromium switches - use 'deb' type (same sandbox behavior)
|
||||
build_electron_args 'deb'
|
||||
|
||||
# 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 the official binary and keep the launcher alive so explicit
|
||||
# quit can clean up Desktop-owned helpers that outlive the main process.
|
||||
log_message "Executing: \$app_exec \${electron_args[*]} \$*"
|
||||
run_electron_and_cleanup "\$app_exec" "\${electron_args[@]}" "\$@"
|
||||
exit \$?
|
||||
EOF
|
||||
chmod +x "$staging_dir/$package_name"
|
||||
|
||||
# --- Create RPM Spec File ---
|
||||
echo 'Creating RPM spec file...'
|
||||
|
||||
# Build icon installation commands from the official hicolor tree. The
|
||||
# source basename stays upstream's claude-desktop.png; the destination
|
||||
# is renamed to $package_name.png so our files never collide with the
|
||||
# icons installed by Anthropic's official claude-desktop package.
|
||||
icon_install_cmds=""
|
||||
official_hicolor="${CLAUDE_EXTRACT_DIR:?}/usr/share/icons/hicolor"
|
||||
|
||||
for icon_source_path in "$official_hicolor"/*/apps/claude-desktop.png; do
|
||||
[[ -f $icon_source_path ]] || continue
|
||||
size_dir=$(basename "$(dirname "$(dirname "$icon_source_path")")")
|
||||
icon_install_cmds+="install -Dm 644 $icon_source_path %{buildroot}/usr/share/icons/hicolor/${size_dir}/apps/${package_name}.png
|
||||
"
|
||||
done
|
||||
|
||||
# CW-1: the official Cowork client probes a hardcoded, Debian-layout
|
||||
# firmware list (x64: /usr/share/OVMF/OVMF_CODE{_4M,}.fd, arm64:
|
||||
# /usr/share/AAVMF/AAVMF_CODE.fd) with no env override. Fedora ships
|
||||
# its own /usr/share/OVMF compat layer, but other RPM hosts (openSUSE,
|
||||
# Arch-derived layouts) put edk2 firmware elsewhere, so Cowork breaks
|
||||
# out of the box there. The %post scriptlet below creates a compat
|
||||
# symlink at the probed path when no probed path exists but a known
|
||||
# edk2/qemu layout does; %postun removes it on erase (never a real
|
||||
# file, never another package's symlink). Filed upstream as the
|
||||
# OVMF-probe-rigidity report.
|
||||
if [[ $rpm_arch == 'aarch64' ]]; then
|
||||
fw_probe_dir='/usr/share/AAVMF'
|
||||
fw_probe_list='/usr/share/AAVMF/AAVMF_CODE.fd'
|
||||
fw_link_name='AAVMF_CODE.fd'
|
||||
fw_candidates='/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw'
|
||||
fw_candidates+=' /usr/share/qemu/aavmf-aarch64-code.bin'
|
||||
else
|
||||
fw_probe_dir='/usr/share/OVMF'
|
||||
fw_probe_list='/usr/share/OVMF/OVMF_CODE_4M.fd'
|
||||
fw_probe_list+=' /usr/share/OVMF/OVMF_CODE.fd'
|
||||
fw_link_name='OVMF_CODE_4M.fd'
|
||||
fw_candidates='/usr/share/edk2/ovmf/OVMF_CODE.fd'
|
||||
fw_candidates+=' /usr/share/edk2/x64/OVMF_CODE.4m.fd'
|
||||
fw_candidates+=' /usr/share/qemu/ovmf-x86_64-code.bin'
|
||||
fi
|
||||
|
||||
cat > "$rpmbuild_dir/SPECS/$package_name.spec" << SPECEOF
|
||||
Name: $package_name
|
||||
Version: $rpm_version
|
||||
Release: $rpm_release%{?dist}
|
||||
Summary: $description
|
||||
|
||||
License: Proprietary
|
||||
URL: https://claude.ai
|
||||
|
||||
# The 'claude-desktop' name is our legacy repack (<= 1.15200.x); no
|
||||
# official rpm exists. Obsoletes gives dnf the automatic rename
|
||||
# upgrade path; the bound mirrors the deb Conflicts/Replaces scope.
|
||||
Obsoletes: claude-desktop < 1.16000
|
||||
Provides: claude-desktop = %{version}-%{release}
|
||||
|
||||
# Disable automatic dependency scanning (we bundle everything)
|
||||
AutoReqProv: no
|
||||
|
||||
# Disable debug package generation
|
||||
%define debug_package %{nil}
|
||||
|
||||
# Disable binary stripping (Electron binaries don't like it)
|
||||
%define __strip /bin/true
|
||||
|
||||
# Disable build ID generation (avoids issues with Electron binaries)
|
||||
%define _build_id_links none
|
||||
|
||||
%description
|
||||
Claude is an AI assistant from Anthropic.
|
||||
This package provides the desktop interface for Claude.
|
||||
|
||||
Supported on RPM-based Linux distributions (Fedora, RHEL, CentOS, etc.)
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}/usr/lib/$package_name
|
||||
mkdir -p %{buildroot}/usr/share/applications
|
||||
mkdir -p %{buildroot}/usr/bin
|
||||
|
||||
# Install icons
|
||||
$icon_install_cmds
|
||||
|
||||
# Copy application files (the extracted official usr/lib/claude-desktop
|
||||
# tree: Electron ELF, chrome-sandbox, resources/, locales/, ...)
|
||||
cp -a $app_staging_dir/. %{buildroot}/usr/lib/$package_name/
|
||||
|
||||
# Copy shared launcher library (launcher-common.sh sources doctor.sh
|
||||
# at runtime, so both must live in the same directory)
|
||||
cp $(dirname "$script_dir")/launcher-common.sh %{buildroot}/usr/lib/$package_name/
|
||||
sed -i "s/@@WM_CLASS@@/$WM_CLASS/" "%{buildroot}/usr/lib/$package_name/launcher-common.sh"
|
||||
cp $(dirname "$script_dir")/doctor.sh %{buildroot}/usr/lib/$package_name/
|
||||
|
||||
# Install desktop entry
|
||||
install -Dm 644 $staging_dir/$package_name.desktop %{buildroot}/usr/share/applications/$package_name.desktop
|
||||
|
||||
# Install AppStream metainfo (GNOME Software / KDE Discover)
|
||||
install -Dm 644 $staging_dir/$metainfo_name %{buildroot}/usr/share/metainfo/$metainfo_name
|
||||
|
||||
# Install launcher script
|
||||
install -Dm 755 $staging_dir/$package_name %{buildroot}/usr/bin/$package_name
|
||||
|
||||
# Normalize file modes — the cp -r above honors the build umask, and
|
||||
# the "-" first field of %defattr ships buildroot *file* modes verbatim
|
||||
# (only directory modes are forced to 0755), so a umask-077 build would
|
||||
# package an unreadable app.asar and a non-executable electron binary.
|
||||
# Must run before the chrome-sandbox chmod below so 4755 survives.
|
||||
find %{buildroot}/usr/lib/$package_name -type f -exec chmod u=rwX,go=rX {} +
|
||||
|
||||
# Set the chrome-sandbox suid bit in the buildroot so the /usr/lib
|
||||
# directory walk in %files records 4755 in the payload (preserves #539
|
||||
# without the "File listed twice" warning #609 — see %files block).
|
||||
# The official data.tar records the bit, but our non-root ar|tar
|
||||
# extraction strips it.
|
||||
chmod 4755 %{buildroot}/usr/lib/$package_name/chrome-sandbox
|
||||
|
||||
%post
|
||||
# Update desktop database for MIME types
|
||||
update-desktop-database /usr/share/applications > /dev/null 2>&1 || true
|
||||
|
||||
# Cowork firmware compat symlink (CW-1): the official client probes a
|
||||
# hardcoded Debian-layout firmware list with no env override. If no
|
||||
# probed path exists but a known edk2/qemu layout does, bridge it.
|
||||
fw_have=''
|
||||
for fw_probe in $fw_probe_list; do
|
||||
if [ -e "\$fw_probe" ]; then
|
||||
fw_have=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "\$fw_have" ]; then
|
||||
for fw_src in $fw_candidates; do
|
||||
if [ -e "\$fw_src" ]; then
|
||||
if mkdir -p $fw_probe_dir 2>/dev/null \\
|
||||
&& ln -sf "\$fw_src" $fw_probe_dir/$fw_link_name 2>/dev/null
|
||||
then
|
||||
echo "Cowork firmware compat symlink: $fw_probe_dir/$fw_link_name -> \$fw_src"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
%postun
|
||||
# Update desktop database after removal
|
||||
update-desktop-database /usr/share/applications > /dev/null 2>&1 || true
|
||||
|
||||
# CW-1 cleanup on erase only (\$1 = 0; upgrades keep the symlink).
|
||||
# Remove the compat symlink only when it is ours: a symlink no rpm
|
||||
# package owns (distro compat links — e.g. Fedora's directory-level
|
||||
# /usr/share/OVMF — are package-owned and must survive), pointing at
|
||||
# one of the layouts %post bridges.
|
||||
if [ "\$1" -eq 0 ] && [ -L $fw_probe_dir/$fw_link_name ] \\
|
||||
&& ! rpm -qf $fw_probe_dir/$fw_link_name >/dev/null 2>&1; then
|
||||
case "\$(readlink $fw_probe_dir/$fw_link_name)" in
|
||||
/usr/share/edk2/*|/usr/share/qemu/*)
|
||||
rm -f $fw_probe_dir/$fw_link_name
|
||||
rmdir $fw_probe_dir 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
%files
|
||||
%defattr(-, root, root, 0755)
|
||||
%attr(755, root, root) /usr/bin/$package_name
|
||||
/usr/lib/$package_name
|
||||
/usr/share/applications/$package_name.desktop
|
||||
/usr/share/metainfo/$metainfo_name
|
||||
/usr/share/icons/hicolor/*/apps/${package_name}.png
|
||||
SPECEOF
|
||||
|
||||
echo 'RPM spec file created'
|
||||
|
||||
# --- Build RPM Package ---
|
||||
echo 'Building RPM package...'
|
||||
|
||||
rpmbuild_log="$work_dir/rpmbuild.log"
|
||||
rpmbuild --define "_topdir $rpmbuild_dir" \
|
||||
--define "_rpmdir $work_dir" \
|
||||
--target "$rpm_arch" \
|
||||
-bb "$rpmbuild_dir/SPECS/$package_name.spec" 2>&1 |
|
||||
tee "$rpmbuild_log"
|
||||
if (( PIPESTATUS[0] != 0 )); then
|
||||
echo 'Failed to build RPM package' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Guard against re-introducing #609. The "File listed twice" warning
|
||||
# means %files has overlapping listings, and on modern rpmbuild any
|
||||
# %exclude workaround silently strips the file from the payload.
|
||||
if grep -qF 'File listed twice' "$rpmbuild_log"; then
|
||||
echo 'rpmbuild emitted "File listed twice" — %files has overlapping listings (see #609)' >&2
|
||||
grep -F 'File listed twice' "$rpmbuild_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find and move the built RPM (it will be in a subdirectory)
|
||||
rpm_file=$(find "$work_dir" -name "${package_name}-${rpm_version}*.rpm" -type f | head -n 1)
|
||||
if [[ -z $rpm_file ]]; then
|
||||
echo 'Could not find built RPM file' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rename to consistent format at work_dir root
|
||||
# Use original $version to maintain filename compatibility with DEB and AppImage
|
||||
final_rpm="$work_dir/${package_name}-${version}-1.${rpm_arch}.rpm"
|
||||
if [[ $rpm_file != "$final_rpm" ]]; then
|
||||
mv "$rpm_file" "$final_rpm" || exit 1
|
||||
fi
|
||||
|
||||
echo "RPM package built successfully: $final_rpm"
|
||||
echo '--- RPM Package Build Finished ---'
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user