---
name: CI

on:
  pull_request:
    branches: [master]
    types: [opened, synchronize, reopened]
  push:
    branches: [master]
  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) }}

  setup_release:
    name: Setup Release
    outputs:
      publish_release: ${{ steps.setup_release.outputs.publish_release }}
      release_body: ${{ steps.setup_release.outputs.release_body }}
      release_commit: ${{ steps.setup_release.outputs.release_commit }}
      release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }}
      release_tag: ${{ steps.setup_release.outputs.release_tag }}
      release_version: ${{ steps.setup_release.outputs.release_version }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Release
        id: setup_release
        uses: LizardByte/setup-release-action@v2024.919.143601
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

  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:
    env:
      APP_ID: dev.lizardbyte.app.Sunshine
      NODE_VERSION: "20"
      PLATFORM_VERSION: "23.08"
    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@v10
        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 node
        id: node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install npm dependencies
        run: |
          npm install --package-lock-only

      - name: Debug package-lock.json
        run: |
          cat package-lock.json

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

      - name: Setup Dependencies Linux Flatpak
        run: |
          python -m pip install ./packaging/linux/flatpak/deps/flatpak-builder-tools/node

          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.node${NODE_VERSION}/${{ matrix.arch }}/${PLATFORM_VERSION} \
            "

          flatpak run org.flatpak.Builder --version

      - name: flatpak node generator
        # https://github.com/flatpak/flatpak-builder-tools/blob/master/node/README.md
        run: |
          flatpak-node-generator npm package-lock.json

      - name: Debug generated-sources.json
        run: |
          cat generated-sources.json

      - name: Cache Flatpak build
        uses: actions/cache@v4
        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 }}"
          commit=${{ needs.setup_release.outputs.release_commit }}

          # check the branch variable
          if [ -z "$branch" ]
          then
            echo "This is a PUSH event"
            branch=${{ github.ref_name }}
            build_version=${{ needs.setup_release.outputs.release_tag }}
            clone_url=${{ github.event.repository.clone_url }}
          else
            echo "This is a PR event"
            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

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

      - name: Debug Manifest
        working-directory: build
        run: |
          cat ${APP_ID}.yml

      - name: Build Linux Flatpak
        working-directory: build
        run: |
          sudo su $(whoami) -c "flatpak run org.flatpak.Builder \
            --arch=${{ matrix.arch }} \
            --force-clean \
            --repo=repo \
            --sandbox \
            --stop-at=cuda build-sunshine ${APP_ID}.yml"
          cp -r .flatpak-builder copy-of-flatpak-builder
          sudo su $(whoami) -c "flatpak run org.flatpak.Builder \
            --arch=${{ matrix.arch }} \
            --force-clean \
            --repo=repo \
            --sandbox \
            build-sunshine ${APP_ID}.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 ${APP_ID}"
          sudo su $(whoami) -c "flatpak build-bundle \
            --runtime \
            --arch=${{ matrix.arch }} \
            ./repo \
            ../artifacts/sunshine_debug_${{ matrix.arch }}.flatpak ${APP_ID}.Debug"

      - name: Lint Flatpak
        working-directory: build
        run: |
          echo "Linting flatpak manifest"
          flatpak run --command=flatpak-builder-lint org.flatpak.Builder \
            manifest ${APP_ID}.yml > _flatpak-lint-exceptions_manifest.json || true

          echo "Linting flatpak repo"
          # TODO: add arg
          # --mirror-screenshots-url=https://dl.flathub.org/media \
          flatpak run --command=flatpak-builder-lint org.flatpak.Builder \
            repo repo > _flatpak-lint-exceptions_repo.json || true

          checks=(manifest repo)
          exit_code=0

          # check if files are equal
          for check in "${checks[@]}"; do
            echo "Validating $check"

            # load baseline and result files
            baseline="${{ github.workspace }}/packaging/linux/flatpak/flatpak-lint-baseline_${check}.json"
            result="_flatpak-lint-exceptions_${check}.json"

            # Extract errors from both JSON files
            readarray -t result_errors < <(jq -r '.errors[]' "$result")
            readarray -t baseline_errors < <(jq -r '.errors[]' "$baseline")

            # Loop through result errors and check against baseline errors
            for error in "${result_errors[@]}"; do
              if printf '%s\n' "${baseline_errors[@]}" | grep -q -F "$error"; then
                echo "::warning:: '$error'"
              else
                echo "::error:: '$error'"
                exit_code=1
              fi
            done
          done

          # if exit code is not 0, print results
          if [ $exit_code -ne 0 ]; then
            echo "Manifest lint results:"
            cat _flatpak-lint-exceptions_manifest.json
            echo "Repo lint results:"
            cat _flatpak-lint-exceptions_repo.json
          fi

          # exit with the correct code
          exit $exit_code

      - name: Package Flathub repo archive
        # copy files required to generate the Flathub repo
        if: ${{ matrix.arch == 'x86_64' }}
        run: |
          mkdir -p flathub/modules
          cp ./build/generated-sources.json ./flathub/
          cp ./build/package-lock.json ./flathub/
          cp ./build/${APP_ID}.yml ./flathub/
          cp ./build/${APP_ID}.metainfo.xml ./flathub/
          cp ./packaging/linux/flatpak/README.md ./flathub/
          cp ./packaging/linux/flatpak/flathub.json ./flathub/
          cp -r ./packaging/linux/flatpak/modules/. ./flathub/modules/
          # submodules will need to be handled in the workflow that creates the PR

          # create the archive
          tar -czf ./artifacts/flathub.tar.gz -C ./flathub .

      - 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.publish_release == 'true' }}
        uses: LizardByte/create-release-action@v2024.919.143026
        with:
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}
          name: ${{ needs.setup_release.outputs.release_tag }}
          prerelease: true
          tag: ${{ needs.setup_release.outputs.release_tag }}
          token: ${{ secrets.GH_BOT_TOKEN }}

  build_linux:
    name: Linux ${{ matrix.type }}
    runs-on: ubuntu-${{ matrix.dist }}
    needs: [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: '--appimage-build'
            dist: 22.04

    steps:
      - name: Maximize build space
        uses: easimon/maximize-build-space@v10
        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: Setup Dependencies Linux
        timeout-minutes: 5
        run: |
          # create the artifacts directory
          mkdir -p artifacts

          # allow libfuse2 for appimage on 22.04+
          sudo add-apt-repository universe

          sudo apt-get install -y \
            libdrm-dev \
            libfuse2 \
            libgl-dev \
            libwayland-dev \
            libx11-xcb-dev \
            libxcb-dri3-dev \
            libxfixes-dev

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

      - name: Build latest libva
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        timeout-minutes: 5
        run: |
          gh release download --archive=tar.gz --repo=intel/libva
          tar xzf libva-*.tar.gz && rm libva-*.tar.gz
          cd libva-*
          ./autogen.sh --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu \
            --enable-drm \
            --enable-x11 \
            --enable-glx \
            --enable-wayland \
            --without-legacy  # emgd, nvctrl, fglrx
          make -j $(nproc)
          sudo make install
          cd .. && rm -rf libva-*

      - name: Build Linux
        env:
          BRANCH: ${{ github.head_ref || github.ref_name }}
          BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }}
          COMMIT: ${{ needs.setup_release.outputs.release_commit }}
        run: |
          chmod +x ./scripts/linux_build.sh
          ./scripts/linux_build.sh \
            --publisher-name='${{ github.repository_owner }}' \
            --publisher-website='https://app.lizardbyte.dev' \
            --publisher-issue-url='https://app.lizardbyte.dev/support' \
            --skip-cleanup \
            --skip-package \
            --ubuntu-test-repo ${{ matrix.EXTRA_ARGS }}

      - name: Set AppImage Version
        if: |
          matrix.type == 'AppImage'
        run: |
          version=${{ needs.setup_release.outputs.release_tag }}
          echo "VERSION=${version}" >> $GITHUB_ENV

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

          # 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: |
          rm -rf ./build/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 \
            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 &
          sleep 5  # give Xvfb time to start

          ./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 ../src \
            --exclude-noncode-lines \
            --exclude-throw-branches \
            --exclude-unreachable-branches \
            --verbose \
            --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 }}
          verbose: true

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.publish_release == 'true' }}
        uses: LizardByte/create-release-action@v2024.919.143026
        with:
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}
          name: ${{ needs.setup_release.outputs.release_tag }}
          prerelease: true
          tag: ${{ needs.setup_release.outputs.release_tag }}
          token: ${{ secrets.GH_BOT_TOKEN }}

  build_homebrew:
    needs: [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: "13"
            os_name: "macos"
          - os_version: "14"
            os_name: "macos"
          - os_version: "latest"
            os_name: "ubuntu"
          - os_version: "latest"  # this job will only configure the formula for release, no validation
            os_name: "ubuntu"
            release: true
    name: Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }})
    runs-on: ${{ matrix.os_name }}-${{ matrix.os_version }}

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

      - name: Fix python
        if: matrix.os_name == 'macos' && matrix.os_version == '13'
        run: |
          rm '/usr/local/bin/2to3'
          rm '/usr/local/bin/2to3-3.12'
          rm '/usr/local/bin/idle3'
          rm '/usr/local/bin/idle3.12'
          rm '/usr/local/bin/pydoc3'
          rm '/usr/local/bin/pydoc3.12'
          rm '/usr/local/bin/python3'
          rm '/usr/local/bin/python3-config'
          rm '/usr/local/bin/python3.12'
          rm '/usr/local/bin/python3.12-config'
          brew install python

      - name: Configure formula
        run: |
          # variables for formula
          branch="${{ github.head_ref }}"
          commit=${{ needs.setup_release.outputs.release_commit }}

          # check the branch variable
          if [ -z "$branch" ]
          then
            echo "This is a PUSH event"
            build_version=${{ needs.setup_release.outputs.release_tag }}
            clone_url=${{ github.event.repository.clone_url }}
            branch="${{ github.ref_name }}"
            default_branch="${{ github.event.repository.default_branch }}"

            if [ "${{ matrix.release }}" == "true" ]; then
              # we will publish the formula with the release tag
              tag="${{ needs.setup_release.outputs.release_tag }}"
              version="${{ needs.setup_release.outputs.release_version }}"
            else
              tag="${{ github.ref_name }}"
              version="0.0.${{ github.run_number }}"
            fi
          else
            echo "This is a PR event"
            build_version="0.0.${{ github.event.number }}"
            clone_url=${{ github.event.pull_request.head.repo.clone_url }}
            branch="${{ github.event.pull_request.head.ref }}"
            default_branch="${{ github.event.pull_request.head.repo.default_branch }}"
            tag="${{ github.event.pull_request.head.ref }}"
            version="0.0.${{ github.event.number }}"
          fi
          echo "Branch: ${branch}"
          echo "Clone URL: ${clone_url}"
          echo "Tag: ${tag}"

          mkdir -p build
          cmake \
            -B build \
            -S . \
            -DBUILD_VERSION="${build_version}" \
            -DFORMULA_VERSION="${version}" \
            -DGITHUB_BRANCH="${branch}" \
            -DGITHUB_COMMIT="${commit}" \
            -DGITHUB_CLONE_URL="${clone_url}" \
            -DGITHUB_DEFAULT_BRANCH="${default_branch}" \
            -DGITHUB_TAG="${tag}" \
            -DSUNSHINE_CONFIGURE_HOMEBREW=ON \
            -DSUNSHINE_CONFIGURE_ONLY=ON

          # 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: Setup Xvfb
        if: |
          matrix.release != true &&
          runner.os == 'Linux'
        run: |
          sudo apt-get update -y
          sudo apt-get install -y \
            xvfb

          export DISPLAY=:1
          Xvfb ${DISPLAY} -screen 0 1024x768x24 &

          echo "DISPLAY=${DISPLAY}" >> $GITHUB_ENV

      - name: Validate Homebrew Formula
        if: |
          matrix.release != true
        uses: LizardByte/homebrew-release-action@v2024.919.145818
        with:
          formula_file: ${{ github.workspace }}/homebrew/sunshine.rb
          git_email: ${{ secrets.GH_BOT_EMAIL }}
          git_username: ${{ secrets.GH_BOT_NAME }}
          publish: false
          token: ${{ secrets.GH_BOT_TOKEN }}
          validate: true

      - name: Create/Update GitHub Release
        if: >-
          matrix.release &&
          needs.setup_release.outputs.publish_release == 'true'
        uses: LizardByte/create-release-action@v2024.919.143026
        with:
          allowUpdates: true
          artifacts: '${{ github.workspace }}/homebrew/*'
          body: ${{ needs.setup_release.outputs.release_body }}
          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}
          name: ${{ needs.setup_release.outputs.release_tag }}
          prerelease: true
          tag: ${{ needs.setup_release.outputs.release_tag }}
          token: ${{ secrets.GH_BOT_TOKEN }}

      - name: Patch homebrew formula
        # create beta version of the formula
        # don't run this on macOS, as the sed command fails
        if: >-
          matrix.release
        run: |
          # variables
          formula_file="homebrew/sunshine-beta.rb"

          # rename the file
          mv homebrew/sunshine.rb $formula_file

          # update the formula
          sed -i 's/class Sunshine < Formula/class SunshineBeta < Formula/' $formula_file
          sed -i 's/# conflicts_with/conflicts_with/' $formula_file

          # print new file
          echo "New formula:"
          cat $formula_file

      - name: Upload Homebrew Beta Formula
        if: >-
          github.repository_owner == 'LizardByte' &&
          matrix.release &&
          needs.setup_release.outputs.publish_release == 'true'
        uses: LizardByte/homebrew-release-action@v2024.919.145818
        with:
          formula_file: ${{ github.workspace }}/homebrew/sunshine-beta.rb
          git_email: ${{ secrets.GH_BOT_EMAIL }}
          git_username: ${{ secrets.GH_BOT_NAME }}
          publish: true
          token: ${{ secrets.GH_BOT_TOKEN }}
          validate: false

  build_mac_port:
    needs: [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: "13"
            release: true
          - 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 }}"
          commit=${{ needs.setup_release.outputs.release_commit }}

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

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

          # 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 permissions
        run: |
          # https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access
          # https://github.com/actions/runner-images/issues/9529
          # https://github.com/actions/runner-images/pull/9530

          # function to execute sql query for each value
          function execute_sql_query {
            local value=$1
            local dbPath=$2

            echo "Executing SQL query for value: $value"
            sudo sqlite3 "$dbPath" "INSERT OR IGNORE INTO access VALUES($value);"
          }

          # Find all provisioner paths and store them in an array
          readarray -t provisioner_paths < <(sudo find /opt /usr -name provisioner)
          echo "Provisioner paths: ${provisioner_paths[@]}"

          # Create an empty array
          declare -a values=()

          # Loop through the provisioner paths and add them to the values array
          for p_path in "${provisioner_paths[@]}"; do
            # Adjust the service name and other parameters as needed
            values+=("'kTCCServiceAccessibility','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,NULL,1592919552")
            values+=("'kTCCServiceScreenCapture','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159")
          done

          echo "Values: ${values[@]}"

          if [[ "${{ matrix.os_version }}" == "14" ]]; then
            # TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded
            for i in "${!values[@]}"; do
              values[$i]="${values[$i]},NULL,NULL,'UNUSED',${values[$i]##*,}"
            done
          fi

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

          for value in "${values[@]}"; do
            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';"
              execute_sql_query "$value" "$dbPath"
              echo "Updated permissions for $dbPath"
              echo "-------------------"
              sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';"
            done
          done

      - name: Run tests
        id: test
        timeout-minutes: 10
        working-directory:
          /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work/build/tests
        run: |
          sudo ./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:
          /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}/src \
            --exclude-noncode-lines \
            --exclude-throw-branches \
            --exclude-unreachable-branches \
            --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 }}
          verbose: true

      - name: Create/Update GitHub Release
        if: ${{ needs.setup_release.outputs.publish_release == 'true' }}
        uses: LizardByte/create-release-action@v2024.919.143026
        with:
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}
          name: ${{ needs.setup_release.outputs.release_tag }}
          prerelease: true
          tag: ${{ needs.setup_release.outputs.release_tag }}
          token: ${{ secrets.GH_BOT_TOKEN }}

  build_win:
    name: Windows
    runs-on: windows-2019
    needs: [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:
          msystem: ucrt64
          update: true
          install: >-
            git
            mingw-w64-ucrt-x86_64-boost
            mingw-w64-ucrt-x86_64-cmake
            mingw-w64-ucrt-x86_64-cppwinrt
            mingw-w64-ucrt-x86_64-curl-winssl
            mingw-w64-ucrt-x86_64-graphviz
            mingw-w64-ucrt-x86_64-miniupnpc
            mingw-w64-ucrt-x86_64-nlohmann-json
            mingw-w64-ucrt-x86_64-nodejs
            mingw-w64-ucrt-x86_64-nsis
            mingw-w64-ucrt-x86_64-onevpl
            mingw-w64-ucrt-x86_64-openssl
            mingw-w64-ucrt-x86_64-opus
            mingw-w64-ucrt-x86_64-toolchain
            wget

      - name: Install Doxygen
        # GCC compiled doxygen has issues when running graphviz
        env:
          DOXYGEN_VERSION: "1.11.0"
        run: |
          # Set version variables
          $doxy_ver = $env:DOXYGEN_VERSION
          $_doxy_ver = $doxy_ver.Replace(".", "_")

          # Download the Doxygen installer
          Invoke-WebRequest -Uri `
            "https://github.com/doxygen/doxygen/releases/download/Release_${_doxy_ver}/doxygen-${doxy_ver}-setup.exe" `
            -OutFile "doxygen-setup.exe"

          # Run the installer
          Start-Process `
            -FilePath .\doxygen-setup.exe `
            -ArgumentList `
              '/VERYSILENT' `
          -Wait `
          -NoNewWindow

          # Clean up
          Remove-Item -Path doxygen-setup.exe

      - name: Setup 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.setup_release.outputs.release_tag }}
          COMMIT: ${{ needs.setup_release.outputs.release_commit }}
        run: |
          mkdir -p build
          cmake \
            -B build \
            -G Ninja \
            -S . \
            -DBUILD_WERROR=ON \
            -DCMAKE_BUILD_TYPE=RelWithDebInfo \
            -DSUNSHINE_ASSETS_DIR=assets \
            -DSUNSHINE_PUBLISHER_NAME='${{ github.repository_owner }}' \
            -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \
            -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' \
            -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip'
          ninja -C build

      - 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 ../src \
            --exclude-noncode-lines \
            --exclude-throw-branches \
            --exclude-unreachable-branches \
            --verbose \
            --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 }}
          verbose: true

      - 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.publish_release == 'true' }}
        uses: LizardByte/create-release-action@v2024.919.143026
        with:
          allowUpdates: true
          body: ${{ needs.setup_release.outputs.release_body }}
          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}
          name: ${{ needs.setup_release.outputs.release_tag }}
          prerelease: true
          tag: ${{ needs.setup_release.outputs.release_tag }}
          token: ${{ secrets.GH_BOT_TOKEN }}