From 1607940f552048115570b5ca1392861009e50c79 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 24 Jun 2026 11:39:52 +0100 Subject: [PATCH 1/5] feat: add CPU-only Linux portable build and release workflow Adds a Linux .tar.gz portable package mirroring the existing Windows/macOS build paths. Bundles a python-build-standalone runtime (CPU torch + demucs) plus the Tauri binary so users extract and run ./StemDeck. - scripts/linux/make-portable.sh: stages PBS Python, force-installs CPU-only torch, builds the Tauri binary, and produces StemDeck-Linux-x64.tar.gz with the backend/app + python/ layout find_repo_root resolves at runtime. - .github/workflows/linux-release.yml: builds on hosted ubuntu-latest on release publish; installs Tauri v2 apt deps + uv, ClamAV-scans, uploads. - packaging/linux/{README-LINUX,THIRD_PARTY_NOTICES}.txt: extract-and-run instructions noting ffmpeg + WebKitGTK are system (apt) prerequisites. FFmpeg is not bundled: the Linux shell expects ffmpeg on PATH. NVIDIA/CUDA and AppImage variants are intentionally deferred to later phases. --- .github/workflows/linux-release.yml | 92 ++++++++++++++ packaging/linux/README-LINUX.txt | 53 ++++++++ packaging/linux/THIRD_PARTY_NOTICES.txt | 77 ++++++++++++ scripts/linux/make-portable.sh | 159 ++++++++++++++++++++++++ 4 files changed, 381 insertions(+) create mode 100644 .github/workflows/linux-release.yml create mode 100644 packaging/linux/README-LINUX.txt create mode 100644 packaging/linux/THIRD_PARTY_NOTICES.txt create mode 100755 scripts/linux/make-portable.sh diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml new file mode 100644 index 00000000..fdb138b7 --- /dev/null +++ b/.github/workflows/linux-release.yml @@ -0,0 +1,92 @@ +name: Linux Release + +on: + release: + types: [published] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-upload: + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: write + defaults: + run: + shell: bash + steps: + - name: clean workspace + run: rm -rf .build dist + + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: install build dependencies + run: | + sudo apt-get update + # Tauri v2 build deps + tooling. webkit2gtk-4.1 matches the tauri = "2" + # crate; ffmpeg is a runtime dependency, not bundled. + sudo apt-get install -y --no-install-recommends \ + build-essential curl file wget libssl-dev libxdo-dev \ + libwebkit2gtk-4.1-dev libgtk-3-dev \ + libayatana-appindicator3-dev librsvg2-dev + + - name: install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: install rust toolchain + run: rustup default stable + + - name: write version files + run: | + if [ -z "${GITHUB_REF_NAME:-}" ]; then + echo "GITHUB_REF_NAME is not set" >&2 + exit 1 + fi + VERSION="${GITHUB_REF_NAME#v}" + printf '{ "version": "%s" }\n' "$VERSION" > static/version.json + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" desktop/src-tauri/Cargo.toml + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" desktop/src-tauri/tauri.conf.json + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" desktop/package.json + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "Wrote version $VERSION to all version files" + + - name: build Linux CPU + run: | + PACKAGE_NAME=StemDeck-Linux-x64 \ + PACKAGE_VERSION="$GITHUB_REF_NAME" \ + bash scripts/linux/make-portable.sh + + - name: scan artifacts + run: | + echo "Artifacts staged in: $PWD/dist" + ls -lh dist + echo "SHA256 checksums:" + ( cd dist && sha256sum *.tar.gz ) + + echo "Pulling latest ClamAV scanner image..." + docker pull clamav/clamav:latest + + echo "Running ClamAV scan over dist/..." + docker run --rm -v "$PWD/dist:/scan:ro" clamav/clamav:latest \ + clamscan --recursive --infected --bell /scan + echo "ClamAV scan completed successfully. No infected files reported." + + - name: upload artifacts + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + files: | + dist/StemDeck-Linux-x64.tar.gz + dist/StemDeck-Linux-x64.tar.gz.sha256 + append_body: true + body: | + ### Artifact scan + + - The Linux portable package (CPU) was scanned with ClamAV in CI before upload. diff --git a/packaging/linux/README-LINUX.txt b/packaging/linux/README-LINUX.txt new file mode 100644 index 00000000..6159cf8a --- /dev/null +++ b/packaging/linux/README-LINUX.txt @@ -0,0 +1,53 @@ +StemDeck Linux Portable Alpha (CPU) +=================================== + +Run +--- + +1. Extract the tarball: + tar -xzf StemDeck-Linux-x64.tar.gz +2. Install the runtime prerequisites (see below). +3. Run the launcher: + cd StemDeck-Linux-x64 + ./StemDeck +4. Let first-run setup prepare local runtime assets. + +Prerequisites +------------- + +This portable package bundles its own Python runtime (torch + demucs), but the +desktop shell links against your system's WebKitGTK libraries, and StemDeck +expects FFmpeg on your PATH. Install both with your package manager. + + Debian / Ubuntu: + sudo apt update + sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0 ffmpeg + + Fedora: + sudo dnf install webkit2gtk4.1 gtk3 ffmpeg + + Arch: + sudo pacman -S webkit2gtk-4.1 gtk3 ffmpeg + +Notes +----- + +- This is the CPU-only build. Stem separation runs on the CPU and is slower + than a GPU build; an NVIDIA/CUDA variant may ship later. +- This is a portable folder, not a system package. No .desktop entry, service, + or package-manager integration is created. +- User data lives under $XDG_DATA_HOME/stemdeck (or ~/.local/share/stemdeck). +- Your stem library is written to ~/Documents/StemDeck/. +- Demucs model weights download from the backend on first use into the data + directory under models/. + +Troubleshooting +--------------- + +- "./StemDeck: error while loading shared libraries" — install the WebKitGTK + and GTK packages listed above. +- "ffmpeg not found" or a job failing immediately — install ffmpeg and ensure + `ffmpeg -version` works in your shell. +- If setup fails, check internet access and retry. +- Inspect logs under the data directory's logs/ folder. +- Deleting the data directory forces first-run setup to recreate runtime state. diff --git a/packaging/linux/THIRD_PARTY_NOTICES.txt b/packaging/linux/THIRD_PARTY_NOTICES.txt new file mode 100644 index 00000000..0a59a8cc --- /dev/null +++ b/packaging/linux/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,77 @@ +THIRD-PARTY NOTICES +=================== + +StemDeck includes third-party open-source software. Each component is +copyrighted by its respective authors and distributed under its own license. + +This starter notice is not a substitute for the full license inventory that +must be generated from the final packaged Python runtime before a public +release. + +Bundled Components +------------------ + +Python +License: Python Software Foundation License +Website: https://www.python.org/ + +Tauri +License: MIT or Apache-2.0, depending on component +Website: https://tauri.app/ + +FastAPI +License: MIT +Website: https://fastapi.tiangolo.com/ + +Uvicorn +License: BSD +Website: https://www.uvicorn.org/ + +yt-dlp +License: Unlicense +Website: https://github.com/yt-dlp/yt-dlp + +Demucs +License: MIT +Website: https://github.com/facebookresearch/demucs + +PyTorch / Torch +License: BSD-style +Website: https://pytorch.org/ + +torchaudio +License: BSD-style +Website: https://pytorch.org/audio/ + +librosa +License: ISC +Website: https://librosa.org/ + +pyloudnorm +License: MIT +Website: https://github.com/csteinmetz1/pyloudnorm + +soundfile +License: BSD +Website: https://github.com/bastibe/python-soundfile + +System Requirements (Not Bundled) +--------------------------------- + +FFmpeg is not bundled in the Linux package. StemDeck calls the `ffmpeg` binary +from your PATH; install it via your system package manager. FFmpeg may be +distributed under LGPL or GPL terms depending on how your distribution compiles +it. + +WebKitGTK and GTK shared libraries are provided by your Linux distribution and +are not bundled. They are distributed under LGPL terms. + +Demucs model weights are not bundled. They are downloaded during first use. +StemDeck should display the model source and license/usage terms before or +during download. + +Disclaimer +---------- + +This notice file is not legal advice. Before public release, verify the exact +licenses of every bundled package and generated binary artifact. diff --git a/scripts/linux/make-portable.sh b/scripts/linux/make-portable.sh new file mode 100755 index 00000000..6b62e0c5 --- /dev/null +++ b/scripts/linux/make-portable.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# +# Build a portable Linux StemDeck package: a single .tar.gz containing the +# Tauri binary plus a self-contained Python runtime (torch + demucs), so the +# user extracts and runs ./StemDeck with no toolchain. +# +# This is the Linux analog of scripts/windows/make-portable.ps1. Like the macOS +# runtime pack (scripts/macos/make-runtime-pack.sh) it bundles a full +# python-build-standalone install — a plain `venv` will not work because the +# desktop shell checks for the stdlib under python/lib/ (python_stdlib_present +# in desktop/src-tauri/src/main.rs). +# +# Phase 1 ships the CPU-only variant. FFmpeg is NOT bundled: the Linux desktop +# shell expects `ffmpeg` on PATH (see ensure_ffmpeg), so users install it via +# their package manager (e.g. `sudo apt install ffmpeg`). +# +# Layout produced (so find_repo_root matches its backend/app + python branch): +# StemDeck-Linux-x64/ +# StemDeck # Tauri ELF binary +# cpu-only # marker read by is_cpu_only_package +# README-LINUX.txt +# THIRD_PARTY_NOTICES.txt +# backend/{app,static,pyproject.toml,uv.lock} +# python/{bin/python,lib/pythonX.Y/...} # full PBS install + +set -euo pipefail + +PACKAGE_NAME="${PACKAGE_NAME:-StemDeck-Linux-x64}" +PACKAGE_VERSION="${PACKAGE_VERSION:-}" +OUTPUT_ROOT="${OUTPUT_ROOT:-dist}" +PYTHON_VERSION="${PYTHON_VERSION:-3.12}" +TORCH_VERSION="${TORCH_VERSION:-2.6.0}" +SKIP_TAURI_BUILD="${SKIP_TAURI_BUILD:-0}" + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +STAGE="${REPO_ROOT}/${OUTPUT_ROOT}/${PACKAGE_NAME}" +ARCHIVE_PATH="${REPO_ROOT}/${OUTPUT_ROOT}/${PACKAGE_NAME}.tar.gz" +CHECKSUM_PATH="${ARCHIVE_PATH}.sha256" +PYTHON_DIR="${STAGE}/python" +BACKEND_DIR="${STAGE}/backend" +TARGET_BIN="${REPO_ROOT}/desktop/src-tauri/target/release/stemdeck" + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "ERROR: this packaging script must run on Linux." >&2 + exit 1 +fi + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "ERROR: required command not found on PATH: $1" >&2 + exit 1 + fi +} + +require_command uv +require_command cargo +require_command node +require_command npm +require_command tar +require_command sha256sum + +# python-build-standalone (PBS) Python for x86_64 Linux. Unlike a venv, the +# full install carries its own stdlib under lib/, which the desktop shell needs. +echo "==> Installing python-build-standalone ${PYTHON_VERSION}" +uv python install "cpython-${PYTHON_VERSION}-linux-x86_64-gnu" +PBS_PYTHON="$(uv python find "cpython-${PYTHON_VERSION}-linux-x86_64-gnu")" +PBS_BASE_PREFIX="$("$PBS_PYTHON" -c 'import sys; print(sys.base_prefix)')" +if [[ ! -d "${PBS_BASE_PREFIX}/lib" ]]; then + echo "ERROR: PBS base prefix has no lib/ dir: ${PBS_BASE_PREFIX}" >&2 + exit 1 +fi + +echo "==> Cleaning stage" +rm -rf "$STAGE" "$ARCHIVE_PATH" "$CHECKSUM_PATH" +mkdir -p "$STAGE" "$BACKEND_DIR" "$PYTHON_DIR" + +# Copy the entire PBS install into python/ (-a preserves symlinks/permissions). +echo "==> Bundling Python runtime from ${PBS_BASE_PREFIX}" +cp -a "$PBS_BASE_PREFIX/." "$PYTHON_DIR/" +# PBS ships an EXTERNALLY-MANAGED marker that blocks installs into the copy. +find "$PYTHON_DIR/lib" -name "EXTERNALLY-MANAGED" -delete 2>/dev/null || true + +BUNDLED_PYTHON="${PYTHON_DIR}/bin/python" + +echo "==> Installing StemDeck into bundled Python" +# --system is required because python/ is a full PBS install, not a venv. +uv pip install --system --python "$BUNDLED_PYTHON" pip setuptools wheel +# Version is git-derived (hatch-vcs / setuptools-scm). Pin it so the install +# does not depend on git tags in the build checkout (#169). +if [[ -n "$PACKAGE_VERSION" ]]; then + export SETUPTOOLS_SCM_PRETEND_VERSION="${PACKAGE_VERSION#v}" +fi +uv pip install --system --python "$BUNDLED_PYTHON" "$REPO_ROOT" + +# Linux PyPI torch wheels bundle CUDA. Force-reinstall the CPU-only variant +# afterwards: pip strips the local '+cpu' version when resolving, so the project +# install pulls the CUDA wheel even if a CPU wheel was requested. Mirrors the +# proven Windows swap (--force-reinstall --no-deps replaces just torch wheels). +echo "==> Forcing CPU-only torch" +"$BUNDLED_PYTHON" -m pip install \ + "torch==${TORCH_VERSION}+cpu" "torchaudio==${TORCH_VERSION}+cpu" \ + --index-url https://download.pytorch.org/whl/cpu \ + --force-reinstall --no-deps + +echo "==> Verifying imports" +"$BUNDLED_PYTHON" -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile; print('torch', torch.__version__)" + +echo "==> Staging backend" +cp -R "$REPO_ROOT/app" "$BACKEND_DIR/app" +cp -R "$REPO_ROOT/static" "$BACKEND_DIR/static" +cp "$REPO_ROOT/pyproject.toml" "$BACKEND_DIR/pyproject.toml" +cp "$REPO_ROOT/uv.lock" "$BACKEND_DIR/uv.lock" +RESOLVED_VERSION="${PACKAGE_VERSION#v}" +printf '{ "version": "%s" }\n' "$RESOLVED_VERSION" > "$BACKEND_DIR/static/version.json" + +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" + +# CPU-only marker: read by is_cpu_only_package so the shell skips GPU detection. +touch "$STAGE/cpu-only" + +echo "==> Stripping build-time artifacts from bundled Python" +find "$PYTHON_DIR" -type d -name "__pycache__" -prune -exec rm -rf {} + 2>/dev/null || true +find "$PYTHON_DIR" -type f \( -name "*.pyc" -o -name "*.pyo" \) -delete 2>/dev/null || true +TORCH_LIB="${PYTHON_DIR}/lib/python${PYTHON_VERSION}/site-packages/torch" +for rel in include test share/cmake; do + rm -rf "${TORCH_LIB:?}/${rel}" 2>/dev/null || true +done +# Static link archives are only needed to build C++ extensions, never to run. +find "$TORCH_LIB" -name "*.a" -type f -delete 2>/dev/null || true + +echo "==> Building Tauri desktop binary" +if [[ "$SKIP_TAURI_BUILD" != "1" ]]; then + pushd "$REPO_ROOT/desktop" >/dev/null + if [[ -f package-lock.json ]]; then + npm ci --include=dev + else + npm install --include=dev + fi + CI=true node node_modules/@tauri-apps/cli/tauri.js build + popd >/dev/null +fi + +if [[ ! -f "$TARGET_BIN" ]]; then + echo "ERROR: Tauri binary not found at ${TARGET_BIN}" >&2 + exit 1 +fi +cp "$TARGET_BIN" "$STAGE/StemDeck" +chmod +x "$STAGE/StemDeck" + +echo "==> Creating archive" +tar -czf "$ARCHIVE_PATH" -C "${REPO_ROOT}/${OUTPUT_ROOT}" "$PACKAGE_NAME" +( cd "${REPO_ROOT}/${OUTPUT_ROOT}" && sha256sum "${PACKAGE_NAME}.tar.gz" > "${PACKAGE_NAME}.tar.gz.sha256" ) + +echo "==> Done" +echo "Variant : CPU-only" +echo "Stage : ${STAGE}" +echo "Archive : ${ARCHIVE_PATH}" +echo "Checksum: ${CHECKSUM_PATH}" From b4f77b2336c2e294a4904dd896108587980ce7ab Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 24 Jun 2026 12:07:04 +0100 Subject: [PATCH 2/5] fix: don't set PYTHONHOME on Linux (breaks PBS stdlib resolution) The Linux backend failed to start with 'ModuleNotFoundError: No module named encodings'. PYTHONHOME was being set to python/bin instead of the prefix python/, so CPython looked for its stdlib under python/bin/lib and could not boot. Linux bundles python-build-standalone exactly like macOS, which detects its own prefix by walking up from bin/ and must NOT have PYTHONHOME set. The two PYTHONHOME sites were gated #[cfg(not(target_os = "macos"))], wrongly including Linux alongside Windows. Only Windows -- whose portable venv keeps the stdlib under base/Lib -- needs PYTHONHOME, so gate both sites (start_backend and python_stdlib_ok) to #[cfg(windows)]. This also fixes the latent inconsistency where probe_runtime reported Python ready (python_stdlib_ok set PYTHONHOME=python, the correct prefix) while start_backend set PYTHONHOME=python/bin and failed. --- desktop/src-tauri/src/main.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index d243eb4d..06b43ad7 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -550,11 +550,14 @@ fn start_backend( (Stdio::null(), Stdio::null()) }); - // On macOS, python-build-standalone detects its own prefix by walking up from - // bin/ — PYTHONHOME is not needed and actively breaks startup when mis-computed. - // On Windows the venv launcher needs PYTHONHOME to locate the bundled stdlib. + // On macOS and Linux, python-build-standalone detects its own prefix by + // walking up from bin/ — PYTHONHOME is not needed and actively breaks + // startup when mis-computed (it would point at python/bin, whose + // lib/python3.X has no stdlib, so even `encodings` fails to import). + // Only Windows, whose portable venv keeps the stdlib under base/Lib, + // needs PYTHONHOME to locate the bundled stdlib. // Compute before moving python into Command::new. - #[cfg(not(target_os = "macos"))] + #[cfg(windows)] let pythonhome = python .parent() .and_then(|bin_dir| bin_dir.parent().map(|venv| (venv, bin_dir))) @@ -570,7 +573,7 @@ fn start_backend( "--port", &port.to_string(), ]); - #[cfg(not(target_os = "macos"))] + #[cfg(windows)] if let Some(ref pythonhome) = pythonhome { cmd.env("PYTHONHOME", pythonhome); } @@ -929,17 +932,17 @@ fn python_stdlib_ok(python: &Path) -> bool { } let mut cmd = Command::new(python); cmd.args(["-c", "import encodings"]); - #[cfg(not(target_os = "macos"))] + // Only Windows needs PYTHONHOME: its portable venv keeps the stdlib under + // base/Lib. macOS and Linux use python-build-standalone, which auto-detects + // its prefix from bin/ — setting PYTHONHOME there points at the wrong dir + // and breaks the import (parity with start_backend). + #[cfg(windows)] { let venv_root = python.parent().and_then(|b| b.parent()); - // On Windows the portable venv layout puts the stdlib in base/Lib/, not Lib/. let pythonhome = venv_root.map(|venv| { - #[cfg(windows)] - { - let base = venv.join("base"); - if base.join("Lib").join("os.py").is_file() { - return base; - } + let base = venv.join("base"); + if base.join("Lib").join("os.py").is_file() { + return base; } venv.to_path_buf() }); From f75dc9e9278fcc959fb166c7abcca08d178faea1 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 24 Jun 2026 14:58:41 +0100 Subject: [PATCH 3/5] feat: add NVIDIA/CUDA Linux portable variant Adds a second Linux package, StemDeck-Linux-x64.NVIDIA.tar.gz, with CUDA-enabled torch baked in (mirrors the Windows NVIDIA variant). - make-portable.sh: CPU_ONLY toggle (default 1). CPU_ONLY=0 keeps the project's default torch wheel, which on Linux x86_64 is the CUDA build, and omits the cpu-only marker so the desktop shell detects the GPU and uses CUDA at runtime. No app-side changes needed -- the CUDA detection/ install path in main.rs is already cfg(not(macos)) and covers Linux. - linux-release.yml: builds both variants in one job. CPU first (full Tauri build), then NVIDIA with SKIP_TAURI_BUILD=1 reusing the same binary. Adds a free-disk-space step (CUDA bundle is several GB) and drops each uncompressed stage after taring to stay within the hosted runner's disk. - README-LINUX.txt: documents both variants and the NVIDIA driver prerequisite (nvidia-smi must work; CUDA runtime is bundled, no toolkit install needed; falls back to CPU when no GPU). --- .github/workflows/linux-release.yml | 26 ++++++++++++++++++- packaging/linux/README-LINUX.txt | 31 +++++++++++++++++----- scripts/linux/make-portable.sh | 40 ++++++++++++++++++++--------- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index fdb138b7..d3572f19 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -58,11 +58,33 @@ jobs: echo "VERSION=$VERSION" >> "$GITHUB_ENV" echo "Wrote version $VERSION to all version files" + - name: free disk space + run: | + # The NVIDIA package bundles CUDA-enabled torch (several GB). Reclaim + # space the hosted runner spends on preinstalled SDKs we don't use. + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android \ + /usr/local/share/boost "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}" || true + df -h / + - name: build Linux CPU run: | PACKAGE_NAME=StemDeck-Linux-x64 \ PACKAGE_VERSION="$GITHUB_REF_NAME" \ bash scripts/linux/make-portable.sh + # Drop the uncompressed stage but keep the tarball; frees disk for the + # larger NVIDIA build. The Tauri binary under target/ is preserved. + rm -rf dist/StemDeck-Linux-x64 + + - name: build Linux NVIDIA + run: | + # CPU_ONLY=0 keeps the CUDA torch wheel; SKIP_TAURI_BUILD=1 reuses the + # binary built in the CPU step (identical for both variants). + PACKAGE_NAME=StemDeck-Linux-x64.NVIDIA \ + PACKAGE_VERSION="$GITHUB_REF_NAME" \ + CPU_ONLY=0 \ + SKIP_TAURI_BUILD=1 \ + bash scripts/linux/make-portable.sh + rm -rf dist/StemDeck-Linux-x64.NVIDIA - name: scan artifacts run: | @@ -85,8 +107,10 @@ jobs: files: | dist/StemDeck-Linux-x64.tar.gz dist/StemDeck-Linux-x64.tar.gz.sha256 + dist/StemDeck-Linux-x64.NVIDIA.tar.gz + dist/StemDeck-Linux-x64.NVIDIA.tar.gz.sha256 append_body: true body: | ### Artifact scan - - The Linux portable package (CPU) was scanned with ClamAV in CI before upload. + - The Linux portable packages (CPU and NVIDIA) were scanned with ClamAV in CI before upload. diff --git a/packaging/linux/README-LINUX.txt b/packaging/linux/README-LINUX.txt index 6159cf8a..87288092 100644 --- a/packaging/linux/README-LINUX.txt +++ b/packaging/linux/README-LINUX.txt @@ -1,14 +1,20 @@ -StemDeck Linux Portable Alpha (CPU) -=================================== +StemDeck Linux Portable Alpha +============================= + +This comes in two variants. Pick one: + +- StemDeck-Linux-x64.tar.gz CPU-only (smaller; runs anywhere) +- StemDeck-Linux-x64.NVIDIA.tar.gz NVIDIA/CUDA (larger; much faster on an + NVIDIA GPU, falls back to CPU if no GPU) Run --- -1. Extract the tarball: +1. Extract the tarball, e.g.: tar -xzf StemDeck-Linux-x64.tar.gz 2. Install the runtime prerequisites (see below). 3. Run the launcher: - cd StemDeck-Linux-x64 + cd StemDeck-Linux-x64 # or StemDeck-Linux-x64.NVIDIA ./StemDeck 4. Let first-run setup prepare local runtime assets. @@ -29,11 +35,24 @@ expects FFmpeg on your PATH. Install both with your package manager. Arch: sudo pacman -S webkit2gtk-4.1 gtk3 ffmpeg +NVIDIA variant +-------------- + +The NVIDIA/CUDA build bundles a CUDA-enabled PyTorch. To use the GPU you also +need a working NVIDIA driver on the host such that `nvidia-smi` runs and reports +your GPU. The CUDA runtime itself is bundled — you do NOT need a separate CUDA +toolkit install, only the driver. + + Check your driver: + nvidia-smi + +If no usable GPU is detected, the NVIDIA build still runs but falls back to CPU. +If you do not have an NVIDIA GPU, use the CPU-only tarball instead — it is +smaller and avoids downloading the CUDA runtime. + Notes ----- -- This is the CPU-only build. Stem separation runs on the CPU and is slower - than a GPU build; an NVIDIA/CUDA variant may ship later. - This is a portable folder, not a system package. No .desktop entry, service, or package-manager integration is created. - User data lives under $XDG_DATA_HOME/stemdeck (or ~/.local/share/stemdeck). diff --git a/scripts/linux/make-portable.sh b/scripts/linux/make-portable.sh index 6b62e0c5..9674d581 100755 --- a/scripts/linux/make-portable.sh +++ b/scripts/linux/make-portable.sh @@ -31,6 +31,11 @@ OUTPUT_ROOT="${OUTPUT_ROOT:-dist}" PYTHON_VERSION="${PYTHON_VERSION:-3.12}" TORCH_VERSION="${TORCH_VERSION:-2.6.0}" SKIP_TAURI_BUILD="${SKIP_TAURI_BUILD:-0}" +# CPU_ONLY=1 (default): force the CPU-only torch wheel and mark the package so the +# desktop shell skips GPU detection. CPU_ONLY=0: keep the project's default torch, +# which on Linux x86_64 is the CUDA build (NVIDIA variant) — the shell then detects +# the GPU and uses CUDA at runtime. +CPU_ONLY="${CPU_ONLY:-1}" REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" STAGE="${REPO_ROOT}/${OUTPUT_ROOT}/${PACKAGE_NAME}" @@ -92,18 +97,22 @@ if [[ -n "$PACKAGE_VERSION" ]]; then fi uv pip install --system --python "$BUNDLED_PYTHON" "$REPO_ROOT" -# Linux PyPI torch wheels bundle CUDA. Force-reinstall the CPU-only variant -# afterwards: pip strips the local '+cpu' version when resolving, so the project -# install pulls the CUDA wheel even if a CPU wheel was requested. Mirrors the -# proven Windows swap (--force-reinstall --no-deps replaces just torch wheels). -echo "==> Forcing CPU-only torch" -"$BUNDLED_PYTHON" -m pip install \ - "torch==${TORCH_VERSION}+cpu" "torchaudio==${TORCH_VERSION}+cpu" \ - --index-url https://download.pytorch.org/whl/cpu \ - --force-reinstall --no-deps +if [[ "$CPU_ONLY" == "1" ]]; then + # Linux PyPI torch wheels bundle CUDA. Force-reinstall the CPU-only variant + # afterwards: pip strips the local '+cpu' version when resolving, so the project + # install pulls the CUDA wheel even if a CPU wheel was requested. Mirrors the + # proven Windows swap (--force-reinstall --no-deps replaces just torch wheels). + echo "==> Forcing CPU-only torch" + "$BUNDLED_PYTHON" -m pip install \ + "torch==${TORCH_VERSION}+cpu" "torchaudio==${TORCH_VERSION}+cpu" \ + --index-url https://download.pytorch.org/whl/cpu \ + --force-reinstall --no-deps +else + echo "==> Keeping CUDA torch (NVIDIA variant)" +fi echo "==> Verifying imports" -"$BUNDLED_PYTHON" -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile; print('torch', torch.__version__)" +"$BUNDLED_PYTHON" -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile; print('torch', torch.__version__, 'cuda', torch.version.cuda)" echo "==> Staging backend" cp -R "$REPO_ROOT/app" "$BACKEND_DIR/app" @@ -117,7 +126,10 @@ 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" # CPU-only marker: read by is_cpu_only_package so the shell skips GPU detection. -touch "$STAGE/cpu-only" +# Omitted for the NVIDIA variant so the shell detects the GPU and uses CUDA. +if [[ "$CPU_ONLY" == "1" ]]; then + touch "$STAGE/cpu-only" +fi echo "==> Stripping build-time artifacts from bundled Python" find "$PYTHON_DIR" -type d -name "__pycache__" -prune -exec rm -rf {} + 2>/dev/null || true @@ -153,7 +165,11 @@ tar -czf "$ARCHIVE_PATH" -C "${REPO_ROOT}/${OUTPUT_ROOT}" "$PACKAGE_NAME" ( cd "${REPO_ROOT}/${OUTPUT_ROOT}" && sha256sum "${PACKAGE_NAME}.tar.gz" > "${PACKAGE_NAME}.tar.gz.sha256" ) echo "==> Done" -echo "Variant : CPU-only" +if [[ "$CPU_ONLY" == "1" ]]; then + echo "Variant : CPU-only" +else + echo "Variant : NVIDIA/CUDA" +fi echo "Stage : ${STAGE}" echo "Archive : ${ARCHIVE_PATH}" echo "Checksum: ${CHECKSUM_PATH}" From 9676cdefdbedeed579be059fededa6a7b553c499 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 24 Jun 2026 15:28:01 +0100 Subject: [PATCH 4/5] ci: run Linux release on self-hosted linux/x64 runner Targets the org's self-hosted wsl2 runner ([self-hosted, linux, x64]) instead of hosted ubuntu-latest, matching the Windows/macOS release jobs. Drops the free-disk-space step: it was a hosted-runner workaround and would needlessly rm system directories on a persistent self-hosted box (WSL2's virtual disk has ample room for the CUDA bundle). --- .github/workflows/linux-release.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index d3572f19..850a8cf7 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -12,7 +12,8 @@ concurrency: jobs: build-and-upload: - runs-on: ubuntu-latest + # Runner must be linux/x64 with rustup, Node.js, Docker, and sudo apt access. + runs-on: [self-hosted, linux, x64] timeout-minutes: 90 permissions: contents: write @@ -58,14 +59,6 @@ jobs: echo "VERSION=$VERSION" >> "$GITHUB_ENV" echo "Wrote version $VERSION to all version files" - - name: free disk space - run: | - # The NVIDIA package bundles CUDA-enabled torch (several GB). Reclaim - # space the hosted runner spends on preinstalled SDKs we don't use. - sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android \ - /usr/local/share/boost "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}" || true - df -h / - - name: build Linux CPU run: | PACKAGE_NAME=StemDeck-Linux-x64 \ From 2d755436964f9523324501a2d8ad06c4b9018a9b Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 24 Jun 2026 17:50:12 +0100 Subject: [PATCH 5/5] ci: add workflow_dispatch test build for Linux release Lets you run the full two-variant build + ClamAV scan on the self-hosted runner without publishing a release, to validate the runner toolchain and the CUDA build. Resolves the version from a manual input (default 0.0.0, must be valid PEP 440) instead of the branch ref, and skips the upload step on non-release events. --- .github/workflows/linux-release.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 850a8cf7..9b045ecc 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -3,6 +3,15 @@ name: Linux Release on: release: types: [published] + # Manual test build: builds and scans both variants on the self-hosted runner + # but does NOT upload (no release to attach to). Lets you validate the runner + # toolchain and the CUDA build without cutting a release tag. + workflow_dispatch: + inputs: + version: + description: "Version for the test build (must be valid PEP 440, e.g. 0.0.0)" + required: false + default: "0.0.0" permissions: {} @@ -45,12 +54,18 @@ jobs: run: rustup default stable - name: write version files + env: + DISPATCH_VERSION: ${{ inputs.version }} run: | - if [ -z "${GITHUB_REF_NAME:-}" ]; then - echo "GITHUB_REF_NAME is not set" >&2 + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${DISPATCH_VERSION#v}" + else + VERSION="${GITHUB_REF_NAME#v}" + fi + if [ -z "$VERSION" ]; then + echo "Could not determine a version" >&2 exit 1 fi - VERSION="${GITHUB_REF_NAME#v}" printf '{ "version": "%s" }\n' "$VERSION" > static/version.json sed -i "s/^version = \".*\"/version = \"$VERSION\"/" desktop/src-tauri/Cargo.toml sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" desktop/src-tauri/tauri.conf.json @@ -62,7 +77,7 @@ jobs: - name: build Linux CPU run: | PACKAGE_NAME=StemDeck-Linux-x64 \ - PACKAGE_VERSION="$GITHUB_REF_NAME" \ + PACKAGE_VERSION="$VERSION" \ bash scripts/linux/make-portable.sh # Drop the uncompressed stage but keep the tarball; frees disk for the # larger NVIDIA build. The Tauri binary under target/ is preserved. @@ -73,7 +88,7 @@ jobs: # CPU_ONLY=0 keeps the CUDA torch wheel; SKIP_TAURI_BUILD=1 reuses the # binary built in the CPU step (identical for both variants). PACKAGE_NAME=StemDeck-Linux-x64.NVIDIA \ - PACKAGE_VERSION="$GITHUB_REF_NAME" \ + PACKAGE_VERSION="$VERSION" \ CPU_ONLY=0 \ SKIP_TAURI_BUILD=1 \ bash scripts/linux/make-portable.sh @@ -95,6 +110,8 @@ jobs: echo "ClamAV scan completed successfully. No infected files reported." - name: upload artifacts + # Only attach to a real release; a manual test build has nothing to upload to. + if: github.event_name == 'release' uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: files: |