From eab268d1b721013940c0907f6f23f78ffe2c2aab Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:53:51 +0100 Subject: [PATCH] build(linux): stage desktop-entry assets into the portable tarball Prep for the optional Linux installer (#342). Carrying the icon and a .desktop template inside the package is what lets the installer be self-contained: no second download, and no asset URL that can drift from the release being installed. The Tauri icon is already square at 1024x1024, so it doubles as the desktop icon with no separate artwork to keep in sync. Exec= is quoted in the template. The freedesktop spec splits Exec on whitespace, so the unquoted form used by the reference installer in #342 produces an entry that tries to run ".../My" when the user installs to a path like ~/My Apps. It is invisible until someone picks a custom directory, which is why it is pinned by a test. Both variants pick this up: the CPU and NVIDIA packages run the same script with CPU_ONLY toggled. install.sh itself, and the README-LINUX.txt change documenting it, land with #361 -- this commit deliberately ships nothing that references a file which does not exist yet. Closes #360 --- packaging/linux/stemdeck.desktop.in | 13 ++++++ scripts/linux/make-portable.sh | 9 ++++ tests/test_packaging_linux.py | 71 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 packaging/linux/stemdeck.desktop.in create mode 100644 tests/test_packaging_linux.py diff --git a/packaging/linux/stemdeck.desktop.in b/packaging/linux/stemdeck.desktop.in new file mode 100644 index 00000000..4aa9fe78 --- /dev/null +++ b/packaging/linux/stemdeck.desktop.in @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=StemDeck +GenericName=Stem Separation +Comment=Free, local stem separation for music production and practice +Exec="@EXEC@" +Icon=@ICON@ +Terminal=false +StartupNotify=true +StartupWMClass=StemDeck +Categories=AudioVideo;Audio;Music; +Keywords=stems;separation;audio;vocals;drums;bass;karaoke;demucs; diff --git a/scripts/linux/make-portable.sh b/scripts/linux/make-portable.sh index 8574e8fa..45d70bc7 100755 --- a/scripts/linux/make-portable.sh +++ b/scripts/linux/make-portable.sh @@ -148,6 +148,15 @@ printf '{ "version": "%s" }\n' "$RESOLVED_VERSION" > "$BACKEND_DIR/static/versio cp "$REPO_ROOT/packaging/linux/README-LINUX.txt" "$STAGE/README-LINUX.txt" cp "$REPO_ROOT/packaging/linux/THIRD_PARTY_NOTICES.txt" "$STAGE/THIRD_PARTY_NOTICES.txt" +# Desktop-entry assets for the optional installer (#360). Carried inside the +# package so the installer needs no second download and no asset URL that could +# drift from the release it is installing. The Tauri icon is already square, so +# it doubles as the desktop icon with no separate artwork to keep in sync. +echo "==> Staging desktop-entry assets" +mkdir -p "$STAGE/packaging" +cp "$REPO_ROOT/desktop/src-tauri/icons/icon.png" "$STAGE/packaging/stemdeck.png" +cp "$REPO_ROOT/packaging/linux/stemdeck.desktop.in" "$STAGE/packaging/stemdeck.desktop.in" + # CPU-only marker: read by is_cpu_only_package so the shell skips GPU detection. # Omitted for the NVIDIA variant so the shell detects the GPU and uses CUDA. if [[ "$CPU_ONLY" == "1" ]]; then diff --git a/tests/test_packaging_linux.py b/tests/test_packaging_linux.py new file mode 100644 index 00000000..57b79d77 --- /dev/null +++ b/tests/test_packaging_linux.py @@ -0,0 +1,71 @@ +"""The Linux desktop-entry assets staged into the portable tarball (#360). + +These are plain data files with no code path to exercise them until the +installer lands (#361), so the things worth pinning are the ones that fail +silently at the user's end: a launcher that will not start, or an icon the +packaging script cannot find. +""" + +from __future__ import annotations + +import shlex +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "packaging" / "linux" / "stemdeck.desktop.in" +ICON = ROOT / "desktop" / "src-tauri" / "icons" / "icon.png" +MAKE_PORTABLE = ROOT / "scripts" / "linux" / "make-portable.sh" + + +def _entries() -> dict[str, str]: + lines = TEMPLATE.read_text(encoding="utf-8").splitlines() + assert lines[0] == "[Desktop Entry]", "the group header must come first" + return dict(line.split("=", 1) for line in lines[1:] if line and not line.startswith("#")) + + +def test_exec_is_quoted_so_a_path_with_spaces_still_launches(): + """The freedesktop spec splits Exec on whitespace. + + The reference installer in #342 emitted an unquoted Exec, so installing to + a directory such as ~/My Apps produced an entry that tried to run a binary + called ".../My". Quoting is the whole fix, and it is invisible until + someone picks a custom path. + """ + exec_line = _entries()["Exec"].replace("@EXEC@", "/home/u/My Apps/StemDeck-Linux-x64/StemDeck") + assert shlex.split(exec_line) == ["/home/u/My Apps/StemDeck-Linux-x64/StemDeck"] + + +def test_placeholders_are_present_for_the_installer_to_substitute(): + entries = _entries() + assert "@EXEC@" in entries["Exec"] + assert entries["Icon"] == "@ICON@" + + +@pytest.mark.parametrize("key", ["Type", "Name", "Exec", "Icon", "Categories"]) +def test_required_keys_are_present(key): + assert key in _entries() + + +def test_type_and_categories_are_registered_values(): + entries = _entries() + assert entries["Type"] == "Application" + cats = [c for c in entries["Categories"].split(";") if c] + # A registered main category is required; Audio and Music are additional + # ones that only carry meaning alongside it. + assert "AudioVideo" in cats + assert "Multimedia" not in cats, "Multimedia is not a registered category" + assert entries["Categories"].endswith(";"), "the list must be semicolon-terminated" + + +def test_the_icon_the_packaging_script_copies_exists(): + """make-portable.sh copies this by path. A move would break the Linux build + at package time, long after the change that caused it.""" + assert ICON.is_file() + + +def test_make_portable_stages_both_assets(): + script = MAKE_PORTABLE.read_text(encoding="utf-8") + assert "packaging/stemdeck.png" in script + assert "packaging/stemdeck.desktop.in" in script