Files
amethyst/scripts/fix-deb-desktop-entry.sh
T
Claude db8ea85b5d fix(desktop): ship the .deb menu entry as a package file
jpackage's postinst registers the menu entry with `xdg-desktop-menu
install` under `set -e`. Without /usr/share/desktop-directories (minimal
and non-GNOME installs without gnome-menus) that exits 3, the package is
left half-configured, and apt, gdebi and the App Center report a failed
install. Its prerm fails the same way, so the package can't be removed
either.

add-deb-wm-class.sh becomes fix-deb-desktop-entry.sh. Besides adding
StartupWMClass it now ships the entry in /usr/share/applications under
the name xdg-desktop-menu gave its copy (pinned launchers keep working,
an upgrade replaces that copy) and drops the xdg-desktop-menu calls from
postinst and prerm. It fails the build if jpackage's scripts gain other
xdg-utils calls.

It is also where the .deb's compression is now decided: it always writes
xz, rebuilding even when nothing else needs changing. Before, the .deb
came out xz only because this step happened to repack last after
jpackage's zstd; the control-only steps copy the payload through, so the
steps now give the same .deb in any order.

The smoke test runs the step and installs strictly: it used to tolerate
a failed install (`|| true`) because of this postinst on CI runners, so
it now also guards against the failure coming back.

Checked on the 1.18.0 x64 payload, on a system without the menu
directories: the released .deb ends half-configured, the new one installs
and purges cleanly. Upgrading from 1.18.0 works both from a working
install (its copy is replaced by the owned file, one entry left) and from
one stuck half-configured (dpkg's failed-upgrade path). The entry passes
desktop-file-validate, a second run is a no-op, and running the step
first or last gives the same .deb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GtLex2qj7QL6sJX3BCswqh
2026-10-12 14:32:27 +00:00

126 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install the desktop app's menu entry the Debian way in a jpackage-built .deb,
# and give the package its final compression.
#
# 1. StartupWMClass. Linux docks and app switchers (GNOME, KDE, ...) find which
# app a window belongs to by matching its WM_CLASS against the
# StartupWMClass of the app's desktop entry. The app names its windows
# "Amethyst" (AmethystDesktop.kt), and the AppImage and Flatpak entries
# declare it, but jpackage's own desktop entry has no StartupWMClass. With
# the .deb, the switcher showed a generic icon and no app name, and the
# running app did not group with its launcher.
#
# 2. The entry as a package file. jpackage leaves the entry under
# /opt/<package>/lib and registers it from postinst with
# `xdg-desktop-menu install`, under `set -e`. xdg-desktop-menu exits 3 ("No
# writable system menu directory found") when /usr/share/desktop-directories
# is missing, as on minimal and non-GNOME installs without gnome-menus, so the
# package was left half-configured and apt, gdebi and the App Center all
# reported the install as failed. The entry is now a file the package owns in
# /usr/share/applications, under the name xdg-desktop-menu gave its copy (so
# pinned launchers keep working and an upgrade replaces that copy), and
# postinst/prerm no longer call xdg-desktop-menu. desktop-file-utils' dpkg
# trigger refreshes the menu cache. An upgrade from an older .deb still runs
# that version's prerm, which removes the copy it registered; if that fails,
# jpackage's prerm answers dpkg's `failed-upgrade` call with success and the
# upgrade goes on.
#
# 3. Compression. This is the one step that rewrites the payload, so it decides
# how the shipped .deb is compressed: always xz, whatever jpackage's
# dpkg-deb defaulted to (zstd on Ubuntu) and whatever ran before. The steps
# that only edit the control file (scripts/deb-control.sh) copy the payload
# through untouched, so the order the steps run in doesn't matter.
#
# Neither jpackage nor the Compose Multiplatform DSL can change any of this
# (Compose empties jpackage's --resource-dir right before it runs), so we
# rewrite the .deb after the fact. The .rpm gets StartupWMClass from
# desktopApp/build.gradle.kts; rpm treats a failed %post as a warning, so the
# .rpm keeps jpackage's registration.
#
# Usage: fix-deb-desktop-entry.sh <path-to-deb> [<path-to-deb> ...]
set -euo pipefail
WM_CLASS='Amethyst'
# xz: smaller downloads than zstd, and readable by every dpkg in a supported Debian or Ubuntu.
COMPRESSION='xz'
# An unmatched glob reaches us as the literal pattern: fail rather than ship an unchanged .deb.
if [[ $# -eq 0 ]]; then
echo "error: no .deb given" >&2
exit 1
fi
for deb in "$@"; do
if [[ ! -f "$deb" ]]; then
echo "error: not a file: $deb" >&2
exit 1
fi
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
dpkg-deb -R "$deb" "$work/pkg"
changes=()
# jpackage installs its entry under /opt/<package>/lib. The bundled runtime has
# directories named like java.desktop under legal/: skip it.
mapfile -t entries < <(find "$work/pkg/opt" -path "*/lib/runtime" -prune -o -type f -name "*.desktop" -print)
if [[ ${#entries[@]} -eq 0 ]]; then
echo "error: no desktop entry under /opt in $deb" >&2
exit 1
fi
for entry in "${entries[@]}"; do
if ! grep -q '^StartupWMClass=' "$entry"; then
echo "StartupWMClass=${WM_CLASS}" >> "$entry"
changes+=("StartupWMClass=${WM_CLASS}")
fi
shipped="$work/pkg/usr/share/applications/$(basename "$entry")"
if ! cmp -s "$entry" "$shipped"; then
install -D -m 644 "$entry" "$shipped"
changes+=("/usr/share/applications/$(basename "$entry")")
fi
done
for script in postinst prerm; do
file="$work/pkg/DEBIAN/$script"
if [[ -f "$file" ]] && grep -qE '^[[:space:]]*xdg-desktop-menu[[:space:]]' "$file"; then
sed -i -E '/^[[:space:]]*xdg-desktop-menu[[:space:]]/d' "$file"
changes+=("no xdg-desktop-menu in $script")
fi
done
# jpackage also registers icons and MIME types through xdg-utils when an app
# declares file associations. This app declares none; if that changes, those
# calls can fail the same way and need the same treatment.
if grep -lE 'xdg-(desktop-menu|icon-resource|mime)' "$work/pkg/DEBIAN/"*; then
echo "error: maintainer scripts above still call xdg-utils; extend $0 to cover them" >&2
exit 1
fi
if [[ ${#changes[@]} -gt 0 && -f "$work/pkg/DEBIAN/md5sums" ]]; then
# Keep dpkg --verify quiet when the package lists its files' checksums.
(cd "$work/pkg" && find . -path ./DEBIAN -prune -o -type f -printf '%P\0' | sort -z | xargs -0 md5sum) > "$work/md5sums"
mv -f "$work/md5sums" "$work/pkg/DEBIAN/md5sums"
fi
data="$(ar t "$deb" | grep '^data\.tar' || true)"
if [[ ${#changes[@]} -eq 0 && $data == "data.tar.${COMPRESSION}" ]]; then
echo "Desktop entry already installed, leaving as-is: $deb"
rm -rf "$work"
trap - EXIT
continue
fi
if [[ $data != "data.tar.${COMPRESSION}" ]]; then
changes+=("${data:-payload} -> data.tar.${COMPRESSION}")
fi
# Built beside it and moved over it, so an interrupted run leaves the original intact.
dpkg-deb --root-owner-group "-Z${COMPRESSION}" -b "$work/pkg" "$work/out.deb" >/dev/null
mv -f "$work/out.deb" "$deb"
echo "Fixed desktop entry: $deb"
printf ' %s\n' "${changes[@]}"
rm -rf "$work"
trap - EXIT
done