3e076d4dd9
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
438 lines
18 KiB
Bash
Executable File
438 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Arguments passed from the main script
|
|
version="$1"
|
|
architecture="$2"
|
|
work_dir="$3" # The top-level build directory (e.g., ./build)
|
|
app_staging_dir="$4" # Directory containing the prepared app files
|
|
package_name="$5"
|
|
maintainer="$6"
|
|
description="$7"
|
|
|
|
echo '--- Starting Debian Package Build ---'
|
|
echo "Version: $version"
|
|
echo "Architecture: $architecture"
|
|
echo "Work Directory: $work_dir"
|
|
echo "App Staging Directory: $app_staging_dir"
|
|
echo "Package Name: $package_name"
|
|
|
|
package_root="$work_dir/package"
|
|
install_dir="$package_root/usr"
|
|
|
|
# Clean previous package structure if it exists
|
|
rm -rf "$package_root"
|
|
|
|
# Create Debian package structure
|
|
echo "Creating package structure in $package_root..."
|
|
mkdir -p "$package_root/DEBIAN" || exit 1
|
|
mkdir -p "$install_dir/lib/$package_name" || exit 1
|
|
mkdir -p "$install_dir/share/applications" || exit 1
|
|
mkdir -p "$install_dir/share/icons" || exit 1
|
|
mkdir -p "$install_dir/bin" || exit 1
|
|
|
|
# --- Icon Installation ---
|
|
echo 'Installing icons...'
|
|
# 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
|