#!/usr/bin/env bash set -euo pipefail # Build Linux distribution (self-contained venv + tarball) # Usage: ./build-dist-linux.sh [VERSION] source "$(dirname "$0")/build-common.sh" detect_version "${1:-}" echo "Building Media Server v${VERSION_CLEAN} for Linux" # --- Configuration --- DIST_DIR="dist/media-server" BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-linux-x64" clean_dist "${DIST_DIR}" build verify_frontend # --- Sanity check: native deps required for dbus-python + PyGObject --- # These compile from sdist on Linux. We don't try to install them via the # distro package manager here (the CI image must already have them); fail # with a clear message instead of cryptic gcc/pkg-config errors deep # inside pip. need_pkg() { local pkg="$1" if ! command -v pkg-config >/dev/null 2>&1; then echo "ERROR: pkg-config is required (apt: pkg-config / dnf: pkgconf-pkg-config)" >&2 exit 1 fi if ! pkg-config --exists "$pkg" 2>/dev/null; then echo "ERROR: $pkg headers missing — install libdbus-1-dev libglib2.0-dev (apt)" >&2 echo " or dbus-devel glib2-devel (dnf) before running this script." >&2 exit 1 fi } need_pkg dbus-1 need_pkg glib-2.0 # --- Create self-contained virtualenv --- echo "Creating virtualenv..." python3 -m venv "${DIST_DIR}/venv" source "${DIST_DIR}/venv/bin/activate" pip install --quiet --upgrade pip # Install with the linux extra so dbus-python / PyGObject / python-xlib land # in the venv. Falls back to base deps if the extra is unsupported (shouldn't # happen post-0.3.2 but keep the fallback for older tags re-built here). if ! pip install --quiet ".[linux]"; then echo "WARN: '.[linux]' extra unavailable; installing base deps only" >&2 pip install --quiet "." fi # Remove the installed package (app source is on PYTHONPATH via launcher) rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server* rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*.dist-info deactivate # Trim venv site-packages LINUX_SP=$(echo "${DIST_DIR}"/venv/lib/python*/site-packages) cleanup_site_packages "$LINUX_SP" "so" "so" copy_app_files "$DIST_DIR" # --- Create launcher --- cat > "${DIST_DIR}/media-server.sh" << 'LAUNCHER' #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" export PYTHONPATH="$SCRIPT_DIR/app" source "$SCRIPT_DIR/venv/bin/activate" exec python -m media_server.main "$@" LAUNCHER chmod +x "${DIST_DIR}/media-server.sh" # --- Create systemd service installer --- # Emits a non-templated unit that runs as the invoking user with the env # bits MPRIS / PulseAudio need: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR # must be set or DBus.SessionBus() either fails or talks to the wrong bus. # We also relax ProtectHome to read-write for ~/.config/media-server + # ~/.cache/media-server so audit.log and thumbnail cache writes succeed. cat > "${DIST_DIR}/install-service.sh" << 'SERVICE' #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SERVICE_NAME="media-server" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" if [ "$EUID" -ne 0 ]; then echo "Please run with sudo: sudo ./install-service.sh" exit 1 fi REAL_USER="${SUDO_USER:-$USER}" USER_UID="$(id -u "$REAL_USER")" USER_HOME="$(getent passwd "$REAL_USER" | cut -d: -f6)" RUNTIME_DIR="/run/user/${USER_UID}" cat > "$SERVICE_FILE" << EOF [Unit] Description=Media Server After=network.target sound.target [Service] Type=simple User=${REAL_USER} WorkingDirectory=${SCRIPT_DIR} ExecStart=${SCRIPT_DIR}/media-server.sh Restart=on-failure RestartSec=5 # Required by MPRIS (dbus.SessionBus) and PulseAudio/PipeWire loopback. Environment=PYTHONUNBUFFERED=1 Environment=HOME=${USER_HOME} Environment=XDG_RUNTIME_DIR=${RUNTIME_DIR} Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=${RUNTIME_DIR}/bus # Light sandboxing — keep ~/.config/media-server + ~/.cache/media-server # writable so audit.log / thumbnails / config CRUD continue to work. NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=${USER_HOME}/.config/media-server ${USER_HOME}/.cache/media-server [Install] WantedBy=multi-user.target EOF # Pre-create state dirs so ReadWritePaths actually has something to grant. sudo -u "$REAL_USER" mkdir -p \ "${USER_HOME}/.config/media-server" \ "${USER_HOME}/.cache/media-server" systemctl daemon-reload systemctl enable "${SERVICE_NAME}" systemctl start "${SERVICE_NAME}" echo "Service '${SERVICE_NAME}' installed and started." echo "Check status: systemctl status ${SERVICE_NAME}" echo "Tail logs: journalctl -u ${SERVICE_NAME} -f" SERVICE chmod +x "${DIST_DIR}/install-service.sh" # --- Package --- echo "Creating archive..." cp -r "${DIST_DIR}" "${BUILD_OUTPUT}" tar -czf "${BUILD_OUTPUT}.tar.gz" -C build "MediaServer-v${VERSION_CLEAN}-linux-x64" echo "Build complete: ${BUILD_OUTPUT}.tar.gz"