diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun index d3f6117e..36e0d211 100755 --- a/packaging/linux/AppRun +++ b/packaging/linux/AppRun @@ -8,7 +8,8 @@ # (and whenever the AppImage's path changes) install the .desktop entry and the # hicolor icons into $XDG_DATA_HOME, then launch. Every failure here must leave # the launch untouched. AppImage managers can opt out with DESKTOPINTEGRATION=1, -# and users with ARTCRAFT_LAUNCHER_NO_DESKTOP_INTEGRATION=1. +# and users with ARTCRAFT_LAUNCHER_NO_DESKTOP_INTEGRATION=1. Nothing is installed +# while another entry (appimaged, AppImageLauncher, Gear Lever) runs this AppImage. set -u @@ -38,10 +39,17 @@ integrate() { data="${XDG_DATA_HOME:-$home/.local/share}" src_desktop="$APPDIR/usr/share/applications/$DESK_ID.desktop" [ -f "$src_desktop" ] || return 0 + apps="$data/applications" + # A second entry would list the app twice; drop the one we wrote if it is unchanged. + if other_entry_runs "$apps" "$appimage"; then + if own_entry "$apps/$DESK_ID.desktop" && rm -f "$apps/$DESK_ID.desktop"; then + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1 + fi + return 0 + fi # Rewrite Exec to this AppImage (the binary is not on the host PATH) and drop # TryExec, which would hide the entry; everything else is shipped as packaged. - apps="$data/applications" mkdir -p "$apps" 2>/dev/null || return 0 tmp="$apps/.$DESK_ID.tmp.$$" esc="$(printf '%s' "$appimage" | sed 's/[\\&|]/\\&/g')" @@ -97,7 +105,36 @@ own_entry() { [ "$existing_exec" = "$generated_exec" ] || return 1 expected_body="$(sed -e '/^Exec=/d' -e '/^TryExec=/d' "$src_desktop")" || return 1 existing_body="$(sed -e '/^Exec=/d' -e '/^X-ArtCraft-Launcher-Generated-Exec=/d' "$1")" || return 1 - [ "$expected_body" = "$existing_body" ] + # Entries written before the StartupWMClass fix differ only in that line. + old_body="$(printf '%s\n' "$expected_body" | sed 's/^StartupWMClass=.*/StartupWMClass=artcraft-launcher/')" + [ "$expected_body" = "$existing_body" ] || [ "$old_body" = "$existing_body" ] +} + +# True when a visible entry in $1 other than ours runs the AppImage $2 and has our window class. +other_entry_runs() { + grep -lF -e "$2" "$1"/*.desktop 2>/dev/null | ( + while IFS= read -r f; do + [ "$f" = "$1/$DESK_ID.desktop" ] && continue + main=false runs=false class=false + while IFS= read -r line || [ -n "$line" ]; do + # Only the [Desktop Entry] group; an action's Exec isn't what the entry launches. + case "$line" in + "[Desktop Entry]") main=true && continue ;; + "["*) main=false ;; + esac + [ "$main" = true ] || continue + case "$line" in + # Menu editors hide an entry this way; it lists nothing. + Hidden=true | NoDisplay=true) runs=false && break ;; + # A class from before an in-place update can't match the window, so ours has to stay. + "StartupWMClass=$DESK_ID") class=true ;; + Exec=*"$2" | Exec=*"$2 "* | Exec=*"$2\""* | Exec=*"$2'"*) runs=true ;; + esac + done <"$f" + [ "$runs" = true ] && [ "$class" = true ] && exit 0 + done + exit 1 + ) } integrate diff --git a/packaging/linux/apprun-test.sh b/packaging/linux/apprun-test.sh index 28c9890f..9eb4837b 100755 --- a/packaging/linux/apprun-test.sh +++ b/packaging/linux/apprun-test.sh @@ -181,4 +181,64 @@ ln -s "$TMP/manager-entry" "$DESKTOP" "$APPDIR/AppRun" >/dev/null || fail "launch with desktop symlink failed" [[ -L "$DESKTOP" ]] || fail "desktop symlink replaced" +# 15. appimaged lists this AppImage under its own name: drop our entry, install nothing more. +rm -rf "$XDG_DATA_HOME" +"$APPDIR/AppRun" >/dev/null || fail "launch before appimaged failed" +apps="$XDG_DATA_HOME/applications" +real="$(readlink -f "$APPIMAGE")" +printf '[Desktop Entry]\nName=ArtCraft Launcher\nExec="/usr/bin/appimaged" wrap "%s" %%F\nStartupWMClass=%s\nActions=Trash;\n\n[Desktop Action Trash]\nName=Move to Trash\nExec=gio trash "%s"\n' "$real" "$APP_ID" "$real" >"$apps/appimagekit_0123abcd.desktop" +rm -rf "$XDG_DATA_HOME/icons" +out="$("$APPDIR/AppRun" managed)" || fail "launch beside appimaged failed" +[[ "$out" = "stub: managed" ]] || fail "launch beside appimaged lost arguments" +[[ ! -e "$DESKTOP" ]] || fail "own entry kept beside appimaged's" +[[ ! -e "$XDG_DATA_HOME/icons" ]] || fail "icons installed beside appimaged's entry" + +# 15a. Gear Lever's entry (single-quoted path, no trailing newline) counts too. +rm -rf "$XDG_DATA_HOME" && mkdir -p "$apps" +spaced="$TMP/Art Craft.AppImage" +: >"$spaced" +printf "[Desktop Entry]\nName=ArtCraft Launcher\nStartupWMClass=%s\nExec=env DESKTOPINTEGRATION=1 '%s'" "$APP_ID" "$(readlink -f "$spaced")" >"$apps/artcraft_launcher.desktop" +APPIMAGE="$spaced" "$APPDIR/AppRun" >/dev/null || fail "launch beside Gear Lever failed" +[[ ! -e "$DESKTOP" ]] || fail "integrated beside Gear Lever's entry" + +# 15b. Our entry with user changes stays beside a manager's entry. +cp "$apps/artcraft_launcher.desktop" "$TMP/gear-lever-entry" +sed "s|^Exec=.*|Exec=\"$(readlink -f "$APPIMAGE")\"|" "$TMP/gear-lever-entry" >"$apps/artcraft_launcher.desktop" +sed 's/^Name=.*/Name=My Launcher/' "$TMP/generated-entry" >"$DESKTOP" +cp "$DESKTOP" "$TMP/renamed-entry" +"$APPDIR/AppRun" >/dev/null || fail "launch with renamed entry failed" +cmp -s "$DESKTOP" "$TMP/renamed-entry" || fail "user-changed entry removed" + +# 15c. An entry for another file that shares our path as a prefix doesn't count. +rm -rf "$XDG_DATA_HOME" && mkdir -p "$apps" +printf '[Desktop Entry]\nStartupWMClass=%s\nExec="%s.old"\n' "$APP_ID" "$(readlink -f "$APPIMAGE")" >"$apps/old-copy.desktop" +"$APPDIR/AppRun" >/dev/null || fail "launch beside an older copy's entry failed" +[[ -f "$DESKTOP" ]] || fail "an entry for another file blocked integration" + +# 15d. An entry from a release with the old StartupWMClass is ours: refreshed, and dropped beside appimaged's. +sed 's/^StartupWMClass=.*/StartupWMClass=artcraft-launcher/' "$TMP/generated-entry" >"$DESKTOP" +"$APPDIR/AppRun" >/dev/null || fail "launch over an older release's entry failed" +cmp -s "$DESKTOP" "$TMP/generated-entry" || fail "older release's entry not refreshed" +sed 's/^StartupWMClass=.*/StartupWMClass=artcraft-launcher/' "$TMP/generated-entry" >"$DESKTOP" +printf '[Desktop Entry]\nExec="/usr/bin/appimaged" wrap "%s" %%F\nStartupWMClass=%s\n' "$real" "$APP_ID" >"$apps/appimagekit_0123abcd.desktop" +"$APPDIR/AppRun" >/dev/null || fail "launch over an older release's entry beside appimaged failed" +[[ ! -e "$DESKTOP" ]] || fail "older release's entry kept beside appimaged's" + +# 15e. A manager's entry hidden with a menu editor lists nothing: ours is installed and kept. +for key in NoDisplay Hidden; do + printf '[Desktop Entry]\nExec="/usr/bin/appimaged" wrap "%s" %%F\nStartupWMClass=%s\n%s=true\n' "$real" "$APP_ID" "$key" >"$apps/appimagekit_0123abcd.desktop" + "$APPDIR/AppRun" >/dev/null || fail "launch beside a hidden entry failed" + [[ -f "$DESKTOP" ]] || fail "no entry of ours beside a $key=true manager entry" +done + +# 15f. A manager's entry left with the old window class by an in-place update: ours stays. +printf '[Desktop Entry]\nExec="/usr/bin/appimaged" wrap "%s" %%F\nStartupWMClass=artcraft-launcher\n' "$real" >"$apps/appimagekit_0123abcd.desktop" +"$APPDIR/AppRun" >/dev/null || fail "launch beside an old-class manager entry failed" +[[ -f "$DESKTOP" ]] || fail "own entry dropped beside a manager entry with the old class" + +# 15g. Only the [Desktop Entry] group counts: an action that runs this AppImage doesn't. +printf '[Desktop Entry]\nExec=other-app\nStartupWMClass=%s\nActions=Launcher;\n\n[Desktop Action Launcher]\nExec="%s"\n' "$APP_ID" "$real" >"$apps/appimagekit_0123abcd.desktop" +"$APPDIR/AppRun" >/dev/null || fail "launch beside an entry that runs this AppImage from an action failed" +[[ -f "$DESKTOP" ]] || fail "own entry dropped beside an entry that runs this AppImage only from an action" + echo "AppRun desktop integration ok"