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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:21 +08:00
commit 3e076d4dd9
341 changed files with 80308 additions and 0 deletions
+379
View File
@@ -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
+437
View File
@@ -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>
+376
View File
@@ -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