---
name: CI

on:
  pull_request:
    branches: [master, nightly]
    types: [opened, synchronize, reopened]
  push:
    branches: [master, nightly]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  github_env:
    name: GitHub Env Debug
    runs-on: ubuntu-latest

    steps:
      - name: Dump github context
        run: echo "$GITHUB_CONTEXT"
        shell: bash
        env:
          GITHUB_CONTEXT: ${{ toJson(github) }}

  check_changelog:
    name: Check Changelog
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }}
        uses: actions/checkout@v4

      - name: Verify Changelog
        id: verify_changelog
        if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }}
        # base_ref for pull request check, ref for push
        uses: LizardByte/.github/actions/verify_changelog@master
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
    outputs:
      next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }}
      next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }}
      last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }}
      release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }}

  # todo - remove this job once versioning is fully automated by cmake
  check_versions:
    name: Check Versions
    runs-on: ubuntu-latest
    needs: check_changelog
    if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }}
    # base_ref for pull request check, ref for push
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Check CMakeLists.txt Version
        run: |
          version=$(grep -o -E '^project\(Sunshine VERSION [0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt | \
            grep -o -E '[0-9]+\.[0-9]+\.[0-9]+')
          echo "cmakelists_version=${version}" >> $GITHUB_ENV

      - name: Compare CMakeList.txt Version
        if: ${{ env.cmakelists_version != needs.check_changelog.outputs.next_version_bare }}
        run: |
          echo CMakeLists version: "$cmakelists_version"
          echo Changelog version: "${{ needs.check_changelog.outputs.next_version_bare }}"
          echo Within 'CMakeLists.txt' change "project(Sunshine [VERSION $cmakelists_version]" to \
            "project(Sunshine [VERSION ${{ needs.check_changelog.outputs.next_version_bare }}]"
          exit 1

  setup_release:
    name: Setup Release
    needs: check_changelog
    runs-on: ubuntu-latest
    steps:
      - name: Set release details
        id: release_details
        env:
          RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }}
        run: |
          # determine to create a release or not
          if [[ $GITHUB_EVENT_NAME == "push" ]]; then
            RELEASE=true
          else
            RELEASE=false
          fi

          # set the release tag
          COMMIT=${{ github.sha }}
          if [[ $GITHUB_REF == refs/heads/master ]]; then
            TAG="${{ needs.check_changelog.outputs.next_version }}"
            RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}"
            RELEASE_BODY="$RELEASE_BODY"
            PRE_RELEASE="false"
          elif [[ $GITHUB_REF == refs/heads/nightly ]]; then
            TAG="nightly-dev"
            RELEASE_NAME="nightly"
            RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}"
            PRE_RELEASE="true"
          fi

          echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT
          echo "release_tag=${TAG}" >> $GITHUB_OUTPUT
          echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT
          echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT
          echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT

          # this is stupid but works for multiline strings
          echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
          echo "$RELEASE_BODY" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

    outputs:
      create_release: ${{ steps.release_details.outputs.create_release }}
      release_tag: ${{ steps.release_details.outputs.release_tag }}
      release_commit: ${{ steps.release_details.outputs.release_commit }}
      release_name: ${{ steps.release_details.outputs.release_name }}
      release_body: ${{ env.RELEASE_BODY }}
      pre_release: ${{ steps.release_details.outputs.pre_release }}

  setup_flatpak_matrix:
    name: Setup Flatpak Matrix
    runs-on: ubuntu-latest
    steps:
      - name: Set release details
        id: flatpak_matrix
        # https://www.cynkra.com/blog/2020-12-23-dynamic-gha
        run: |
          # determine which architectures to build
          if [[ $GITHUB_EVENT_NAME == "push" ]]; then
            matrix=$((
              echo '{ "arch" : ["x86_64", "aarch64"] }'
            ) | jq -c .)
          else
            matrix=$((
              echo '{ "arch" : ["x86_64"] }'
            ) | jq -c .)
          fi

          echo $matrix
          echo $matrix | jq .
          echo "matrix=$matrix" >> $GITHUB_OUTPUT

    outputs:
      matrix: ${{ steps.flatpak_matrix.outputs.matrix }}

  build_linux_flatpak:
    name: Linux Flatpak
    runs-on: ubuntu-22.04
    needs: [setup_release, setup_flatpak_matrix]
    strategy:
      fail-fast: false  # false to test all, true to fail entire job if any fail
      matrix: ${{fromJson(needs.setup_flatpak_matrix.outputs.matrix)}}

    steps:
      - name: Maximize build space
        uses: easimon/maximize-build-space@v8
        with:
          root-reserve-mb: 10240
          remove-dotnet: 'true'
          remove-android: 'true'
          remove-haskell: 'true'
          remove-codeql: 'true'
          remove-docker-images: 'true'

      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Setup Dependencies Linux Flatpak
        run: |
          PLATFORM_VERSION=22.08

          sudo apt-get update -y
          sudo apt-get install -y \
            cmake \
            flatpak \
            qemu-user-static

          sudo su $(whoami) -c "flatpak --user remote-add --if-not-exists flathub \
            https://flathub.org/repo/flathub.flatpakrepo"

          sudo su $(whoami) -c "flatpak --user install -y flathub \
            org.flatpak.Builder \
            org.freedesktop.Platform/${{ matrix.arch }}/${PLATFORM_VERSION} \
            org.freedesktop.Sdk/${{ matrix.arch }}/${PLATFORM_VERSION} \
            org.freedesktop.Sdk.Extension.node18/${{ matrix.arch }}/${PLATFORM_VERSION} \
            org.freedesktop.Sdk.Extension.vala/${{ matrix.arch }}/${PLATFORM_VERSION} \
            "

      - name: Cache Flatpak build
        uses: actions/cache@v3
        with:
          path: ./build/.flatpak-builder
          key: flatpak-${{ matrix.arch }}-${{ github.sha }}
          restore-keys: |
            flatpak-${{ matrix.arch }}-

      - name: Configure Flatpak Manifest
        run: |
          # variables for manifest
          branch=${GITHUB_HEAD_REF}

          # check the branch variable
          if [ -z "$branch" ]
          then
            echo "This is a PUSH event"
            branch=${{ github.ref_name }}
            build_version=${{ needs.check_changelog.outputs.next_version }}
            commit=${{ github.sha }}
            clone_url=${{ github.event.repository.clone_url }}
          else
            echo "This is a PR event"
            commit=${{ github.event.pull_request.head.sha }}
            clone_url=${{ github.event.pull_request.head.repo.clone_url }}
          fi
          echo "Branch: ${branch}"
          echo "Commit: ${commit}"
          echo "Clone URL: ${clone_url}"

          mkdir -p build
          mkdir -p artifacts

          cd build
          cmake -DGITHUB_CLONE_URL=${clone_url} \
            -DBUILD_VERSION=${build_version} \
            -DGITHUB_BRANCH=${branch} \
            -DGITHUB_COMMIT=${commit} \
            -DSUNSHINE_CONFIGURE_FLATPAK_MAN=ON \
            -DSUNSHINE_CONFIGURE_ONLY=ON \
            ..

      - name: Build Linux Flatpak
        working-directory: build
        run: |
          sudo su $(whoami) -c 'flatpak run org.flatpak.Builder --arch=${{ matrix.arch }} --repo=repo --force-clean \
            --stop-at=cuda build-sunshine dev.lizardbyte.sunshine.yml'
          cp -r .flatpak-builder copy-of-flatpak-builder
          sudo su $(whoami) -c 'flatpak run org.flatpak.Builder --arch=${{ matrix.arch }} --repo=repo --force-clean \
            build-sunshine dev.lizardbyte.sunshine.yml'
          rm -rf .flatpak-builder
          mv copy-of-flatpak-builder .flatpak-builder
          sudo su $(whoami) -c 'flatpak build-bundle --arch=${{ matrix.arch }} ./repo \
            ../artifacts/sunshine_${{ matrix.arch }}.flatpak dev.lizardbyte.sunshine'
          sudo su $(whoami) -c 'flatpak build-bundle --runtime --arch=${{ matrix.arch }} ./repo \
            ../artifacts/sunshine_debug_${{ matrix.arch }}.flatpak dev.lizardbyte.sunshine.Debug'

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: sunshine-linux-flatpak-${{ matrix.arch }}
          path: artifacts/

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.create_release == 'true' }}
        uses: ncipollo/release-action@v1
        with:
          name: ${{ needs.setup_release.outputs.release_name }}
          tag: ${{ needs.setup_release.outputs.release_tag }}
          commit: ${{ needs.setup_release.outputs.release_commit }}
          artifacts: "*artifacts/*"
          token: ${{ secrets.GH_BOT_TOKEN }}
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          discussionCategory: announcements
          prerelease: ${{ needs.setup_release.outputs.pre_release }}

  build_linux:
    name: Linux ${{ matrix.type }}
    runs-on: ubuntu-${{ matrix.dist }}
    needs: [check_changelog, setup_release]
    strategy:
      fail-fast: false  # false to test all, true to fail entire job if any fail
      matrix:
        include:  # package these differently
          - type: AppImage
            EXTRA_ARGS: '-DSUNSHINE_BUILD_APPIMAGE=ON'
            dist: 20.04

    steps:
      - name: Maximize build space
        uses: easimon/maximize-build-space@v8
        with:
          root-reserve-mb: 30720
          remove-dotnet: 'true'
          remove-android: 'true'
          remove-haskell: 'true'
          remove-codeql: 'true'
          remove-docker-images: 'true'

      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install wget
        run: |
          sudo apt-get update -y
          sudo apt-get install -y \
            wget

      - name: Install CUDA
        env:
          CUDA_VERSION: 11.8.0
          CUDA_BUILD: 520.61.05
        timeout-minutes: 4
        run: |
          url_base="https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/local_installers"
          url="${url_base}/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux.run"
          sudo wget -q -O /root/cuda.run ${url}
          sudo chmod a+x /root/cuda.run
          sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr/local/cuda --no-opengl-libs --no-man-page --no-drm
          sudo rm /root/cuda.run

      - name: Setup Dependencies Linux
        timeout-minutes: 5
        run: |
          # allow newer gcc
          sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y

          sudo apt-get install -y \
            build-essential \
            cmake \
            gcc-10 \
            g++-10 \
            libayatana-appindicator3-dev \
            libavdevice-dev \
            libboost-filesystem-dev \
            libboost-locale-dev \
            libboost-log-dev \
            libboost-program-options-dev \
            libcap-dev \
            libcurl4-openssl-dev \
            libdrm-dev \
            libevdev-dev \
            libminiupnpc-dev \
            libmfx-dev \
            libnotify-dev \
            libnuma-dev \
            libopus-dev \
            libpulse-dev \
            libssl-dev \
            libva-dev \
            libvdpau-dev \
            libwayland-dev \
            libx11-dev \
            libxcb-shm0-dev \
            libxcb-xfixes0-dev \
            libxcb1-dev \
            libxfixes-dev \
            libxrandr-dev \
            libxtst-dev \
            python3

          # clean apt cache
          sudo apt-get clean
          sudo rm -rf /var/lib/apt/lists/*

          # Update gcc alias
          # https://stackoverflow.com/a/70653945/11214013
          sudo update-alternatives --install \
            /usr/bin/gcc gcc /usr/bin/gcc-10 100 \
            --slave /usr/bin/g++ g++ /usr/bin/g++-10 \
            --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \
            --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \
            --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10

      - name: Setup python
        id: python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Build Linux
        env:
          BRANCH: ${{ github.head_ref || github.ref_name }}
          BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version }}
          COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
        timeout-minutes: 5
        run: |
          echo "nproc: $(nproc)"

          mkdir -p build
          mkdir -p artifacts

          cd build
          cmake \
            -DBUILD_WERROR=ON \
            -DCMAKE_BUILD_TYPE=Release \
            -DCMAKE_CUDA_COMPILER:PATH=/usr/local/cuda/bin/nvcc \
            -DCMAKE_INSTALL_PREFIX=/usr \
            -DSUNSHINE_ASSETS_DIR=share/sunshine \
            -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \
            -DSUNSHINE_ENABLE_WAYLAND=ON \
            -DSUNSHINE_ENABLE_X11=ON \
            -DSUNSHINE_ENABLE_DRM=ON \
            -DSUNSHINE_ENABLE_CUDA=ON \
            ${{ matrix.EXTRA_ARGS }} \
            ..
          make -j $(expr $(nproc) - 1)  # use all but one core

      - name: Set AppImage Version
        if: |
          matrix.type == 'AppImage' &&
          (needs.check_changelog.outputs.next_version_bare != needs.check_changelog.outputs.last_version)
        run: |
          version=${{ needs.check_changelog.outputs.next_version_bare }}
          echo "VERSION=${version}" >> $GITHUB_ENV

      - name: Package Linux - AppImage
        if: ${{ matrix.type == 'AppImage' }}
        working-directory: build
        run: |
          # install sunshine to the DESTDIR
          make install DESTDIR=AppDir

          # custom AppRun file
          cp -f ../packaging/linux/AppImage/AppRun ./AppDir/
          chmod +x ./AppDir/AppRun

          # variables
          DESKTOP_FILE="${DESKTOP_FILE:-sunshine.desktop}"
          ICON_FILE="${ICON_FILE:-sunshine.png}"

          # AppImage
          # https://docs.appimage.org/packaging-guide/index.html
          wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
          chmod +x linuxdeploy-x86_64.AppImage

          # https://github.com/linuxdeploy/linuxdeploy-plugin-gtk
          sudo apt-get install libgtk-3-dev librsvg2-dev -y
          wget -q https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
          chmod +x linuxdeploy-plugin-gtk.sh
          export DEPLOY_GTK_VERSION=3

          ./linuxdeploy-x86_64.AppImage \
            --appdir ./AppDir \
            --plugin gtk \
            --executable ./sunshine \
            --icon-file "../$ICON_FILE" \
            --desktop-file "./$DESKTOP_FILE" \
            --output appimage

          # move
          mv Sunshine*.AppImage ../artifacts/sunshine.AppImage

          # permissions
          chmod +x ../artifacts/sunshine.AppImage

      - name: Delete cuda
        # free up space on the runner
        run: |
          sudo rm -rf /usr/local/cuda

      - name: Verify AppImage
        if: ${{ matrix.type == 'AppImage' }}
        run: |
          wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage
          chmod +x appimagelint-x86_64.AppImage

          ./appimagelint-x86_64.AppImage ./artifacts/sunshine.AppImage

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: sunshine-linux-${{ matrix.type }}-${{ matrix.dist }}
          path: artifacts/

      - name: Install test deps
        run: |
          sudo apt-get update -y
          sudo apt-get install -y \
            doxygen \
            graphviz \
            python3-venv \
            x11-xserver-utils \
            xvfb

          # clean apt cache
          sudo apt-get clean
          sudo rm -rf /var/lib/apt/lists/*

      - name: Run tests
        id: test
        working-directory: build/tests
        run: |
          export DISPLAY=:1
          Xvfb ${DISPLAY} -screen 0 1024x768x24 &

          ./test_sunshine --gtest_color=yes

      - name: Generate gcov report
        # any except canceled or skipped
        if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure')
        id: test_report
        working-directory: build
        run: |
          ${{ steps.python.outputs.python-path }} -m pip install gcovr
          ${{ steps.python.outputs.python-path }} -m gcovr -r .. \
            --exclude '.*tests/.*' \
            --exclude '.*tests/.*' \
            --xml-pretty \
            -o coverage.xml

      - name: Upload coverage
        # any except canceled or skipped
        if: >-
          always() &&
          (steps.test_report.outcome == 'success') &&
          startsWith(github.repository, 'LizardByte/')
        uses: codecov/codecov-action@v4
        with:
          disable_search: true
          fail_ci_if_error: true
          files: ./build/coverage.xml
          flags: ${{ runner.os }}
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.create_release == 'true' }}
        uses: ncipollo/release-action@v1
        with:
          name: ${{ needs.setup_release.outputs.release_name }}
          tag: ${{ needs.setup_release.outputs.release_tag }}
          commit: ${{ needs.setup_release.outputs.release_commit }}
          artifacts: "*artifacts/*"
          token: ${{ secrets.GH_BOT_TOKEN }}
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          discussionCategory: announcements
          prerelease: ${{ needs.setup_release.outputs.pre_release }}

  build_mac_brew:
    needs: [check_changelog, setup_release]
    strategy:
      fail-fast: false  # false to test all, true to fail entire job if any fail
      matrix:
        include:
          # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
          # while GitHub has larger macOS runners, they are not available for our repos :(
          - os_version: "12"
            release: true
          - os_version: "13"
          - os_version: "14"
    name: Homebrew (macOS-${{ matrix.os_version }})
    runs-on: macos-${{ matrix.os_version }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Dependencies Homebrew
        run: |
          # install dependencies using homebrew
          brew install cmake

      - name: Configure formula
        run: |
          # variables for formula
          branch=${GITHUB_HEAD_REF}

          # check the branch variable
          if [ -z "$branch" ]
          then
            echo "This is a PUSH event"
            build_version=${{ needs.check_changelog.outputs.next_version }}
            clone_url=${{ github.event.repository.clone_url }}
            branch="${{ github.ref_name }}"
            commit=${{ github.sha }}
            default_branch="${{ github.event.repository.default_branch }}"
          else
            echo "This is a PR event"
            clone_url=${{ github.event.pull_request.head.repo.clone_url }}
            branch="${{ github.event.pull_request.head.ref }}"
            commit=${{ github.event.pull_request.head.sha }}
            default_branch="${{ github.event.pull_request.head.repo.default_branch }}"
          fi
          echo "Branch: ${branch}"
          echo "Clone URL: ${clone_url}"

          mkdir build
          cd build
          cmake \
            -DBUILD_VERSION="${build_version}" \
            -DGITHUB_BRANCH="${branch}" \
            -DGITHUB_COMMIT="${commit}" \
            -DGITHUB_CLONE_URL="${clone_url}" \
            -DGITHUB_DEFAULT_BRANCH="${default_branch}" \
            -DSUNSHINE_CONFIGURE_HOMEBREW=ON \
            -DSUNSHINE_CONFIGURE_ONLY=ON \
            ..
          cd ..

          # copy formula to artifacts
          mkdir -p homebrew
          cp -f ./build/sunshine.rb ./homebrew/sunshine.rb

          # testing
          cat ./homebrew/sunshine.rb

      - name: Upload Artifacts
        if: ${{ matrix.release }}
        uses: actions/upload-artifact@v4
        with:
          name: sunshine-homebrew
          path: homebrew/

      - name: Should Publish Homebrew Formula
        id: homebrew_publish
        run: |
          PUBLISH=false
          if [[ \
            "${{ matrix.release }}" == "true" && \
            "${{ github.repository_owner }}" == "LizardByte" && \
            "${{ needs.setup_release.outputs.create_release }}" == "true" && \
            "${{ github.ref }}" == "refs/heads/master" \
          ]]; then
            PUBLISH=true
          fi

          echo "publish=${PUBLISH}" >> $GITHUB_OUTPUT

      - name: Validate and Publish Homebrew Formula
        uses: LizardByte/homebrew-release-action@v2024.417.220943
        with:
          formula_file: ${{ github.workspace }}/homebrew/sunshine.rb
          git_email: ${{ secrets.GH_BOT_EMAIL }}
          git_username: ${{ secrets.GH_BOT_NAME }}
          publish: ${{ steps.homebrew_publish.outputs.publish }}
          token: ${{ secrets.GH_BOT_TOKEN }}

  build_mac_port:
    needs: [check_changelog, setup_release]
    strategy:
      fail-fast: false  # false to test all, true to fail entire job if any fail
      matrix:
        include:
          # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
          # while GitHub has larger macOS runners, they are not available for our repos :(
          - os_version: "12"
            release: true
          - os_version: "13"
          - os_version: "14"
    name: Macports (macOS-${{ matrix.os_version }})
    runs-on: macos-${{ matrix.os_version }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Checkout ports
        uses: actions/checkout@v4
        with:
          repository: macports/macports-ports
          fetch-depth: 64
          path: ports

      - name: Checkout mpbb
        uses: actions/checkout@v4
        with:
          repository: macports/mpbb
          path: mpbb

      - name: Setup Dependencies Macports
        run: |
          # install dependencies using homebrew
          brew install cmake

      - name: Setup python
        id: python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Configure Portfile
        run: |
          # variables for Portfile
          branch=${GITHUB_HEAD_REF}

          # check the branch variable
          if [ -z "$branch" ]
          then
            echo "This is a PUSH event"
            branch="${{ github.ref_name }}"
            build_version=${{ needs.check_changelog.outputs.next_version }}
            commit=${{ github.sha }}
            clone_url=${{ github.event.repository.clone_url }}
          else
            echo "This is a PR event"
            commit=${{ github.event.pull_request.head.sha }}
            clone_url=${{ github.event.pull_request.head.repo.clone_url }}
          fi
          echo "Commit: ${commit}"
          echo "Clone URL: ${clone_url}"

          mkdir build
          cd build
          cmake \
            -DBUILD_VERSION=${build_version} \
            -DGITHUB_BRANCH=${branch} \
            -DGITHUB_COMMIT=${commit} \
            -DGITHUB_CLONE_URL=${clone_url} \
            -DSUNSHINE_CONFIGURE_PORTFILE=ON \
            -DSUNSHINE_CONFIGURE_ONLY=ON \
            ..
          cd ..

          # copy Portfile to artifacts
          mkdir -p artifacts
          cp -f ./build/Portfile ./artifacts/

          # copy Portfile to ports
          mkdir -p ./ports/multimedia/Sunshine
          cp -f ./build/Portfile ./ports/multimedia/Sunshine/Portfile

          # testing
          cat ./artifacts/Portfile

      - name: Bootstrap MacPorts
        run: |
          . ports/.github/workflows/bootstrap.sh

          # Add getopt, mpbb and the MacPorts paths to $PATH for the subsequent steps.
          echo "/opt/mports/bin" >> $GITHUB_PATH
          echo "${PWD}/mpbb" >> $GITHUB_PATH
          echo "/opt/local/bin" >> $GITHUB_PATH
          echo "/opt/local/sbin" >> $GITHUB_PATH

      - name: Run port lint
        run: |
          port -q lint "Sunshine"

      - name: Build port
        env:
          subportlist: ${{ steps.subportlist.outputs.subportlist }}
        id: build
        run: |
          subport="Sunshine"

          workdir="/tmp/mpbb/$subport"
          mkdir -p "$workdir/logs"

          echo "::group::Installing dependencies"
          sudo mpbb \
            --work-dir "$workdir" \
            install-dependencies \
            "$subport"
          echo "::endgroup::"

          echo "::group::Installing ${subport}"
          sudo mpbb \
            --work-dir "$workdir" \
            install-port \
            --source \
            "$subport"
          echo "::endgroup::"

      - name: Build Logs
        if: always()
        run: |
          logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log"
          cat "$logfile"
          sudo mv "${logfile}" "${logfile}.bak"

      - name: Upload Artifacts
        if: ${{ matrix.release }}
        uses: actions/upload-artifact@v4
        with:
          name: sunshine-macports
          path: artifacts/

      - name: Fix screen capture permissions
        if: ${{ matrix.os_version != 12 }}  # macOS-12 is okay
        # can be removed if the following is fixed in the runner image
        # https://github.com/actions/runner-images/issues/9529
        # https://github.com/actions/runner-images/pull/9530
        run: |
          # https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access

          # permissions for screen capture
          values="'kTCCServiceScreenCapture','/opt/off/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159"
          if [[ "${{ matrix.os_version }}" == "14" ]]; then
            # TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded
            values="${values},NULL,NULL,'UNUSED',${values##*,}"
          fi

          # system and user databases
          dbPaths=(
            "/Library/Application Support/com.apple.TCC/TCC.db"
            "$HOME/Library/Application Support/com.apple.TCC/TCC.db"
          )

          sqlQuery="INSERT OR IGNORE INTO access VALUES($values);"

          for dbPath in "${dbPaths[@]}"; do
            echo "Column names for $dbPath"
            echo "-------------------"
            sudo sqlite3 "$dbPath" "PRAGMA table_info(access);"
            echo "Current permissions for $dbPath"
            echo "-------------------"
            sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';"
            sudo sqlite3 "$dbPath" "$sqlQuery"
            echo "Updated permissions for $dbPath"
            echo "-------------------"
            sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';"
          done

      - name: Run tests
        id: test
        timeout-minutes: 10
        run: |
          sudo port test "Sunshine"

      - name: Test Logs
        if: always()
        run: |
          logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log"
          cat "$logfile"

      - name: Generate gcov report
        # any except canceled or skipped
        if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure')
        id: test_report
        working-directory:
          /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work
        run: |
          base_dir=$(pwd)
          build_dir=${base_dir}/build

          # get the directory name that starts with Sunshine-*
          dir=$(ls -d Sunshine-*)

          cd ${build_dir}
          ${{ steps.python.outputs.python-path }} -m pip install gcovr
          sudo ${{ steps.python.outputs.python-path }} -m gcovr -r ../${dir} \
            --exclude '.*${dir}/tests/.*' \
            --exclude '.*${dir}/third-party/.*' \
            --gcov-object-directory $(pwd) \
            --verbose \
            --xml-pretty \
            -o ${{ github.workspace }}/build/coverage.xml

      - name: Upload coverage
        # any except canceled or skipped
        if: >-
          always() &&
          (steps.test_report.outcome == 'success') &&
          startsWith(github.repository, 'LizardByte/')
        uses: codecov/codecov-action@v4
        with:
          disable_search: true
          fail_ci_if_error: false  # todo: re-enable this when action is fixed
          files: ./build/coverage.xml
          flags: ${{ runner.os }}-${{ matrix.os_version }}
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.create_release == 'true' && matrix.release }}
        uses: ncipollo/release-action@v1
        with:
          name: ${{ needs.setup_release.outputs.release_name }}
          tag: ${{ needs.setup_release.outputs.release_tag }}
          commit: ${{ needs.setup_release.outputs.release_commit }}
          artifacts: "*artifacts/*"
          token: ${{ secrets.GH_BOT_TOKEN }}
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          discussionCategory: announcements
          prerelease: ${{ needs.setup_release.outputs.pre_release }}

  build_win:
    name: Windows
    runs-on: windows-2019
    needs: [check_changelog, setup_release]

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Prepare tests
        id: prepare-tests
        if: false  # todo: DirectX11 is not available, so even software encoder fails
        run: |
          # function to download and extract a zip file
          function DownloadAndExtract {
            param (
              [string]$Uri,
              [string]$OutFile
            )

            $maxRetries = 5
            $retryCount = 0
            $success = $false

            while (-not $success -and $retryCount -lt $maxRetries) {
              $retryCount++
              Write-Host "Downloading $Uri to $OutFile, attempt $retryCount of $maxRetries"
              try {
                Invoke-WebRequest -Uri $Uri -OutFile $OutFile
                $success = $true
              } catch {
                Write-Host "Attempt $retryCount of $maxRetries failed with error: $($_.Exception.Message). Retrying..."
                Start-Sleep -Seconds 5
              }
            }

            if (-not $success) {
              Write-Host "Failed to download the file after $maxRetries attempts."
              exit 1
            }

            # use .NET to get the base name of the file
            $baseName = (Get-Item $OutFile).BaseName

            # Extract the zip file
            Expand-Archive -Path $OutFile -DestinationPath $baseName
          }

          # virtual display driver
          DownloadAndExtract `
            -Uri "https://www.amyuni.com/downloads/usbmmidd_v2.zip" `
            -OutFile "usbmmidd_v2.zip"

          # install
          Set-Location -Path usbmmidd_v2/usbmmidd_v2
          ./deviceinstaller64 install usbmmidd.inf usbmmidd

          # create the virtual display
          ./deviceinstaller64 enableidd 1

          # move up a directory
          Set-Location -Path ../..

          # install devcon
          DownloadAndExtract `
            -Uri "https://github.com/Drawbackz/DevCon-Installer/releases/download/1.4-rc/Devcon.Installer.zip" `
            -OutFile "Devcon.Installer.zip"
          Set-Location -Path Devcon.Installer
          # hash needs to match OS version
          # https://github.com/Drawbackz/DevCon-Installer/blob/master/devcon_sources.json
          Start-Process -FilePath "./Devcon Installer.exe" -Wait -ArgumentList `
            'install', `
            '-hash', '54004C83EE34F6A55380528A8B29F4C400E61FBB947A19E0AB9E5A193D7D961E', `
            '-addpath', `
            '-update', `
            '-dir', 'C:\Windows\System32'

          # disable Hyper-V Video
          # https://stackoverflow.com/a/59490940
          C:\Windows\System32\devcon.exe disable "VMBUS\{da0a7802-e377-4aac-8e77-0558eb1073f8}"

          # move up a directory
          Set-Location -Path ..

          # multi monitor tool
          DownloadAndExtract `
            -Uri "http://www.nirsoft.net/utils/multimonitortool-x64.zip" `
            -OutFile "multimonitortool.zip"

          # enable the virtual display
          # http://www.nirsoft.net/utils/multi_monitor_tool.html
          Set-Location -Path multimonitortool

          # Original Hyper-V is \\.\DISPLAY1, it will recreate itself as \\.\DISPLAY6 (or something higher than 2)
          # USB Mobile Monitor Virtual Display is \\.\DISPLAY2

          # these don't seem to work if not using runAs
          # todo: do they work if not using runAs?
          Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /enable \\.\DISPLAY2'
          Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /SetPrimary \\.\DISPLAY2'

          # wait a few seconds
          Start-Sleep -s 5

          # list monitors
          ./MultiMonitorTool.exe /stext monitor_list.txt

          # wait a few seconds
          Start-Sleep -s 5

          # print the monitor list
          Get-Content -Path monitor_list.txt

      - name: Setup Dependencies Windows
        uses: msys2/setup-msys2@v2
        with:
          update: true
          install: >-
            base-devel
            diffutils
            doxygen
            git
            make
            mingw-w64-x86_64-binutils
            mingw-w64-x86_64-boost
            mingw-w64-x86_64-cmake
            mingw-w64-x86_64-curl
            mingw-w64-x86_64-graphviz
            mingw-w64-x86_64-miniupnpc
            mingw-w64-x86_64-nlohmann-json
            mingw-w64-x86_64-nodejs
            mingw-w64-x86_64-nsis
            mingw-w64-x86_64-onevpl
            mingw-w64-x86_64-openssl
            mingw-w64-x86_64-opus
            mingw-w64-x86_64-toolchain
            nasm
            wget
            yasm

      - name: Setup python
        # use this instead of msys2 python due to known issues using wheels, https://www.msys2.org/docs/python/
        id: setup-python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Python Path
        id: python-path
        shell: msys2 {0}
        run: |
          # replace backslashes with double backslashes
          python_path=$(echo "${{ steps.setup-python.outputs.python-path }}" | sed 's/\\/\\\\/g')

          # step output
          echo "python-path=${python_path}"
          echo "python-path=${python_path}" >> $GITHUB_OUTPUT

      - name: Build Windows
        shell: msys2 {0}
        env:
          BRANCH: ${{ github.head_ref || github.ref_name }}
          BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version }}
          COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
        run: |
          mkdir build
          cd build
          cmake \
            -DBUILD_WERROR=ON \
            -DCMAKE_BUILD_TYPE=RelWithDebInfo \
            -DSUNSHINE_ASSETS_DIR=assets \
            -DTESTS_PYTHON_EXECUTABLE='${{ steps.python-path.outputs.python-path }}' \
            -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip' \
            -G "MinGW Makefiles" \
            ..
          mingw32-make -j$(nproc)

      - name: Package Windows
        shell: msys2 {0}
        run: |
          mkdir -p artifacts
          cd build

          # package
          cpack -G NSIS
          cpack -G ZIP

          # move
          mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe
          mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip

      - name: Run tests
        id: test
        shell: msys2 {0}
        working-directory: build/tests
        run: |
          ./test_sunshine.exe --gtest_color=yes

      - name: Generate gcov report
        # any except canceled or skipped
        if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure')
        id: test_report
        shell: msys2 {0}
        working-directory: build
        run: |
          ${{ steps.python-path.outputs.python-path }} -m pip install gcovr
          ${{ steps.python-path.outputs.python-path }} -m gcovr -r .. \
            --exclude '.*tests/.*' \
            --exclude '.*tests/.*' \
            --xml-pretty \
            -o coverage.xml

      - name: Upload coverage
        # any except canceled or skipped
        if: >-
          always() &&
          (steps.test_report.outcome == 'success') &&
          startsWith(github.repository, 'LizardByte/')
        uses: codecov/codecov-action@v4
        with:
          disable_search: true
          fail_ci_if_error: true
          files: ./build/coverage.xml
          flags: ${{ runner.os }}
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Package Windows Debug Info
        working-directory: build
        run: |
          # use .dbg file extension for binaries to avoid confusion with real packages
          Get-ChildItem -File -Recurse | `
            % { Rename-Item -Path $_.PSPath -NewName $_.Name.Replace(".exe",".dbg") }

          # save the binaries with debug info
          7z -r `
            "-xr!CMakeFiles" `
            "-xr!cpack_artifacts" `
            a "../artifacts/sunshine-win32-debuginfo.7z" "*.dbg"

      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: sunshine-windows
          path: artifacts/

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.create_release == 'true' }}
        uses: ncipollo/release-action@v1
        with:
          name: ${{ needs.setup_release.outputs.release_name }}
          tag: ${{ needs.setup_release.outputs.release_tag }}
          commit: ${{ needs.setup_release.outputs.release_commit }}
          artifacts: "*artifacts/*"
          token: ${{ secrets.GH_BOT_TOKEN }}
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          discussionCategory: announcements
          prerelease: ${{ needs.setup_release.outputs.pre_release }}

  release-winget:
    name: Release to WinGet
    needs: [setup_release, build_win]
    if: |
      (github.repository_owner == 'LizardByte' &&
      needs.setup_release.outputs.create_release == 'true' &&
      github.ref == 'refs/heads/master')
    runs-on: ubuntu-latest
    steps:
      - name: Release to WinGet
        uses: vedantmgoyal2009/winget-releaser@v2
        with:
          identifier: LizardByte.Sunshine
          release-tag: ${{ needs.setup_release.outputs.release_tag }}
          installers-regex: '\.exe$'  # only .exe files
          token: ${{ secrets.GH_BOT_TOKEN }}
