From 4fc07c21fd60744a4875b3d2b8bbf56c6571e47a Mon Sep 17 00:00:00 2001 From: Palash Debnath <4178343+debpalash@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:08:26 +0000 Subject: [PATCH] fix(appimage): stop shipping a dangling .DirIcon, and prove it in CI (#1518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(appimage): stop shipping a dangling .DirIcon, and prove it in CI The Linux icon is blank because the AppImage's .DirIcon is an absolute symlink into the machine that built it. From the published v0.4.2: .DirIcon -> /home/runner/work/OmniVoice-Studio/OmniVoice-Studio/frontend/ src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/ appimage/OmniVoice Studio.AppDir/OmniVoice Studio.png That path exists on nobody's computer. The link dangles the moment the AppImage leaves CI, so file managers have no icon for the file, and the integration tools that read .DirIcon install nothing. A dangling symlink is not a build error — the bundle packs, runs, and passes every check we had — which is how it shipped for a whole release without anyone noticing. Locally built AppDirs are worse: both .DirIcon AND the root .desktop symlink come out absolute, so a from-source bundle has no readable desktop entry either, which is why the icon is missing in the menu and the dock too. - `.DirIcon` is now a real file, copied in through `appimage.files` — the same seam that already places the WebKitGTK marker. - `bundle.category` is set, so the generated desktop entry stops emitting an empty `Categories=`. That is not the same as omitting the key: desktop-file-validate rejects the entry and menu builders skip it. - verify-apprun-bundle.sh — already run against the extracted AppImage in the release job — now fails when .DirIcon is missing or resolves outside the bundle, when the .desktop entry does not resolve inside it, when Icon= names a file that is not at the AppImage root, or when Categories= is present but empty. Its unit test covers each of those, including the exact shape v0.4.2 shipped. The `.DirIcon` copy cannot be verified without a full release build, so the guard is the load-bearing part: the next release either passes it or fails loudly. It can no longer ship blank in silence. Co-Authored-By: Claude Opus 5 (1M context) * docs(changelog): stamp the AppImage icon entries with their PR ref Co-Authored-By: Claude Opus 5 (1M context) * docs(changelog): fold the AppImage icon fix into the existing Fixed section CodeRabbit (#1518): the Unreleased block must carry one `### Fixed` section of one-line entries. Merge the two entries in and drop the narrative and the version reference. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 + frontend/src-tauri/tauri.conf.json | 4 +- scripts/verify-apprun-bundle.sh | 51 +++++++++++++++++++++++- scripts/verify-apprun-bundle.test.sh | 58 +++++++++++++++++++++++++++- 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa0adbd..5a1e8246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ the frozen-backend fallback mirror it for their toolchains. ### Fixed +- The Linux app icon is no longer blank: the AppImage shipped `.DirIcon` as a symlink into the machine that built it, so file managers and app menus drew nothing. (#1518) +- The Linux desktop entry no longer ships an empty `Categories=`, which `desktop-file-validate` rejects and menu builders skip. (#1518) - The Simplified Chinese (zh-CN) translation no longer mistranslates brand names and technical terms — Discord, Tailscale, Hugging Face, IPA, and LLM (Cinematic) were rendered as nonsensical literal translations, and ~250 more awkward machine-translation strings are now natural Chinese. (#1508) — thanks @anyingiit! - Worker restart coverage now waits for the registration response to persist its identity instead of racing the client callback in CI. (#1505) - Dub language and export selections now restore without false schema warnings, and remote-worker port 7443 is identified instead of reported as a generic timeout. (#1504) diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index be50e40b..263a0bb6 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -72,6 +72,7 @@ "icons/icon.icns", "icons/icon.ico" ], + "category": "Music", "resources": [ "../../pyproject.toml", "../../uv.lock", @@ -88,7 +89,8 @@ "linux": { "appimage": { "files": { - "usr/lib/.bundled-webkitgtk-version": "target/.tauri/bundled-webkitgtk-version" + "usr/lib/.bundled-webkitgtk-version": "target/.tauri/bundled-webkitgtk-version", + ".DirIcon": "icons/128x128.png" } } }, diff --git a/scripts/verify-apprun-bundle.sh b/scripts/verify-apprun-bundle.sh index ef9d36e4..fe53de2f 100644 --- a/scripts/verify-apprun-bundle.sh +++ b/scripts/verify-apprun-bundle.sh @@ -27,4 +27,53 @@ fi cmp -s "$ROOT/usr/lib/.bundled-webkitgtk-version" "$EXPECTED_MARKER" \ || fail "bundled WebKitGTK version marker is stale or mismatched" -echo "OK — AppImage uses the custom launcher and current WebKitGTK marker" +# ── Icon integrity ──────────────────────────────────────────────────────── +# v0.4.2 shipped with .DirIcon as an ABSOLUTE symlink into the build machine: +# +# .DirIcon -> /home/runner/work/OmniVoice-Studio/…/OmniVoice Studio.AppDir/… +# +# That path exists on nobody's computer, so the link dangled the moment the +# AppImage left CI, and every tool that reads .DirIcon — file managers for the +# file's own icon, AppImageLauncher and appimaged for desktop integration — got +# nothing and drew a blank. A dangling symlink is not a build error, the bundle +# packs and runs fine, so nothing noticed for an entire release. +# +# `readlink -f` resolves the whole chain; the result has to stay inside the +# AppDir, because anything outside it does not travel with the bundle. +resolved_inside() { + local target + target="$(readlink -f -- "$1" 2>/dev/null)" || return 1 + [ -f "$target" ] || return 1 + case "$target" in + "$(readlink -f -- "$ROOT")"/*) return 0 ;; + *) return 1 ;; + esac +} + +[ -e "$ROOT/.DirIcon" ] || fail ".DirIcon is missing — the AppImage will show no icon" +resolved_inside "$ROOT/.DirIcon" \ + || fail ".DirIcon does not resolve to a file inside the bundle (dangling or absolute symlink): $(readlink -- "$ROOT/.DirIcon" 2>/dev/null)" + +# The desktop entry is what puts the app in the menu, and the icon it names has +# to be reachable — as a root-level file next to the entry, which is where +# desktop integration looks. +DESKTOP="$(find "$ROOT" -maxdepth 1 -name '*.desktop' | head -1)" +[ -n "$DESKTOP" ] || fail "no .desktop entry at the AppImage root" +resolved_inside "$DESKTOP" || fail ".desktop entry does not resolve inside the bundle" + +ICON_NAME="$(sed -n 's/^Icon=//p' "$DESKTOP" | head -1)" +[ -n "$ICON_NAME" ] || fail "the .desktop entry names no Icon" +case "$ICON_NAME" in + /*) fail "Icon= must be a theme name, not an absolute path: $ICON_NAME" ;; +esac +resolved_inside "$ROOT/$ICON_NAME.png" \ + || fail "the .desktop names Icon=$ICON_NAME but $ICON_NAME.png is not at the AppImage root" + +# An empty Categories= is not the same as omitting it: desktop-file-validate +# rejects the entry outright, and menu builders that honour it skip the app. +if grep -q '^Categories=' "$DESKTOP"; then + grep -q '^Categories=..*' "$DESKTOP" \ + || fail "Categories= is present but empty — set bundle.category in tauri.conf.json" +fi + +echo "OK — AppImage uses the custom launcher, current WebKitGTK marker, and a resolvable icon" diff --git a/scripts/verify-apprun-bundle.test.sh b/scripts/verify-apprun-bundle.test.sh index 36409518..b2dbf8ee 100644 --- a/scripts/verify-apprun-bundle.test.sh +++ b/scripts/verify-apprun-bundle.test.sh @@ -9,8 +9,21 @@ trap 'rm -rf "$TMP"' EXIT make_root() { local root="$1" - mkdir -p "$root/usr/lib" + mkdir -p "$root/usr/lib" "$root/usr/share/icons/hicolor/128x128/apps" printf '%s\n' '2.52.3' > "$root/usr/lib/.bundled-webkitgtk-version" + # A healthy icon set: a real root-level PNG named after Icon=, a .DirIcon + # that resolves to it, and a categorised desktop entry. + printf 'PNG' > "$root/usr/share/icons/hicolor/128x128/apps/omnivoice-studio.png" + cp "$root/usr/share/icons/hicolor/128x128/apps/omnivoice-studio.png" "$root/omnivoice-studio.png" + ln -sf omnivoice-studio.png "$root/.DirIcon" + cat > "$root/VoiceStudio.desktop" <<'DESKTOP' +[Desktop Entry] +Type=Application +Name=VoiceStudio +Exec=omnivoice-studio +Icon=omnivoice-studio +Categories=AudioVideo;Audio; +DESKTOP } printf '%s\n' '2.52.3' > "$TMP/expected-marker" @@ -37,4 +50,45 @@ if bash "$VERIFY" "$TMP/broken" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2> exit 1 fi -echo "PASS: direct, wrapped, and broken AppImage launcher chains classified" +# ── Icon integrity ──────────────────────────────────────────────────────── +# The exact shape that shipped in v0.4.2: .DirIcon pointing at the build +# machine's path, which dangles everywhere else. +make_root "$TMP/absicon" +install -m 755 "$EXPECTED" "$TMP/absicon/AppRun" +ln -sf "$TMP/absicon/omnivoice-studio.png" "$TMP/absicon/.DirIcon" +rm -f "$TMP/absicon/omnivoice-studio.png" +if bash "$VERIFY" "$TMP/absicon" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2>&1; then + echo "FAIL: a dangling .DirIcon was accepted" >&2 + exit 1 +fi + +make_root "$TMP/noicon" +install -m 755 "$EXPECTED" "$TMP/noicon/AppRun" +rm -f "$TMP/noicon/.DirIcon" +if bash "$VERIFY" "$TMP/noicon" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2>&1; then + echo "FAIL: a missing .DirIcon was accepted" >&2 + exit 1 +fi + +# Icon= naming a file that is not at the AppImage root: integration finds +# nothing to install and the menu entry draws blank. +make_root "$TMP/iconmismatch" +install -m 755 "$EXPECTED" "$TMP/iconmismatch/AppRun" +rm -f "$TMP/iconmismatch/omnivoice-studio.png" +printf 'PNG' > "$TMP/iconmismatch/SomethingElse.png" +ln -sf SomethingElse.png "$TMP/iconmismatch/.DirIcon" +if bash "$VERIFY" "$TMP/iconmismatch" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2>&1; then + echo "FAIL: Icon= with no matching root PNG was accepted" >&2 + exit 1 +fi + +# The empty Categories= tauri emits when bundle.category is unset. +make_root "$TMP/nocategory" +install -m 755 "$EXPECTED" "$TMP/nocategory/AppRun" +sed -i 's/^Categories=.*/Categories=/' "$TMP/nocategory/VoiceStudio.desktop" +if bash "$VERIFY" "$TMP/nocategory" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2>&1; then + echo "FAIL: an empty Categories= was accepted" >&2 + exit 1 +fi + +echo "PASS: launcher chains classified; dangling, missing, mismatched icons and empty Categories rejected"