diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 374418e..7e8f256 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -35,6 +35,7 @@ jobs: |----------|------| | Windows (installer) | \`MediaServer-{tag}-setup.exe\` | | Windows (portable) | \`MediaServer-{tag}-win-x64.zip\` | + | Linux | \`MediaServer-{tag}-linux-x64.tar.gz\` | ''' print(json.dumps(textwrap.dedent(body).strip())) ") @@ -101,3 +102,41 @@ jobs: -H "Content-Type: application/octet-stream" \ --data-binary "@$FILE" done + + # --- Build Linux tarball --- + build-linux: + needs: create-release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build frontend + run: npm ci && npm run build + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Build Linux distribution + run: | + chmod +x build-dist-linux.sh + ./build-dist-linux.sh "${{ gitea.ref_name }}" + + - name: Upload assets to release + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + RELEASE_ID="${{ needs.create-release.outputs.release_id }}" + BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + + FILE=$(ls build/MediaServer-*-linux-x64.tar.gz | head -1) + echo "Uploading $(basename "$FILE")..." + curl -s -X POST \ + "$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$FILE" diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index c625967..972bdc7 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -32,4 +32,4 @@ jobs: run: ruff check media_server/ - name: Test - run: pytest --tb=short -q + run: pytest --tb=short -q || test $? -eq 5 diff --git a/build-dist-linux.sh b/build-dist-linux.sh new file mode 100644 index 0000000..d64d4f2 --- /dev/null +++ b/build-dist-linux.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build Linux distribution (self-contained venv + tarball) +# Usage: ./build-dist-linux.sh [VERSION] + +# --- Version detection --- +VERSION="${1:-}" + +if [ -z "$VERSION" ]; then + VERSION=$(git describe --tags --exact-match 2>/dev/null || true) +fi + +if [ -z "$VERSION" ]; then + VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}" +fi + +if [ -z "$VERSION" ]; then + VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' \ + media_server/__init__.py 2>/dev/null || echo "0.0.0") +fi + +VERSION_CLEAN="${VERSION#v}" +echo "Building Media Server v${VERSION_CLEAN} for Linux" + +# --- Configuration --- +DIST_DIR="dist/media-server" +BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-linux-x64" + +rm -rf dist build +mkdir -p "${DIST_DIR}" build + +# --- Verify frontend bundle --- +if [ ! -f "media_server/static/dist/app.bundle.js" ]; then + echo "ERROR: Frontend bundle not found. Run 'npm ci && npm run build' first." + exit 1 +fi + +# --- Create self-contained virtualenv --- +echo "Creating virtualenv..." +python3 -m venv "${DIST_DIR}/venv" +source "${DIST_DIR}/venv/bin/activate" +pip install --quiet --upgrade pip +pip install --quiet ".[visualizer]" + +# 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 + +# --- Copy application --- +echo "Copying application files..." +mkdir -p "${DIST_DIR}/app" +cp -r media_server "${DIST_DIR}/app/" + +# Remove source JS (bundle is in dist/) +rm -rf "${DIST_DIR}/app/media_server/static/js" +# Remove source maps from release +rm -f "${DIST_DIR}/app/media_server/static/dist/"*.map + +# Copy config example +cp config.example.yaml "${DIST_DIR}/" + +# --- Write version --- +echo "$VERSION_CLEAN" > "${DIST_DIR}/VERSION" + +# --- 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 --- +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}" + +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 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +EOF + +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}" +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"