feedBack-desktop/scripts/build-windows.sh
2026-06-16 18:48:12 +02:00

277 lines
12 KiB
Bash
Executable File

#!/bin/bash
# Native Windows build script
# Runs in Git Bash (Git for Windows)
set -euo pipefail
# Use plain `pwd` (POSIX path). `pwd -W` returns a Windows-form path
# with backslashes on MSYS / Git Bash, which would then break `dirname`
# and `source "$SCRIPT_DIR/build-common.sh"` since those expect POSIX
# paths. If a Windows-form path is needed downstream (e.g. for cmake-js
# or a non-MSYS tool), convert at the point of use via
# `cygpath -w "$SCRIPT_DIR"`.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG="$PROJECT_DIR/.build-config.json"
# Platform identifier
export PLATFORM="windows"
# Check for Git Bash/MSYS
if [[ "$OSTYPE" != "msys" ]] && [[ "$OSTYPE" != "win32" ]] && [[ -z "${MSYSTEM:-}" ]]; then
echo "Error: This script must be run in Git Bash (Git for Windows)" >&2
echo "Download: https://git-scm.com/download/win" >&2
exit 1
fi
echo "=== Slopsmith Desktop Windows Build ==="
echo ""
# Color setup
export RED='\033[0;31m'
export GREEN='\033[0;32m'
export YELLOW='\033[1;33m'
export BLUE='\033[0;34m'
export NC='\033[0m'
# Source common build logic
source "$SCRIPT_DIR/build-common.sh"
# Platform-specific: Return expected artifact patterns
# Windows target is now "dir" (electron-builder unpacked only) because Velopack
# generates the installer via `vpk pack` on tagged CI runs. For non-tag builds
# (PR validation, main-branch pushes) only the unpacked dir is produced.
get_expected_artifacts() {
printf "%s\n" "$PROJECT_DIR/release/win-unpacked/*.exe"
}
# Platform-specific: Install system dependencies
install_system_deps() {
# Windows: install via Chocolatey if available
if command -v choco.exe &>/dev/null || command -v choco &>/dev/null; then
choco install cmake ffmpeg -y --installargs 'ADD_CMAKE_TO_PATH=System' || echo "Chocolatey install may have failed, continuing..."
else
echo_warning "Chocolatey not found - skipping system package installation"
echo " Make sure cmake and ffmpeg are already in PATH"
fi
}
# Platform-specific: Bundle Python runtime
bundle_python_impl() {
# Windows: download embeddable Python
PYTHON_VERSION=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .versions.python)
PYTHON_MAJOR="${PYTHON_VERSION%%.*}"
PYTHON_MINOR="${PYTHON_VERSION#*.}"
PYTHON_EMBED_URL="https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip"
echo "Downloading Python embeddable..."
local curl_status=0
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$PYTHON_EMBED_URL" -o /tmp/python-embed.zip || curl_status=$?
if [[ "$curl_status" -ne 0 ]]; then
echo_error "Failed to download Python embeddable package"
echo " URL: $PYTHON_EMBED_URL"
echo " curl exit code: $curl_status"
exit 1
fi
# Wipe the existing python dir before extracting so a re-run doesn't
# leave stale files (e.g. an old ._pth from a previous Python version)
# mixed in with the freshly-extracted embeddable distribution.
rm -rf "$PROJECT_DIR/resources/python"
mkdir -p "$PROJECT_DIR/resources/python"
unzip -q /tmp/python-embed.zip -d "$PROJECT_DIR/resources/python/"
# Enable site-packages by editing the ._pth file
# IMPORTANT: On Windows embeddable Python, PYTHONPATH environment variable is IGNORED
# when a ._pth file exists (isolated mode). We must add paths directly to the .pth file.
# The embeddable zip is supposed to ship a ._pth file; if it doesn't,
# the rest of the script's PATH-injection won't work, so fail fast.
PTH_FILE=$(find "$PROJECT_DIR/resources/python" -name "*._pth" | head -1)
if [[ -z "$PTH_FILE" ]]; then
echo_error "No ._pth file found in extracted embeddable Python — upstream zip layout may have changed"
exit 1
fi
if [[ -n "$PTH_FILE" ]]; then
# Enable site-packages
sed -i 's/#import site/import site/' "$PTH_FILE"
echo "Lib/site-packages" >> "$PTH_FILE"
# Add Slopsmith paths (relative to resources/python)
# These must be in the .pth file since PYTHONPATH is ignored in isolated mode
echo "# Slopsmith modules (relative to resources/python)" >> "$PTH_FILE"
echo "../slopsmith" >> "$PTH_FILE"
echo "../slopsmith/lib" >> "$PTH_FILE"
fi
# Install pip
echo "Downloading pip..."
if ! curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors \
https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py; then
echo_error "Failed to download pip installer"
exit 1
fi
"$PROJECT_DIR/resources/python/python.exe" /tmp/get-pip.py --quiet --no-cache-dir
# Install packages
# Install build tools first (required for building from source on Windows embeddable Python)
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
setuptools wheel
# Install slopsmith runtime requirements (single source of truth —
# drift used to silently break desktop builds whenever slopsmith added
# a dep), then desktop-only extras. SLOPSMITH_DIR is exported by
# clone_slopsmith() in build-common.sh; fall back to local-dev paths
# to match bundle-slopsmith.sh's discovery so this works outside CI.
if [[ -z "${SLOPSMITH_DIR:-}" ]]; then
if [[ -d "$PROJECT_DIR/../slopsmith" ]]; then
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
elif [[ -d "$HOME/Repositories/slopsmith" ]]; then
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
fi
fi
if [[ -z "${SLOPSMITH_DIR:-}" ]] || [[ ! -f "$SLOPSMITH_DIR/requirements.txt" ]]; then
echo "ERROR: slopsmith requirements.txt not found (SLOPSMITH_DIR=${SLOPSMITH_DIR:-<unset>})." >&2
echo " Expected SLOPSMITH_DIR to be exported by clone_slopsmith() in build-common.sh," >&2
echo " or slopsmith cloned next to this repo." >&2
exit 1
fi
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
-r "$SLOPSMITH_DIR/requirements.txt"
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
-r "$PROJECT_DIR/.packages/python.txt"
}
# Usage: download_with_retries <url> <output_path> <description>
download_with_retries() {
local url="$1"
local output_path="$2"
local description="$3"
local max_attempts=3
local attempt=1
local delay=10
while [[ $attempt -le $max_attempts ]]; do
echo " Downloading $description (attempt $attempt/$max_attempts)..."
if curl -sL --fail --max-time 120 "$url" -o "$output_path"; then
echo " Successfully downloaded $description"
return 0
fi
local exit_code=$?
echo " Download failed with exit code $exit_code"
if [[ $attempt -lt $max_attempts ]]; then
echo " Retrying in ${delay}s..."
sleep $delay
delay=$((delay * 2))
fi
attempt=$((attempt + 1))
done
echo_error "Failed to download $description after $max_attempts attempts"
return 1
}
# Platform-specific: Bundle system binaries
# These binaries are REQUIRED for core functionality. The build will fail
# if downloads don't succeed after multiple retry attempts.
bundle_binaries_impl() {
mkdir -p "$PROJECT_DIR/resources/bin"
# ffmpeg static build
echo "Downloading ffmpeg..."
if ! download_with_retries \
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" \
"/tmp/ffmpeg.zip" \
"ffmpeg"; then
exit 1
fi
unzip -q /tmp/ffmpeg.zip -d /tmp/ffmpeg
# Validate the expected layout instead of `cp ... || true` — a broken
# / changed zip layout would otherwise drop `ffmpeg.exe` silently and
# surface as a less-direct error in `verify_bundled_binaries`.
FFMPEG_BIN=$(find /tmp/ffmpeg -name 'ffmpeg.exe' -type f | head -1)
if [[ -z "$FFMPEG_BIN" ]]; then
echo_error "ffmpeg.exe not found after extracting /tmp/ffmpeg.zip — upstream zip layout may have changed"
exit 1
fi
cp "$FFMPEG_BIN" "$PROJECT_DIR/resources/bin/"
# Sloppak conversion encodes .ogg with -c:a libvorbis. The BtbN GPL
# build typically ships libvorbis, but verify so a future upstream
# change doesn't silently degrade users to the built-in vorbis
# encoder. The lib/sloppak_convert.py fallback is a safety net for
# unbundled installs, not a license to ship a libvorbis-less binary.
if ! "$PROJECT_DIR/resources/bin/ffmpeg.exe" -hide_banner -encoders 2>/dev/null | grep -wq libvorbis; then
echo_error "bundled ffmpeg lacks libvorbis encoder. Sloppak conversion would fall back to the lower-quality built-in vorbis encoder on user machines."
echo_error "BtbN's GPL build no longer includes --enable-libvorbis; pick a different release asset (or earlier build) that ships it."
exit 1
fi
# ffprobe ships in the same BtbN zip as ffmpeg. demucs's audio loader
# spawns ffprobe before ffmpeg to read stream metadata; without it the
# loader dies with FileNotFoundError before ffmpeg is ever invoked.
FFPROBE_BIN=$(find /tmp/ffmpeg -name 'ffprobe.exe' -type f | head -1)
if [[ -z "$FFPROBE_BIN" ]]; then
echo_error "ffprobe.exe not found after extracting /tmp/ffmpeg.zip — upstream zip layout may have changed"
exit 1
fi
cp "$FFPROBE_BIN" "$PROJECT_DIR/resources/bin/"
# vgmstream-cli
echo "Downloading vgmstream-cli..."
if ! download_with_retries \
"https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win64.zip" \
"/tmp/vgmstream.zip" \
"vgmstream-cli"; then
exit 1
fi
unzip -q /tmp/vgmstream.zip -d /tmp/vgmstream
VGMSTREAM_EXE="/tmp/vgmstream/vgmstream-cli.exe"
shopt -s nullglob
VGMSTREAM_DLLS=("/tmp/vgmstream"/*.dll)
shopt -u nullglob
if [[ ! -f "$VGMSTREAM_EXE" ]]; then
echo_error "vgmstream-cli.exe not found at $VGMSTREAM_EXE after extraction"
exit 1
fi
if [[ ${#VGMSTREAM_DLLS[@]} -eq 0 ]]; then
echo_error "Expected vgmstream DLLs in /tmp/vgmstream after extraction but found none"
exit 1
fi
cp "$VGMSTREAM_EXE" "$PROJECT_DIR/resources/bin/"
cp "${VGMSTREAM_DLLS[@]}" "$PROJECT_DIR/resources/bin/"
# fluidsynth
echo "Downloading fluidsynth..."
# Reuse parse-build-config.py — Git Bash auto-converts the MSYS-style
# $CONFIG path to native Windows form when it's passed as an arg, but
# NOT when it's interpolated into an inline `python -c "...open('$CONFIG')..."`
# string. The previous inline form silently failed with set -e on
# Windows because Python opened a "/d/a/.../config.json" path that
# doesn't exist as a literal Windows path.
FS_URL=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.fluidsynth_windows.url 2>/dev/null || true)
if [[ -n "$FS_URL" ]]; then
if ! download_with_retries \
"$FS_URL" \
"/tmp/fluidsynth.zip" \
"fluidsynth"; then
exit 1
fi
unzip -q /tmp/fluidsynth.zip -d /tmp/fluidsynth
FS_BIN=$(find /tmp/fluidsynth -name 'fluidsynth.exe' -type f | head -1)
if [[ -n "$FS_BIN" ]]; then
cp "$FS_BIN" "$PROJECT_DIR/resources/bin/"
cp "$(dirname "$FS_BIN")"/*.dll "$PROJECT_DIR/resources/bin/" 2>/dev/null || true
fi
else
echo_error "Fluidsynth URL not found in build config"
exit 1
fi
echo_summary "All required Windows binaries downloaded and installed"
}
# Run the build
main "$@"