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"