Files
VoiceStudio/scripts/verify-apprun-bundle.test.sh
Palash DebnathandClaude Opus 5 4fc07c21fd 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>
2026-08-12 22:08:26 +00:00

95 lines
3.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERIFY="$REPO_ROOT/scripts/verify-apprun-bundle.sh"
EXPECTED="$REPO_ROOT/frontend/src-tauri/appimage/AppRun"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
make_root() {
local root="$1"
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"
make_root "$TMP/direct"
install -m 755 "$EXPECTED" "$TMP/direct/AppRun"
bash "$VERIFY" "$TMP/direct" "$EXPECTED" "$TMP/expected-marker"
make_root "$TMP/wrapped"
install -m 755 "$EXPECTED" "$TMP/wrapped/AppRun.wrapped"
cat > "$TMP/wrapped/AppRun" <<'WRAPPER'
#!/usr/bin/env bash
this_dir="$(dirname "$(readlink -f "${0}")")"
exec "$this_dir"/AppRun.wrapped "$@"
WRAPPER
chmod 755 "$TMP/wrapped/AppRun"
bash "$VERIFY" "$TMP/wrapped" "$EXPECTED" "$TMP/expected-marker"
make_root "$TMP/broken"
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$TMP/broken/AppRun"
chmod 755 "$TMP/broken/AppRun"
if bash "$VERIFY" "$TMP/broken" "$EXPECTED" "$TMP/expected-marker" >/dev/null 2>&1; then
echo "FAIL: stock launcher was accepted without the custom launcher" >&2
exit 1
fi
# ── 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"