fix(appimage): stop shipping a dangling .DirIcon, and prove it in CI (#1518)
* 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) <noreply@anthropic.com>
* docs(changelog): stamp the AppImage icon entries with their PR ref
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8fdc5292e4
commit
4fc07c21fd
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user