Skip to content

feat(linux): add an optional installer for desktop integration - #364

Merged
thcp merged 1 commit into
mainfrom
feat/linux-installer
Aug 12, 2026
Merged

feat(linux): add an optional installer for desktop integration#364
thcp merged 1 commit into
mainfrom
feat/linux-installer

Conversation

@thcp

@thcp thcp commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #361, and with #360 already merged this closes out #342.

Written in-house rather than adapting the fork. Portable behaviour is unchanged: extract the tarball, run ./StemDeck, and nothing here is required.

What it does

./install.sh                 install, or upgrade an existing install in place
./install.sh --local         ~/.local/opt/stemdeck
./install.sh --global        /opt/stemdeck (sudo)
./install.sh --prefix DIR    somewhere else
./install.sh --uninstall     remove what the manifest records
./install.sh --yes           never prompt

It installs the package it sits in and downloads nothing. Version and variant come from backend/static/version.json and the cpu-only marker, so the installer and the build cannot disagree, and there is no second artifact to checksum.

The decisions that matter

Install is atomic. The copy goes to <target>.new and is verified before the old install is moved aside. Removing the old copy first is what made a failed upgrade in the reference fork leave the machine with no StemDeck, no launcher, and no manifest recording where it had been. There is a test for exactly that.

A failed copy cleans up after itself. Under set -e a bare cp that dies partway aborts the script before any cleanup line, stranding a package-sized partial. Caught by the test suite, not by reading it.

Version comparison is semver-aware. sort -V ranks 0.8.0-alpha.17 above 0.8.0, so the day a stable release ships every pre-release user would be told they were current. Ten cases pin the ordering.

A missing manifest key reads as empty. grep | head | cut under set -euo pipefail exits 1 on a missing key, killing the script with no message and making its own corrupt-manifest handling unreachable.

Also: Exec is quoted so paths with spaces launch; global installs put the launcher and icon under /usr/share so other users see them; installing from inside the destination is refused rather than moving the running script; non-x86_64 gets a clear refusal.

User data

Never touched. Stems are in ~/Documents/StemDeck, runtime and models 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.

Verification

tests/linux/test_install_sh.sh runs the real installer against a synthetic package in a throwaway HOME. 52 checks, all passing:

area covers
install files, manifest, desktop entry, icon, variant detection, quoted Exec
upgrade in-place location reuse, content replaced, manifest updated
failed upgrade install, user files, launcher and manifest all survive; no staging left
uninstall removes only recorded files; stems and runtime untouched
manifest missing key survivable; corrupt manifest refused with a message
paths install to a directory containing a space
legacy data carried across upgrade, never deleted on uninstall
guards incomplete package, self-install, wrong architecture
semver 10 orderings including the sort -V failure case

Passing under bash 3.2 locally, which is a floor no Linux distro is below. CI adds a linux-installer job running it on ubuntu-latest with shellcheck -S warning (clean) and desktop-file-validate.

Full Python suite: 539 passed. tests/test_stems_api.py::test_all_stems_zip_ogg fails identically on clean main (local ffmpeg lacks libvorbis; the skip guard only checks that ffmpeg exists). Unrelated, untouched.

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
@thcp
thcp marked this pull request as ready for review August 12, 2026 18:08
@thcp
thcp merged commit 5565b21 into main Aug 12, 2026
9 checks passed
@thcp
thcp deleted the feat/linux-installer branch August 12, 2026 18:08
@thcp thcp mentioned this pull request Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux installer: install, upgrade and uninstall with desktop integration

1 participant