chore: import upstream snapshot with attribution
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
This commit is contained in:
Executable
+192
@@ -0,0 +1,192 @@
|
||||
#!/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 in the tarball (so we
|
||||
# don't redistribute it); instead the desktop shell downloads a static build on
|
||||
# first launch into the user data dir, falling back to a system `ffmpeg` on PATH
|
||||
# when one exists (see ensure_ffmpeg / download_linux_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}"
|
||||
# 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}"
|
||||
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"
|
||||
|
||||
# Always bake the small CPU-only torch wheel — for BOTH variants. On Linux the
|
||||
# default PyPI torch wheel bundles the full CUDA runtime (~2.5 GB), which makes
|
||||
# the packaged tarball exceed GitHub's 2 GiB per-asset release limit. So we
|
||||
# mirror what the Windows NVIDIA package actually does: ship CPU torch, and let
|
||||
# the desktop shell download the matching CUDA wheel at first run on GPU
|
||||
# machines (install_cuda_torch, gated cfg(not(macos)) so it covers Linux). The
|
||||
# NVIDIA variant differs only by omitting the cpu-only marker below.
|
||||
#
|
||||
# pip strips the local '+cpu' version when resolving, so the project install
|
||||
# pulls the CUDA wheel even if a CPU wheel was requested; --force-reinstall
|
||||
# --no-deps replaces just the torch/torchaudio wheels (proven on Windows).
|
||||
echo "==> Baking CPU-only torch (NVIDIA variant downloads CUDA at first run)"
|
||||
"$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
|
||||
|
||||
# The project install above pulled the default Linux torch, which is the CUDA
|
||||
# build, dragging in nvidia-* CUDA runtime packages (cuDNN, cuBLAS, NCCL, ...) and
|
||||
# triton -- together ~2.5 GB. The CPU torch swap used --no-deps, so those packages
|
||||
# are now orphaned but still installed, bloating the tarball past GitHub's 2 GiB
|
||||
# asset limit. Remove them: CPU torch does not use them, and the NVIDIA variant
|
||||
# re-downloads CUDA at first run anyway.
|
||||
echo "==> Removing orphaned CUDA runtime packages"
|
||||
orphans=$("$BUNDLED_PYTHON" -m pip list --format=freeze 2>/dev/null \
|
||||
| sed -n 's/^\(nvidia-[^=]*\)==.*/\1/p')
|
||||
orphans="$orphans triton"
|
||||
echo " removing:$orphans"
|
||||
"$BUNDLED_PYTHON" -m pip uninstall -y $orphans 2>/dev/null || true
|
||||
|
||||
echo "==> Verifying imports"
|
||||
"$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"
|
||||
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.
|
||||
# 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
|
||||
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"
|
||||
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}"
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="${ARCH:-arm64}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
# Default the version to the current git tag so local builds match the tag with
|
||||
# no VERSION passed; falls back to a dev label outside a tagged checkout. (#169)
|
||||
VERSION="${VERSION:-$(git -C "$REPO_ROOT" describe --tags --always 2>/dev/null || echo 0.0.0-dev)}"
|
||||
VERSION="${VERSION#v}"
|
||||
BUILD_DIR="${REPO_ROOT}/.build"
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "ERROR: make-app.sh must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then
|
||||
echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in node npm cargo; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: required command not found on PATH: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
echo "==> Stamping version ${VERSION}"
|
||||
sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" "$REPO_ROOT/desktop/src-tauri/Cargo.toml"
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$REPO_ROOT/desktop/src-tauri/tauri.conf.json"
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$REPO_ROOT/desktop/package.json"
|
||||
|
||||
cd "$REPO_ROOT/desktop"
|
||||
|
||||
# Tauri CLI reads CI env var as a boolean flag; Woodpecker sets CI=woodpecker
|
||||
# which fails the boolean parser. Force a valid value.
|
||||
export CI=true
|
||||
|
||||
npm ci
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
npx tauri build --bundles app --target aarch64-apple-darwin
|
||||
TARGET_DIR="$REPO_ROOT/desktop/src-tauri/target/aarch64-apple-darwin/release"
|
||||
else
|
||||
npx tauri build --bundles app --target x86_64-apple-darwin
|
||||
TARGET_DIR="$REPO_ROOT/desktop/src-tauri/target/x86_64-apple-darwin/release"
|
||||
fi
|
||||
|
||||
APP_DIR="${TARGET_DIR}/bundle/macos/StemDeck.app"
|
||||
if [[ ! -d "$APP_DIR" ]]; then
|
||||
APP_DIR="$(find "$REPO_ROOT/desktop/src-tauri/target" -path '*/bundle/macos/StemDeck.app' -type d | head -1)"
|
||||
fi
|
||||
if [[ -z "$APP_DIR" || ! -d "$APP_DIR" ]]; then
|
||||
echo "ERROR: could not find built StemDeck.app" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESOURCES="${APP_DIR}/Contents/Resources"
|
||||
mkdir -p "$RESOURCES"
|
||||
|
||||
if [[ -f "$BUILD_DIR/runtime-manifest-${ARCH}.json" ]]; then
|
||||
cp "$BUILD_DIR/runtime-manifest-${ARCH}.json" "$RESOURCES/runtime-manifest.json"
|
||||
else
|
||||
cp "$REPO_ROOT/desktop/ui/runtime-manifest.json" "$RESOURCES/runtime-manifest.json"
|
||||
fi
|
||||
|
||||
if [[ -f "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" ]]; then
|
||||
cp "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" "$RESOURCES/THIRD_PARTY_NOTICES.txt"
|
||||
fi
|
||||
|
||||
echo "$APP_DIR" > "$BUILD_DIR/app-path-${ARCH}.txt"
|
||||
echo "==> App ready: $APP_DIR"
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="${ARCH:-arm64}"
|
||||
VERSION="${VERSION:-LOCAL_DEV_TEST}"
|
||||
VERSION="${VERSION#v}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${REPO_ROOT}/.build"
|
||||
DIST_DIR="${BUILD_DIR}/macos-dist"
|
||||
DMG_STAGING="${BUILD_DIR}/dmg-staging-${ARCH}"
|
||||
DMG_NAME="StemDeck-macOS-${ARCH}.dmg"
|
||||
DMG_PATH="${DIST_DIR}/${DMG_NAME}"
|
||||
DMG_RW_PATH="${DIST_DIR}/StemDeck-macOS-${ARCH}.rw.dmg"
|
||||
RUNTIME_NAME="StemDeck-runtime-macOS-${ARCH}.tar.zst"
|
||||
RUNTIME_PATH="${BUILD_DIR}/${RUNTIME_NAME}"
|
||||
BACKGROUND_SRC="${REPO_ROOT}/packaging/macos/dmg-background.svg"
|
||||
BACKGROUND_DIR_NAME=".background"
|
||||
BACKGROUND_PNG_NAME="dmg-background.png"
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "ERROR: make-dmg.sh must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then
|
||||
echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in ditto hdiutil qlmanage shasum; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: required command not found on PATH: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
APP_DIR="${REPO_ROOT}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app"
|
||||
else
|
||||
APP_DIR="${REPO_ROOT}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/StemDeck.app"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$APP_DIR" ]]; then
|
||||
echo "ERROR: built app not found: $APP_DIR" >&2
|
||||
echo "Run: ARCH=${ARCH} scripts/macos/make-app.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$RUNTIME_PATH" ]]; then
|
||||
echo "ERROR: runtime pack not found: $RUNTIME_PATH" >&2
|
||||
echo "Run: ARCH=${ARCH} VERSION=${VERSION} scripts/macos/make-runtime-pack.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$DMG_STAGING"
|
||||
mkdir -p "$DMG_STAGING" "$DIST_DIR"
|
||||
mkdir -p "$DMG_STAGING/$BACKGROUND_DIR_NAME"
|
||||
|
||||
ditto "$APP_DIR" "$DMG_STAGING/StemDeck.app"
|
||||
ln -s /Applications "$DMG_STAGING/Applications"
|
||||
|
||||
if [[ -f "$BACKGROUND_SRC" ]]; then
|
||||
qlmanage -t -s 1320 -o "$DMG_STAGING/$BACKGROUND_DIR_NAME" "$BACKGROUND_SRC" >/dev/null 2>&1
|
||||
mv "$DMG_STAGING/$BACKGROUND_DIR_NAME/dmg-background.svg.png" "$DMG_STAGING/$BACKGROUND_DIR_NAME/$BACKGROUND_PNG_NAME"
|
||||
fi
|
||||
|
||||
if [[ -f "$REPO_ROOT/packaging/macos/README-macOS.txt" ]]; then
|
||||
cp "$REPO_ROOT/packaging/macos/README-macOS.txt" "$DMG_STAGING/README-macOS.txt"
|
||||
fi
|
||||
|
||||
if [[ -f "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" ]]; then
|
||||
cp "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" "$DMG_STAGING/THIRD_PARTY_NOTICES.txt"
|
||||
fi
|
||||
|
||||
rm -f "$DMG_PATH" "$DMG_RW_PATH"
|
||||
hdiutil create \
|
||||
-volname "StemDeck" \
|
||||
-srcfolder "$DMG_STAGING" \
|
||||
-ov \
|
||||
-format UDRW \
|
||||
"$DMG_RW_PATH"
|
||||
|
||||
MOUNT_DIR="$(mktemp -d /tmp/stemdeck-dmg.XXXXXX)"
|
||||
cleanup_mount() {
|
||||
hdiutil detach "$MOUNT_DIR" >/dev/null 2>&1 || true
|
||||
rmdir "$MOUNT_DIR" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup_mount EXIT
|
||||
|
||||
hdiutil attach "$DMG_RW_PATH" -readwrite -noverify -nobrowse -mountpoint "$MOUNT_DIR" >/dev/null
|
||||
|
||||
if command -v SetFile >/dev/null 2>&1; then
|
||||
SetFile -a V "$MOUNT_DIR/$BACKGROUND_DIR_NAME" || true
|
||||
SetFile -a V "$MOUNT_DIR/README-macOS.txt" || true
|
||||
SetFile -a V "$MOUNT_DIR/THIRD_PARTY_NOTICES.txt" || true
|
||||
fi
|
||||
|
||||
if [[ -f "$MOUNT_DIR/$BACKGROUND_DIR_NAME/$BACKGROUND_PNG_NAME" ]]; then
|
||||
osascript <<APPLESCRIPT || echo "warning: DMG window styling failed (cosmetic only, DMG is still valid)"
|
||||
tell application "Finder"
|
||||
set dmgFolder to POSIX file "$MOUNT_DIR" as alias
|
||||
open dmgFolder
|
||||
set current view of container window of dmgFolder to icon view
|
||||
set toolbar visible of container window of dmgFolder to false
|
||||
set statusbar visible of container window of dmgFolder to false
|
||||
set bounds of container window of dmgFolder to {100, 100, 760, 500}
|
||||
set viewOptions to icon view options of container window of dmgFolder
|
||||
set arrangement of viewOptions to not arranged
|
||||
set icon size of viewOptions to 104
|
||||
set text size of viewOptions to 13
|
||||
set background picture of viewOptions to POSIX file "$MOUNT_DIR/$BACKGROUND_DIR_NAME/$BACKGROUND_PNG_NAME"
|
||||
set position of item "StemDeck.app" of dmgFolder to {205, 205}
|
||||
set position of item "Applications" of dmgFolder to {455, 205}
|
||||
close container window of dmgFolder
|
||||
open dmgFolder
|
||||
update dmgFolder without registering applications
|
||||
delay 1
|
||||
close container window of dmgFolder
|
||||
end tell
|
||||
APPLESCRIPT
|
||||
fi
|
||||
|
||||
sync
|
||||
hdiutil detach "$MOUNT_DIR" >/dev/null
|
||||
rmdir "$MOUNT_DIR"
|
||||
trap - EXIT
|
||||
|
||||
hdiutil convert "$DMG_RW_PATH" -format UDZO -imagekey zlib-level=9 -o "$DMG_PATH" >/dev/null
|
||||
rm -f "$DMG_RW_PATH"
|
||||
|
||||
CHECKSUMS_PATH="${DIST_DIR}/SHA256SUMS-macOS-${ARCH}.txt"
|
||||
{
|
||||
shasum -a 256 "$DMG_PATH"
|
||||
shasum -a 256 "$RUNTIME_PATH"
|
||||
} | sed "s#${REPO_ROOT}/##" > "$CHECKSUMS_PATH"
|
||||
|
||||
echo "==> DMG ready: $DMG_PATH"
|
||||
echo "==> Runtime pack: $RUNTIME_PATH"
|
||||
echo "==> Checksums: $CHECKSUMS_PATH"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
SOURCE_SVG="${SOURCE_SVG:-${REPO_ROOT}/imgs/stemdeck-svg-assets/stemdeck-icon.svg}"
|
||||
ICON_DIR="${REPO_ROOT}/desktop/src-tauri/icons"
|
||||
WORK_DIR="${TMPDIR:-/tmp}/stemdeck-iconset"
|
||||
RENDER_DIR="${WORK_DIR}/render"
|
||||
ICONSET_DIR="${WORK_DIR}/icon.iconset"
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "ERROR: make-iconset.sh must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in qlmanage sips iconutil; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: required command not found on PATH: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -f "$SOURCE_SVG" ]]; then
|
||||
echo "ERROR: source SVG not found: $SOURCE_SVG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$RENDER_DIR" "$ICONSET_DIR" "$ICON_DIR"
|
||||
|
||||
qlmanage -t -s 1024 -o "$RENDER_DIR" "$SOURCE_SVG" >/dev/null
|
||||
RENDERED_PNG="$(find "$RENDER_DIR" -maxdepth 1 -type f -name '*.png' | head -1)"
|
||||
if [[ -z "$RENDERED_PNG" || ! -f "$RENDERED_PNG" ]]; then
|
||||
echo "ERROR: qlmanage did not render a PNG from $SOURCE_SVG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$RENDERED_PNG" "$ICON_DIR/icon.png"
|
||||
|
||||
for size in 16 32 128 256 512; do
|
||||
sips -z "$size" "$size" "$ICON_DIR/icon.png" \
|
||||
--out "$ICONSET_DIR/icon_${size}x${size}.png" >/dev/null
|
||||
done
|
||||
|
||||
sips -z 32 32 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_16x16@2x.png" >/dev/null
|
||||
sips -z 64 64 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_32x32@2x.png" >/dev/null
|
||||
sips -z 256 256 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_128x128@2x.png" >/dev/null
|
||||
sips -z 512 512 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_256x256@2x.png" >/dev/null
|
||||
cp "$ICON_DIR/icon.png" "$ICONSET_DIR/icon_512x512@2x.png"
|
||||
|
||||
iconutil -c icns "$ICONSET_DIR" -o "$ICON_DIR/icon.icns"
|
||||
|
||||
echo "==> Icon PNG: $ICON_DIR/icon.png"
|
||||
echo "==> Icon ICNS: $ICON_DIR/icon.icns"
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="${ARCH:-arm64}"
|
||||
VERSION="${VERSION:-LOCAL_DEV_TEST}"
|
||||
VERSION="${VERSION#v}"
|
||||
RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://github.com/stemdeckapp/stemdeck/releases/download/v${VERSION}}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${REPO_ROOT}/.build"
|
||||
STAGING="${BUILD_DIR}/runtime-staging-${ARCH}"
|
||||
RUNTIME_DIR="${STAGING}/runtime"
|
||||
PYTHON_DIR="${RUNTIME_DIR}/python"
|
||||
BACKEND_DIR="${RUNTIME_DIR}/backend"
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "ERROR: make-runtime-pack.sh must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then
|
||||
echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-}"
|
||||
if [[ -z "$PYTHON_BIN" ]]; then
|
||||
for candidate in python3.12 python3.11 python3.10 python3; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for cmd in ditto shasum tar "$PYTHON_BIN"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: required command not found on PATH: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
PYTHON_VERSION="$("$PYTHON_BIN" - <<'PY'
|
||||
import sys
|
||||
print(f"{sys.version_info.major}.{sys.version_info.minor}")
|
||||
PY
|
||||
)"
|
||||
PYTHON_MACHINE="$("$PYTHON_BIN" - <<'PY'
|
||||
import platform
|
||||
print(platform.machine())
|
||||
PY
|
||||
)"
|
||||
case "$PYTHON_VERSION" in
|
||||
3.10|3.11|3.12|3.13) ;;
|
||||
*)
|
||||
echo "ERROR: ${PYTHON_BIN} is Python ${PYTHON_VERSION}; Torch 2.6 runtime builds require Python 3.10-3.13." >&2
|
||||
echo "Set PYTHON_BIN=/path/to/python3.12 or PYTHON_BIN=/path/to/python3.11." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
HOST_ARCH="$(uname -m)"
|
||||
if [[ "$ARCH" == "arm64" && "$PYTHON_MACHINE" != "arm64" ]]; then
|
||||
echo "ERROR: arm64 runtime requires an arm64 Python, got ${PYTHON_MACHINE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ARCH" == "x64" && "$PYTHON_MACHINE" != "x86_64" ]]; then
|
||||
echo "ERROR: x64 runtime requires an x86_64 Python, got ${PYTHON_MACHINE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ARCH" == "x64" && "$HOST_ARCH" == "arm64" ]]; then
|
||||
if ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
|
||||
echo "ERROR: x64 runtime on arm64 hosts requires Rosetta 2." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$STAGING"
|
||||
mkdir -p "$PYTHON_DIR" "$BACKEND_DIR" "$BUILD_DIR"
|
||||
|
||||
echo "==> Bundling Python installation (${ARCH})"
|
||||
echo "==> Python: $("$PYTHON_BIN" --version)"
|
||||
echo "==> Python architecture: ${PYTHON_MACHINE}"
|
||||
|
||||
# Get the full PBS (python-build-standalone) installation root.
|
||||
# This directory has the complete stdlib in lib/pythonX.Y/ — unlike a venv,
|
||||
# which only creates site-packages/ and relies on the original base_prefix
|
||||
# (a path that won't exist on user machines) for stdlib.
|
||||
PYTHON_BASE_PREFIX="$("$PYTHON_BIN" - <<'PY'
|
||||
import sys
|
||||
print(sys.base_prefix)
|
||||
PY
|
||||
)"
|
||||
echo "==> Python base prefix: ${PYTHON_BASE_PREFIX}"
|
||||
|
||||
if [[ ! -d "${PYTHON_BASE_PREFIX}/lib" ]]; then
|
||||
echo "ERROR: Python base prefix has no lib/ dir: ${PYTHON_BASE_PREFIX}" >&2
|
||||
echo " Make sure PYTHON_BIN points to a python-build-standalone (UV) Python." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the stdlib is actually present in base_prefix before copying.
|
||||
STDLIB_CHECK="$("$PYTHON_BIN" - <<'PY'
|
||||
import sys, pathlib
|
||||
ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
p = pathlib.Path(sys.base_prefix) / "lib" / ver / "encodings" / "__init__.py"
|
||||
print("ok" if p.is_file() else f"missing:{p}")
|
||||
PY
|
||||
)"
|
||||
if [[ "$STDLIB_CHECK" != "ok" ]]; then
|
||||
echo "ERROR: stdlib not found in base_prefix (${STDLIB_CHECK})" >&2
|
||||
echo " base_prefix: ${PYTHON_BASE_PREFIX}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy the entire PBS Python installation into the runtime bundle.
|
||||
# ditto preserves symlinks, HFS+ metadata, and extended attributes.
|
||||
ditto "$PYTHON_BASE_PREFIX" "$PYTHON_DIR"
|
||||
|
||||
# PBS Python ships an EXTERNALLY-MANAGED marker that blocks uv from installing
|
||||
# packages into it. Remove it so we can treat this copy as our own install.
|
||||
find "$PYTHON_DIR/lib" -name "EXTERNALLY-MANAGED" -delete
|
||||
|
||||
echo "==> Installing packages into bundled Python"
|
||||
# --system is required because $PYTHON_DIR is not a venv (it's a full Python install).
|
||||
uv pip install --system --python "$PYTHON_DIR/bin/python" pip setuptools wheel
|
||||
# The project version is git-derived (hatch-vcs). Pin it explicitly from $VERSION
|
||||
# so the install doesn't depend on git tags being present in the build checkout (#169).
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION="${VERSION#v}" \
|
||||
uv pip install --system --python "$PYTHON_DIR/bin/python" "$REPO_ROOT"
|
||||
|
||||
echo "==> Verifying stdlib and imports"
|
||||
PYTHON_DIR="$PYTHON_DIR" PYTHONHOME="$PYTHON_DIR" "$PYTHON_DIR/bin/python" - <<'PY'
|
||||
import importlib, os, pathlib, sys
|
||||
|
||||
ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
stdlib = pathlib.Path(os.environ["PYTHON_DIR"]) / "lib" / ver
|
||||
if not (stdlib / "encodings" / "__init__.py").is_file():
|
||||
print(f"ERROR: encodings not found in {stdlib}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f" stdlib OK at {stdlib}")
|
||||
|
||||
packages = [
|
||||
"fastapi", "uvicorn", "yt_dlp", "demucs", "torch", "torchaudio",
|
||||
"librosa", "pyloudnorm", "soundfile",
|
||||
]
|
||||
for package in packages:
|
||||
importlib.import_module(package)
|
||||
print(f" OK {package}")
|
||||
PY
|
||||
|
||||
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"
|
||||
|
||||
cat > "$BACKEND_DIR/static/version.json" <<JSON
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"arch": "${ARCH}"
|
||||
}
|
||||
JSON
|
||||
|
||||
echo "==> Capturing dependency inventory"
|
||||
mkdir -p "$RUNTIME_DIR/licenses"
|
||||
uv pip list --system --python "$PYTHON_DIR/bin/python" --format=json > "$RUNTIME_DIR/licenses/pip-list.json"
|
||||
|
||||
cat > "$RUNTIME_DIR/runtime-manifest.json" <<JSON
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"arch": "${ARCH}",
|
||||
"createdBy": "scripts/macos/make-runtime-pack.sh"
|
||||
}
|
||||
JSON
|
||||
|
||||
echo "==> Stripping Python caches"
|
||||
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
|
||||
|
||||
ARCHIVE_NAME="StemDeck-runtime-macOS-${ARCH}.tar.zst"
|
||||
ARCHIVE_PATH="${BUILD_DIR}/${ARCHIVE_NAME}"
|
||||
if command -v zstd >/dev/null 2>&1; then
|
||||
tar --zstd -cf "$ARCHIVE_PATH" -C "$STAGING" runtime
|
||||
else
|
||||
ARCHIVE_NAME="StemDeck-runtime-macOS-${ARCH}.tar.gz"
|
||||
ARCHIVE_PATH="${BUILD_DIR}/${ARCHIVE_NAME}"
|
||||
tar -czf "$ARCHIVE_PATH" -C "$STAGING" runtime
|
||||
fi
|
||||
|
||||
SIZE="$(stat -f%z "$ARCHIVE_PATH")"
|
||||
SHA256="$(shasum -a 256 "$ARCHIVE_PATH" | awk '{print $1}')"
|
||||
RUNTIME_URL="${RELEASE_BASE_URL}/${ARCHIVE_NAME}"
|
||||
|
||||
cat > "${BUILD_DIR}/runtime-manifest-${ARCH}.json" <<JSON
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"arch": "${ARCH}",
|
||||
"runtimeUrl": "${RUNTIME_URL}",
|
||||
"runtimeSha256": "${SHA256}",
|
||||
"runtimeSize": ${SIZE},
|
||||
"archiveName": "${ARCHIVE_NAME}"
|
||||
}
|
||||
JSON
|
||||
|
||||
echo "==> Runtime pack ready"
|
||||
echo "Archive: ${ARCHIVE_PATH}"
|
||||
echo "Size: ${SIZE}"
|
||||
echo "SHA256: ${SHA256}"
|
||||
echo "Manifest: ${BUILD_DIR}/runtime-manifest-${ARCH}.json"
|
||||
@@ -0,0 +1,264 @@
|
||||
param(
|
||||
[string]$Configuration = "release",
|
||||
[string]$OutputRoot = "dist",
|
||||
[string]$PackageName = "StemDeck-Windows-x64",
|
||||
[string]$PackageVersion,
|
||||
[switch]$SkipTauriBuild,
|
||||
[switch]$CpuOnly,
|
||||
[switch]$StripVenv
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$PSNativeCommandErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
if ($env:OS -ne "Windows_NT") {
|
||||
throw "This packaging script must run on Windows."
|
||||
}
|
||||
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$Stage = Join-Path $Root "$OutputRoot\$PackageName"
|
||||
$ZipPath = Join-Path $Root "$OutputRoot\$PackageName.zip"
|
||||
$ChecksumPath = "$ZipPath.sha256"
|
||||
$PythonDir = Join-Path $Stage "python"
|
||||
$PythonExe = Join-Path $PythonDir "Scripts\python.exe"
|
||||
$BackendDir = Join-Path $Stage "backend"
|
||||
$DesktopDir = Join-Path $Root "desktop"
|
||||
$TauriDir = Join-Path $DesktopDir "src-tauri"
|
||||
$TargetExe = Join-Path $TauriDir "target\$Configuration\stemdeck.exe"
|
||||
|
||||
function Require-Command([string]$Name) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command not found on PATH: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-Tree([string]$Source, [string]$Destination) {
|
||||
if (Test-Path $Destination) {
|
||||
Remove-Item -Recurse -Force $Destination
|
||||
}
|
||||
Copy-Item -Recurse -Force $Source $Destination
|
||||
}
|
||||
|
||||
function Copy-TreeContents([string]$Source, [string]$Destination, [string[]]$ExcludeNames = @()) {
|
||||
New-Item -ItemType Directory -Force $Destination | Out-Null
|
||||
Get-ChildItem -LiteralPath $Source -Force |
|
||||
Where-Object { $ExcludeNames -notcontains $_.Name } |
|
||||
ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $Destination -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PyvenvValue([string]$ConfigPath, [string]$Key, [string]$Value) {
|
||||
$content = Get-Content -LiteralPath $ConfigPath
|
||||
$pattern = "^\s*$([regex]::Escape($Key))\s*="
|
||||
$line = "$Key = $Value"
|
||||
$found = $false
|
||||
$updated = foreach ($entry in $content) {
|
||||
if ($entry -match $pattern) {
|
||||
$found = $true
|
||||
$line
|
||||
} else {
|
||||
$entry
|
||||
}
|
||||
}
|
||||
if (-not $found) {
|
||||
$updated += $line
|
||||
}
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[System.IO.File]::WriteAllLines($ConfigPath, [string[]]$updated, $utf8NoBom)
|
||||
}
|
||||
|
||||
function Get-PackageVersion {
|
||||
if ($PackageVersion) {
|
||||
return $PackageVersion.TrimStart("v")
|
||||
}
|
||||
$tauriConfig = Get-Content -LiteralPath (Join-Path $TauriDir "tauri.conf.json") -Raw |
|
||||
ConvertFrom-Json
|
||||
return [string]$tauriConfig.version
|
||||
}
|
||||
|
||||
function Bundle-PythonRuntime([string]$VenvDir, [string]$VenvPython) {
|
||||
$baseExecutable = (& $VenvPython -c "import sys; print(getattr(sys, '_base_executable', sys.executable))").Trim()
|
||||
if (-not (Test-Path $baseExecutable)) {
|
||||
throw "Could not locate base Python executable: $baseExecutable"
|
||||
}
|
||||
$baseHome = Split-Path -Parent $baseExecutable
|
||||
$portableBaseHome = Join-Path $VenvDir "base"
|
||||
$baseLib = Join-Path $baseHome "Lib"
|
||||
$baseDlls = Join-Path $baseHome "DLLs"
|
||||
if (-not (Test-Path $baseLib)) {
|
||||
throw "Could not locate base Python standard library: $baseLib"
|
||||
}
|
||||
|
||||
Write-Host "Bundling Python runtime from $baseHome..."
|
||||
New-Item -ItemType Directory -Force $portableBaseHome | Out-Null
|
||||
Copy-Item -Force $baseExecutable (Join-Path $portableBaseHome "python.exe")
|
||||
$basePythonw = Join-Path $baseHome "pythonw.exe"
|
||||
if (Test-Path $basePythonw) {
|
||||
Copy-Item -Force $basePythonw (Join-Path $portableBaseHome "pythonw.exe")
|
||||
}
|
||||
Get-ChildItem -LiteralPath $baseHome -Filter "*.dll" -File -Force |
|
||||
Copy-Item -Destination $portableBaseHome -Force
|
||||
if (Test-Path $baseDlls) {
|
||||
Copy-Tree $baseDlls (Join-Path $portableBaseHome "DLLs")
|
||||
}
|
||||
Copy-TreeContents $baseLib (Join-Path $portableBaseHome "Lib") @("site-packages")
|
||||
|
||||
$cfg = Join-Path $VenvDir "pyvenv.cfg"
|
||||
Set-PyvenvValue $cfg "home" $portableBaseHome
|
||||
Set-PyvenvValue $cfg "executable" (Join-Path $portableBaseHome "python.exe")
|
||||
}
|
||||
|
||||
function Invoke-TauriBuild {
|
||||
$TauriCli = Join-Path $DesktopDir "node_modules\@tauri-apps\cli\tauri.js"
|
||||
if (-not (Test-Path $TauriCli)) {
|
||||
throw "Tauri CLI not found at $TauriCli. npm install/ci may have omitted devDependencies."
|
||||
}
|
||||
& node $TauriCli build
|
||||
}
|
||||
|
||||
function Assert-Fresh-TauriBuild {
|
||||
if (-not (Test-Path $TargetExe)) {
|
||||
throw "Tauri executable not found at $TargetExe. Remove -SkipTauriBuild or build the NVIDIA package first."
|
||||
}
|
||||
|
||||
$exe = Get-Item $TargetExe
|
||||
$newerSources = @(
|
||||
Get-ChildItem -Path (Join-Path $DesktopDir "ui") -File -Recurse
|
||||
Get-ChildItem -Path (Join-Path $TauriDir "src") -File -Recurse
|
||||
Get-Item (Join-Path $TauriDir "Cargo.toml")
|
||||
Get-Item (Join-Path $TauriDir "tauri.conf.json")
|
||||
) | Where-Object { $_.LastWriteTimeUtc -gt $exe.LastWriteTimeUtc }
|
||||
|
||||
if ($newerSources.Count -gt 0) {
|
||||
$list = ($newerSources | Select-Object -First 8 | ForEach-Object { " - $($_.FullName)" }) -join "`n"
|
||||
throw @"
|
||||
-SkipTauriBuild would package a stale StemDeck.exe.
|
||||
|
||||
The existing executable is older than desktop UI/Tauri source files:
|
||||
$list
|
||||
|
||||
Remove -SkipTauriBuild or run the NVIDIA package build first so the CPU package reuses a fresh executable.
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
Require-Command "node"
|
||||
Require-Command "npm"
|
||||
Require-Command "cargo"
|
||||
|
||||
if (-not (Get-Command "py" -ErrorAction SilentlyContinue) -and -not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
||||
throw "Python launcher not found. Install Python 3.12 on the Windows build agent."
|
||||
}
|
||||
|
||||
if (Test-Path $Stage) {
|
||||
Remove-Item -Recurse -Force $Stage
|
||||
}
|
||||
if (Test-Path $ZipPath) {
|
||||
Remove-Item -Force $ZipPath
|
||||
}
|
||||
if (Test-Path $ChecksumPath) {
|
||||
Remove-Item -Force $ChecksumPath
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force $Stage | Out-Null
|
||||
New-Item -ItemType Directory -Force $BackendDir | Out-Null
|
||||
New-Item -ItemType Directory -Force (Join-Path $Stage "data") | Out-Null
|
||||
foreach ($Dir in @("cache", "downloads", "ffmpeg", "jobs", "logs", "models")) {
|
||||
New-Item -ItemType Directory -Force (Join-Path $Stage "data\$Dir") | Out-Null
|
||||
}
|
||||
if ($CpuOnly) {
|
||||
New-Item -ItemType File -Force (Join-Path $Stage "cpu-only") | Out-Null
|
||||
New-Item -ItemType File -Force (Join-Path $Stage "data\cpu-only") | Out-Null
|
||||
}
|
||||
|
||||
Copy-Tree (Join-Path $Root "app") (Join-Path $BackendDir "app")
|
||||
Copy-Tree (Join-Path $Root "static") (Join-Path $BackendDir "static")
|
||||
$PackageVersion = Get-PackageVersion
|
||||
$VersionJson = @{ version = $PackageVersion } | ConvertTo-Json -Compress
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[System.IO.File]::WriteAllText((Join-Path $BackendDir "static\version.json"), $VersionJson + "`n", $utf8NoBom)
|
||||
Copy-Item -Force (Join-Path $Root "pyproject.toml") (Join-Path $BackendDir "pyproject.toml")
|
||||
Copy-Item -Force (Join-Path $Root "uv.lock") (Join-Path $BackendDir "uv.lock")
|
||||
Copy-Item -Force (Join-Path $Root "packaging\windows\README-WINDOWS.txt") (Join-Path $Stage "README-WINDOWS.txt")
|
||||
Copy-Item -Force (Join-Path $Root "packaging\windows\THIRD_PARTY_NOTICES.txt") (Join-Path $Stage "THIRD_PARTY_NOTICES.txt")
|
||||
|
||||
if (Get-Command "py" -ErrorAction SilentlyContinue) {
|
||||
& py -3.12 -m venv $PythonDir
|
||||
} else {
|
||||
& python -m venv $PythonDir
|
||||
}
|
||||
|
||||
& $PythonExe -m pip install --upgrade pip
|
||||
|
||||
# The project version is git-derived (hatch-vcs). Pin it from $PackageVersion so
|
||||
# the install doesn't depend on git tags in the build checkout (#169).
|
||||
if ($PackageVersion) {
|
||||
$env:SETUPTOOLS_SCM_PRETEND_VERSION = ($PackageVersion -replace '^v', '')
|
||||
}
|
||||
& $PythonExe -m pip install "$Root"
|
||||
|
||||
if ($CpuOnly) {
|
||||
# pip strips local version identifiers when resolving requirements, so it installs
|
||||
# the CUDA wheel from PyPI even when we pre-install the CPU wheel. Force-reinstall
|
||||
# after the fact: uninstalls CUDA torch and replaces it with the CPU-only variant.
|
||||
& $PythonExe -m pip install torch==2.6.0+cpu torchaudio==2.6.0+cpu `
|
||||
--index-url https://download.pytorch.org/whl/cpu `
|
||||
--force-reinstall --no-deps
|
||||
}
|
||||
|
||||
& $PythonExe -c "import fastapi, uvicorn, yt_dlp, demucs, torch, torchaudio, librosa, pyloudnorm, soundfile"
|
||||
|
||||
Bundle-PythonRuntime $PythonDir $PythonExe
|
||||
& $PythonExe -c "import sys, fastapi, uvicorn; print('Portable Python:', sys.executable)"
|
||||
|
||||
if ($StripVenv) {
|
||||
Write-Host "Stripping venv of build-time artifacts..."
|
||||
Get-ChildItem -Path $PythonDir -Filter "__pycache__" -Recurse -Directory -Force |
|
||||
Remove-Item -Recurse -Force
|
||||
foreach ($rel in @("torch\include", "torch\share\cmake", "torch\test")) {
|
||||
$p = Join-Path $PythonDir "Lib\site-packages\$rel"
|
||||
if (Test-Path $p) { Remove-Item -Recurse -Force $p }
|
||||
}
|
||||
# Remove C++ static link libraries from torch — needed only for building C++ extensions,
|
||||
# never for running Python. dnnl.lib alone is ~623 MB.
|
||||
Get-ChildItem -Path (Join-Path $PythonDir "Lib\site-packages\torch") `
|
||||
-Filter "*.lib" -Recurse -File -Force |
|
||||
Remove-Item -Force
|
||||
}
|
||||
|
||||
Push-Location $DesktopDir
|
||||
try {
|
||||
if (Test-Path "package-lock.json") {
|
||||
npm ci --include=dev
|
||||
} else {
|
||||
npm install --include=dev
|
||||
}
|
||||
|
||||
if (-not $SkipTauriBuild) {
|
||||
$env:CI = "true" # Woodpecker sets CI=woodpecker; Tauri only accepts true/false
|
||||
rustup default stable
|
||||
Invoke-TauriBuild
|
||||
} else {
|
||||
Assert-Fresh-TauriBuild
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TargetExe)) {
|
||||
throw "Tauri executable not found at $TargetExe"
|
||||
}
|
||||
|
||||
Copy-Item -Force $TargetExe (Join-Path $Stage "StemDeck.exe")
|
||||
|
||||
Compress-Archive -Path (Join-Path $Stage "*") -DestinationPath $ZipPath -Force
|
||||
$Hash = Get-FileHash -Algorithm SHA256 $ZipPath
|
||||
Set-Content -Path $ChecksumPath -Value "$($Hash.Hash) $PackageName.zip"
|
||||
|
||||
$Variant = if ($CpuOnly) { "CPU-only" } else { "CUDA/GPU (NVIDIA)" }
|
||||
Write-Host "Variant : $Variant"
|
||||
Write-Host "Staged at : $Stage"
|
||||
Write-Host "Zip created : $ZipPath"
|
||||
Write-Host "Checksum : $ChecksumPath"
|
||||
Reference in New Issue
Block a user