5565b216ba
Implements #342. StemDeck stays portable: extract the tarball, run ./StemDeck, and none of this is required. install.sh is there for people who would rather launch from their applications menu. It installs the package it sits in and never downloads anything, so the version and the CPU/NVIDIA variant come from the package itself (backend/static/version.json and the cpu-only marker) and cannot drift from the build being installed. That also removes any need to verify a second download. Design notes, mostly things the reference installer in #342 got wrong: - Install is atomic. The new copy goes to <target>.new and is verified before the old one is moved aside, so a failure partway leaves the working install untouched. Removing the old copy first is what made a failed upgrade in that fork leave the machine with no StemDeck, no launcher and no manifest recording where it had been. - A failed copy cleans up its own staging directory rather than leaving a package-sized partial on disk. - Exec is quoted, so an install path containing a space still launches. - Version comparison is semver-aware. sort -V ranks 0.8.0-alpha.17 above 0.8.0, which would tell every pre-release user they were current the day a stable release shipped. - Reading a missing manifest key yields empty rather than killing the script, which under set -euo pipefail is what a grep|head|cut pipeline does. - Global installs put the launcher in /usr/share/applications and the icon in /usr/share/pixmaps, so other users on the machine can see it. - Installing from inside the destination is refused rather than moving the running script out from under bash. - Non-x86_64 machines get a clear refusal instead of a binary that cannot run. User data is never touched. Stems live in ~/Documents/StemDeck and the runtime, models and logs in $XDG_DATA_HOME/stemdeck, both outside the install directory. Legacy data/ from pre-migration builds is carried across an upgrade, and uninstall refuses to delete it, leaving the folder and saying why. tests/linux/test_install_sh.sh runs the real installer against a synthetic package in a throwaway HOME: 52 checks covering install, upgrade, the failed-upgrade case, uninstall, corrupt manifests, paths with spaces, legacy data, self-install, arch refusal and the semver table. CI runs it on Linux with shellcheck and desktop-file-validate. Closes #361
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""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_the_desktop_assets():
|
|
script = MAKE_PORTABLE.read_text(encoding="utf-8")
|
|
assert "packaging/stemdeck.png" in script
|
|
assert "packaging/stemdeck.desktop.in" in script
|
|
|
|
|
|
def test_make_portable_stages_the_installer():
|
|
"""Without this the installer is not in the tarball, and the README tells
|
|
users to run a file that is not there."""
|
|
script = MAKE_PORTABLE.read_text(encoding="utf-8")
|
|
assert "packaging/linux/install.sh" in script
|
|
assert 'chmod +x "$STAGE/install.sh"' in script
|
|
|
|
|
|
def test_the_installer_exists_and_is_executable():
|
|
installer = ROOT / "packaging" / "linux" / "install.sh"
|
|
assert installer.is_file()
|
|
assert installer.stat().st_mode & 0o111, "install.sh must be executable in the repo"
|
|
|
|
|
|
def test_readme_documents_the_installer():
|
|
readme = (ROOT / "packaging" / "linux" / "README-LINUX.txt").read_text(encoding="utf-8")
|
|
assert "./install.sh" in readme
|
|
assert "--uninstall" in readme
|