456eb3a881
- Generate numpy/_distributor_init_local.py during build so libopenblas can be located when running from the Windows installer - Add os.add_dll_directory() call at runtime as a fallback for embedded Python
166 lines
5.6 KiB
Bash
166 lines
5.6 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Cross-build Windows distribution on Linux
|
|
# Usage: ./build-dist-windows.sh [VERSION]
|
|
|
|
source "$(dirname "$0")/build-common.sh"
|
|
|
|
detect_version "${1:-}"
|
|
echo "Building Media Server v${VERSION_CLEAN} for Windows"
|
|
|
|
# --- Configuration ---
|
|
PYTHON_VERSION="3.11.9"
|
|
PYTHON_SHORT="311"
|
|
DIST_DIR="dist/media-server"
|
|
WHEEL_DIR="build/win-wheels"
|
|
SITE_PACKAGES="${DIST_DIR}/python/Lib/site-packages"
|
|
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
|
|
|
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
|
|
|
# --- Download embedded Python (cache-friendly) ---
|
|
mkdir -p build
|
|
if [ ! -f build/python-embed.zip ]; then
|
|
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
|
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
|
|
-o build/python-embed.zip
|
|
else
|
|
echo "Using cached embedded Python ${PYTHON_VERSION}"
|
|
fi
|
|
unzip -qo build/python-embed.zip -d "${DIST_DIR}/python"
|
|
|
|
# Patch ._pth to enable site-packages and app source
|
|
PTH_FILE=$(ls "${DIST_DIR}"/python/python*._pth | head -1)
|
|
sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
|
|
echo 'Lib\site-packages' >> "$PTH_FILE"
|
|
echo '..\app' >> "$PTH_FILE"
|
|
|
|
# --- Download Windows wheels ---
|
|
echo "Downloading Windows wheels..."
|
|
|
|
# Core dependencies
|
|
# NOTE: uvicorn[standard] pulls in uvloop via `sys_platform != 'win32'` marker.
|
|
# pip evaluates env markers against the HOST (Linux in CI), so uvloop is
|
|
# requested, but `--platform win_amd64 --only-binary :all:` cannot find a
|
|
# Windows wheel for uvloop (none exist). Result: pip backtracks across every
|
|
# uvicorn[standard] version and ResolutionImpossible. Fix: use plain uvicorn
|
|
# and list only the Windows-compatible standard extras we actually need.
|
|
CORE_DEPS=(
|
|
"fastapi>=0.109.0"
|
|
"uvicorn>=0.27.0"
|
|
"httptools>=0.5.0"
|
|
"websockets>=10.4"
|
|
"python-dotenv>=0.13"
|
|
"pydantic>=2.0"
|
|
"pydantic-settings>=2.0"
|
|
"pyyaml>=6.0"
|
|
"mutagen>=1.47.0"
|
|
"pillow>=10.0.0"
|
|
)
|
|
|
|
# Windows-specific dependencies
|
|
# NOTE: wmi is a transitive dep of screen-brightness-control gated on
|
|
# `platform_system == "Windows"`. pip evaluates env markers against the HOST
|
|
# (Linux in CI), so it gets skipped during cross-build. Listed explicitly here
|
|
# so the wheel actually lands in the Windows bundle. Same gotcha as the
|
|
# uvicorn[standard]/uvloop case documented above.
|
|
WIN_DEPS=(
|
|
"winsdk>=1.0.0b10"
|
|
"pywin32>=306"
|
|
"comtypes>=1.2.0"
|
|
"pycaw>=20230407"
|
|
"screen-brightness-control>=0.20.0"
|
|
"wmi>=1.5.1"
|
|
"monitorcontrol>=3.0.0"
|
|
)
|
|
|
|
# Visualizer dependencies
|
|
VIS_DEPS=(
|
|
"soundcard>=0.4.0"
|
|
"numpy>=1.24.0,<2.0"
|
|
# pystray lives here (not WIN_DEPS) so its transitive Pillow resolves in the
|
|
# same pass as CORE_DEPS. Keeping it in the per-dep WIN_DEPS loop downloaded
|
|
# a second Pillow version that clobbered the core one on unzip, producing
|
|
# "_imaging extension was built for another version of Pillow" at runtime.
|
|
"pystray>=0.19.0"
|
|
)
|
|
|
|
# Resolve core + visualizer deps in a SINGLE call so pip picks compatible
|
|
# transitive versions (notably pydantic/pydantic-core must match).
|
|
# Per-dep loops resolve each dep independently and can leave mismatched
|
|
# transitive versions that overwrite each other in the site-packages unzip.
|
|
CROSS_DEPS=("${CORE_DEPS[@]}" "${VIS_DEPS[@]}")
|
|
pip download --quiet --no-cache-dir --dest "$WHEEL_DIR" \
|
|
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
|
|
--implementation cp --only-binary :all: \
|
|
"${CROSS_DEPS[@]}"
|
|
|
|
# Windows-only deps in a loop with --pre: winsdk only ships beta wheels
|
|
# (1.0.0bNN) and each dep needs its own platform/non-platform fallback.
|
|
for dep in "${WIN_DEPS[@]}"; do
|
|
pip download --pre --quiet --no-cache-dir --dest "$WHEEL_DIR" \
|
|
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
|
|
--implementation cp --only-binary :all: \
|
|
"$dep" 2>/dev/null || \
|
|
pip download --pre --quiet --no-cache-dir --dest "$WHEEL_DIR" \
|
|
--only-binary :all: \
|
|
"$dep"
|
|
done
|
|
|
|
# Remove numpy 2.x wheels pulled as transitive deps (soundcard requires <2.0)
|
|
for f in "$WHEEL_DIR"/numpy-2*; do
|
|
[ -f "$f" ] && echo "Removing incompatible: $(basename "$f")" && rm "$f"
|
|
done
|
|
|
|
# Install wheels into site-packages
|
|
echo "Installing wheels..."
|
|
for whl in "$WHEEL_DIR"/*.whl; do
|
|
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
|
done
|
|
|
|
# numpy wheels from PyPI don't include _distributor_init_local.py unless
|
|
# patched by delvewheel. In embedded Python, os.add_dll_directory() is never
|
|
# called, so libopenblas can't be found and numpy fails to import.
|
|
# Generate the missing loader here instead.
|
|
if [ -d "${SITE_PACKAGES}/numpy" ]; then
|
|
cat > "${SITE_PACKAGES}/numpy/_distributor_init_local.py" << 'EOF'
|
|
import os
|
|
import sys
|
|
if sys.platform == 'win32':
|
|
_libs = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'numpy.libs'))
|
|
if os.path.isdir(_libs):
|
|
os.add_dll_directory(_libs)
|
|
EOF
|
|
echo "Generated numpy/_distributor_init_local.py"
|
|
fi
|
|
|
|
cleanup_site_packages "$SITE_PACKAGES" "pyd" "dll"
|
|
verify_frontend
|
|
copy_app_files "$DIST_DIR"
|
|
|
|
# Copy scripts needed for auto-start
|
|
mkdir -p "${DIST_DIR}/scripts"
|
|
cp scripts/start-hidden.vbs "${DIST_DIR}/scripts/"
|
|
|
|
# --- Create launcher ---
|
|
cat > "${DIST_DIR}/media-server.bat" << 'LAUNCHER'
|
|
@echo off
|
|
setlocal
|
|
set "ROOT=%~dp0"
|
|
"%ROOT%python\python.exe" -m media_server.main %*
|
|
LAUNCHER
|
|
|
|
# --- Package ---
|
|
echo "Creating archives..."
|
|
mkdir -p build
|
|
|
|
# Portable ZIP
|
|
cp -r "${DIST_DIR}" "${BUILD_OUTPUT}"
|
|
cd build
|
|
zip -qr "MediaServer-v${VERSION_CLEAN}-win-x64.zip" "MediaServer-v${VERSION_CLEAN}-win-x64"
|
|
cd ..
|
|
|
|
echo "Build complete: build/MediaServer-v${VERSION_CLEAN}-win-x64.zip"
|
|
echo "Dist directory ready for NSIS: ${DIST_DIR}"
|