Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packaging/linux/stemdeck.desktop.in
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 9 additions & 0 deletions scripts/linux/make-portable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions tests/test_packaging_linux.py
Original file line number Diff line number Diff line change
@@ -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