name: Build

on:
  push:
    branches-ignore:
      - gh-readonly-queue/**
  pull_request:
  merge_group:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/') }}

jobs:
  build-cmake:
    runs-on: ${{ matrix.os }}
    env:
      CARGO_HTTP_MULTIPLEXING: false
    strategy:
      fail-fast: false
      matrix:
        name: [ubuntu-latest-fancy, macOS-latest, windows-latest, ubuntu-22.04, windows-latest-mingw]
        include:
        - name: ubuntu-latest-fancy
          os: ubuntu-latest
          cmake-args: -G Ninja
          cmake-init-env: CXXFLAGS=-Werror
          package-file: "*-linux_x86_64.tar.xz"
        - name: ubuntu-22.04
          os: ubuntu-22.04
          cmake-path: /usr/bin/
          cmake-args: -G "Unix Makefiles" -DTEST_MYSQL=ON
          cmake-init-env: CXXFLAGS=-Werror
          gtest-env: GTEST_FILTER=-*SQLite*
          package-file: "*-linux_x86_64.tar.xz"
        - name: macOS-latest
          os: macOS-latest
          cmake-args: -G Ninja
          cmake-init-env: CXXFLAGS=-Werror
          package-file: "*-macos.dmg"
        - name: windows-latest
          os: windows-latest
          cmake-args: -A x64 -DEXCEPTION_HANDLING=ON -DVULKAN=ON
          cmake-init-env: CXXFLAGS=/WX LDFLAGS=/WX
          package-file: "*-win64.zip"
          executable-suffix: .exe
        - name: windows-latest-mingw
          os: windows-latest
          cmake-args: -G Ninja -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DEXCEPTION_HANDLING=ON -DCMAKE_RC_COMPILER=windres -DCMAKE_AR=ar -DVULKAN=ON
          cmake-init-env: CXXFLAGS=-Werror LDFLAGS=-Werror
          package-file: "*-win64.zip"
          executable-suffix: .exe
          # Delete outdated libwinpthread-1.dll after configuring to workaround https://github.com/ddnet/ddnet/issues/10203 when using MinGW.
          # Adding this condition into the individual steps is not possible without using bash shell consistently, which does not work for windows-latest.
          workaround-issue10203-run-after-configure: rm libwinpthread-1.dll

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

    - name: Prepare Linux
      if: contains(matrix.os, 'ubuntu')
      run: |
        sudo apt-get update -y
        # mount: /var/lib/grub/esp: special device /dev/disk/by-id/scsi-... does not exist.
        # sudo apt-get upgrade -y
        packages=(
          # tidy-alphabetical-start
          gcovr
          libavcodec-dev
          libavformat-dev
          libavutil-dev
          libfreetype6-dev
          libglew-dev
          libnotify-dev
          libpng-dev
          libsdl2-dev
          libsqlite3-dev
          libswresample-dev
          libswscale-dev
          libx264-dev
          ninja-build
          pkg-config
          valgrind
          # tidy-alphabetical-end
        )
        sudo apt-get install -y "${packages[@]}"

    - name: Prepare Linux (non-fancy)
      if: contains(matrix.os, 'ubuntu') && !contains(matrix.name, 'fancy')
      run: |
        curl -LO https://github.com/Kitware/CMake/releases/download/v3.13.4/cmake-3.13.4-Linux-x86_64.tar.gz
        sudo tar --strip-components 1 -C /usr -xf cmake-3.13.4-Linux-x86_64.tar.gz
        # Our minimum supported Rust version (MSRV)
        rustup default 1.85.0
        sudo rm -rf /var/lib/mysql/ /var/run/mysqld
        sudo mkdir /var/lib/mysql/ /var/run/mysqld/
        sudo chown mysql:mysql /var/lib/mysql/ /var/run/mysqld/
        sudo mysqld --initialize-insecure --user=mysql --basedir=/usr --datadir=/var/lib/mysql/
        sudo /usr/bin/mysqld_safe --basedir=/usr --datadir='/var/lib/mysql/' &
        sleep 10
        sudo mysql <<EOF
        CREATE DATABASE ddnet;
        CREATE USER 'ddnet'@'localhost' IDENTIFIED BY 'thebestpassword';
        GRANT ALL PRIVILEGES ON ddnet.* TO 'ddnet'@'localhost';
        FLUSH PRIVILEGES;
        EOF

    - name: Prepare Linux (fancy)
      if: contains(matrix.os, 'ubuntu') && contains(matrix.name, 'fancy')
      run: |
        sudo apt-get install cmake libmariadb-dev libwebsockets-dev mariadb-server -y
        sudo systemctl stop mysql
        sudo rm -rf /var/lib/mysql/
        sudo mysql_install_db --user=mysql --datadir=/var/lib/mysql/
        cd /usr; sudo mysqld_safe --datadir='/var/lib/mysql/' --no-watch
        sleep 10
        sudo mysql <<EOF
        CREATE DATABASE ddnet;
        CREATE USER 'ddnet'@'localhost' IDENTIFIED BY 'thebestpassword';
        GRANT ALL PRIVILEGES ON ddnet.* TO 'ddnet'@'localhost';
        FLUSH PRIVILEGES;
        EOF

    - name: Prepare macOS
      if: contains(matrix.os, 'macOS')
      run: |
        brew update
        brew install ffmpeg molten-vk ninja pkg-config sdl2
        brew upgrade freetype
        pip3 install --break-system-packages dmgbuild pyobjc-framework-Quartz
        echo /Library/Frameworks/Python.framework/Versions/3.12/bin >> $GITHUB_PATH
        sudo rm -rf /Library/Developer/CommandLineTools

    - name: Prepare Windows MinGW
      if: contains(matrix.os, 'windows') && contains(matrix.name, 'mingw')
      run: |
        rustup toolchain install stable-x86_64-pc-windows-gnu
        rustup target add x86_64-pc-windows-gnu
        rustup default stable-x86_64-pc-windows-gnu
        rustup set default-host x86_64-pc-windows-gnu

    - name: Install Vulkan SDK
      uses: humbletim/install-vulkan-sdk@v1.2
      with:
        version: 1.3.296.0
        cache: true

    - name: Cache Rust dependencies
      uses: Swatinem/rust-cache@v2
      with:
          cache-bin: false # WORKAROUND(https://github.com/Swatinem/rust-cache/issues/341)
          workspaces: |
              src/mastersrv
              src/masterping
              ./

    - name: Build mastersrv
      if: ${{ !contains(matrix.os, 'ubuntu-22.04') }}
      run: |
        cd src/mastersrv
        cargo build

    - name: Build masterping
      if: ${{ !contains(matrix.os, 'ubuntu-22.04') }}
      run: |
        cd src/masterping
        cargo build

    - name: Build in debug mode
      run: |
        mkdir debug
        cd debug
        ${{ matrix.cmake-path }}cmake --version
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.cmake-init-env }} ${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=Debug -Werror=dev -DDOWNLOAD_GTEST=ON -DDEV=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG=. ..
        ${{ matrix.workaround-issue10203-run-after-configure }}
        ${{ matrix.cmake-path }}cmake --build . --config Debug --target everything

    - name: Test debug
      run: |
        cd debug
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests

    - name: Run debug server
      run: |
        cd debug
        ./DDNet-Server shutdown

    - name: Build in release mode
      run: |
        mkdir release
        cd release
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.cmake-init-env }} ${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=Release -Werror=dev -DDOWNLOAD_GTEST=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=. ..
        ${{ matrix.workaround-issue10203-run-after-configure }}
        ${{ matrix.cmake-path }}cmake --build . --config Release --target everything

    - name: Test release
      run: |
        cd release
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config Release --target run_tests

    - name: Run release server
      run: |
        cd release
        ./DDNet-Server shutdown

    - name: Build headless client
      if: ${{ !contains(matrix.os, 'ubuntu') }}
      run: |
        mkdir headless
        cd headless
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.cmake-init-env }} ${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DHEADLESS_CLIENT=ON -DCMAKE_BUILD_TYPE=Debug -Werror=dev -DDOWNLOAD_GTEST=ON -DDEV=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG=. ..
        ${{ matrix.workaround-issue10203-run-after-configure }}
        ${{ matrix.cmake-path }}cmake --build . --config Debug

    - name: Build headless client with code coverage
      if: contains(matrix.os, 'ubuntu-latest')
      run: |
        mkdir headless
        cd headless
        ${{ matrix.cmake-path }}cmake -E env CXXFLAGS="--coverage -Werror" ${{ matrix.cmake-path }}cmake -E env LDFLAGS="--coverage -Werror" ${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DHEADLESS_CLIENT=ON -DWEBSOCKETS=ON -DCMAKE_BUILD_TYPE=Debug -Werror=dev -DDOWNLOAD_GTEST=ON -DDEV=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG=. ..
        ${{ matrix.workaround-issue10203-run-after-configure }}
        ${{ matrix.cmake-path }}cmake -E env RUSTFLAGS="-Clink-arg=--coverage" ${{ matrix.cmake-path }}cmake --build . --config Debug

    - name: Test headless client (unit tests)
      if: ${{ !contains(matrix.os, 'ubuntu') }}
      run: |
        cd headless
        ${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests

    - name: Test headless client (unit tests) with code coverage
      if: contains(matrix.os, 'ubuntu-latest')
      run: |
        cd headless
        ${{ matrix.cmake-path }}cmake -E env RUSTFLAGS="-Clink-arg=--coverage" RUSTDOCFLAGS="-Clink-arg=--coverage" ${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests

    - name: Upload Codecov report (unit tests)
      if: contains(matrix.os, 'ubuntu-latest')
      uses: codecov/codecov-action@v4
      with:
        flags: unittests

    - name: Build in release mode with debug info and all features on
      if: contains(matrix.name, 'fancy')
      run: |
        mkdir fancy
        cd fancy
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.cmake-init-env }} ${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDOWNLOAD_GTEST=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=. -DANTIBOT=ON -DWEBSOCKETS=ON ..
        ${{ matrix.workaround-issue10203-run-after-configure }}
        ${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target everything

    - name: Test fancy
      if: contains(matrix.name, 'fancy')
      run: |
        cd fancy
        ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target run_tests

    - name: Run fancy server
      if: contains(matrix.name, 'fancy')
      run: |
        cd fancy
        ./DDNet-Server shutdown

    - name: Run integration tests
      id: integration_test
      if: ${{ !contains(matrix.os, 'ubuntu') }}
      continue-on-error: true
      run: |
        cp src/mastersrv/target/debug/mastersrv${{ matrix.executable-suffix }} headless
        python scripts/integration_test.py --show-full-output --test-mastersrv headless

    - name: Run integration tests with Valgrind's Memcheck and code coverage
      id: integration_test_valgrind
      if: contains(matrix.os, 'ubuntu-latest')
      continue-on-error: true
      run: |
        # Remove old coverage data:
        find headless -name '*.gcno' -o -name '*.gcda' -delete
        cp src/mastersrv/target/debug/mastersrv${{ matrix.executable-suffix }} headless
        python scripts/integration_test.py --show-full-output --test-mastersrv --test-websockets --valgrind-memcheck headless

    - name: Upload integration test results
      id: integration_test_artifact_upload
      if: steps.integration_test.outcome == 'failure' || steps.integration_test_valgrind.outcome == 'failure'
      uses: actions/upload-artifact@v4
      with:
        name: ddnet-${{ matrix.name }}-integration-test-results
        path: headless/integration_*
        if-no-files-found: error

    - name: Summarize integration test results
      if: steps.integration_test.outcome == 'failure' || steps.integration_test_valgrind.outcome == 'failure'
      shell: bash
      run: |
        echo -e "\033[31mFAILED: Integration test failed. Refer to the workflow logs and uploaded artifacts in the steps above for details.\033[m"
        echo "### Integration test failed" >> $GITHUB_STEP_SUMMARY
        echo >> $GITHUB_STEP_SUMMARY
        echo "See summary below. Refer to the workflow logs and uploaded artifacts for details." >> $GITHUB_STEP_SUMMARY
        echo >> $GITHUB_STEP_SUMMARY
        echo "Artifact URL: ${{ steps.integration_test_artifact_upload.outputs.artifact-url }}" >> $GITHUB_STEP_SUMMARY
        for integration_dir in headless/integration_*; do
          echo >> $GITHUB_STEP_SUMMARY
          echo "<details><summary>Test $(basename "$integration_dir")</summary>" >> $GITHUB_STEP_SUMMARY
          echo >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          cat "$integration_dir/test_failure.log" >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          echo >> $GITHUB_STEP_SUMMARY
          echo "</details>" >> $GITHUB_STEP_SUMMARY
        done
        exit 1

    - name: Upload Codecov report (integration tests)
      if: contains(matrix.os, 'ubuntu-latest')
      uses: codecov/codecov-action@v4
      with:
        flags: integrationtests

    - name: Package
      run: |
        cd release
        ${{ matrix.cmake-path }}cmake --build . --config Release --target package_default
        mkdir artifacts
        mv ${{ matrix.package-file }} artifacts

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

  build-android:
    runs-on: ubuntu-24.04
    env:
      CARGO_HTTP_MULTIPLEXING: false
    steps:
    - uses: actions/checkout@v4
      with:
        submodules: recursive

    - name: Validate Gradle Wrapper
      uses: gradle/actions/wrapper-validation@v4

    - name: Prepare Linux
      run: |
        sudo apt-get update -y
        sudo apt-get install ninja-build openjdk-21-jdk openssl
        curl -LO https://github.com/Kitware/CMake/releases/download/v3.22.1/cmake-3.22.1-Linux-x86_64.tar.gz
        mkdir -p /home/runner/cmake
        tar --strip-components 1 -C /home/runner/cmake -xf cmake-3.22.1-Linux-x86_64.tar.gz
        rustup default 1.92.0
        cargo install cargo-ndk
        rustup target add armv7-linux-androideabi
        rustup target add i686-linux-android
        rustup target add aarch64-linux-android
        rustup target add x86_64-linux-android
        scripts/android/download_android_sdk.sh /home/runner

    - name: Install Vulkan SDK
      uses: humbletim/install-vulkan-sdk@v1.2
      with:
        version: 1.3.296.0
        cache: true

    - name: Cache Rust dependencies
      uses: Swatinem/rust-cache@v2
      with:
          cache-bin: false # WORKAROUND(https://github.com/Swatinem/rust-cache/issues/341)
          workspaces: |
              src/mastersrv
              src/masterping
              ./

    - name: Build Android app
      env:
        TW_KEY_NAME: /home/runner/DDNet.jks
        TW_KEY_ALIAS: DDNet-Key
      run: |
        export TW_KEY_PW="$(openssl rand -base64 32)"
        keytool -genkey -v -keystore "$TW_KEY_NAME" -keyalg RSA -keysize 2048 -validity 10000 -alias "$TW_KEY_ALIAS" -storepass "$TW_KEY_PW" -dname "CN=DDNet CI, OU=DDNet, O=DDNet"
        export ANDROID_HOME="/home/runner/Android/Sdk"
        unset ANDROID_SDK_ROOT
        export PATH=/home/runner/cmake/bin:$PATH
        scripts/android/cmake_android.sh all DDNet org.ddnet.client Release build-android
        mkdir artifacts
        mv build-android/DDNet.apk artifacts

    - name: Upload Artifacts
      uses: actions/upload-artifact@v4
      with:
        name: ddnet-android
        path: artifacts

  build-emscripten:
    runs-on: ubuntu-24.04
    env:
      CARGO_HTTP_MULTIPLEXING: false
    steps:
    - uses: actions/checkout@v4
      with:
        submodules: recursive

    - name: Prepare Linux
      run: |
        curl -LO https://github.com/Kitware/CMake/releases/download/v3.22.1/cmake-3.22.1-Linux-x86_64.tar.gz
        mkdir -p /home/runner/cmake
        tar --strip-components 1 -C /home/runner/cmake -xf cmake-3.22.1-Linux-x86_64.tar.gz
        rustup default 1.89.0
        rustup target add wasm32-unknown-emscripten
        git clone https://github.com/emscripten-core/emsdk emsdk
        cd emsdk
        ./emsdk install 4.0.22
        ./emsdk activate 4.0.22

    - name: Cache Rust dependencies
      uses: Swatinem/rust-cache@v2
      with:
          cache-bin: false # WORKAROUND(https://github.com/Swatinem/rust-cache/issues/341)
          workspaces: |
              src/mastersrv
              src/masterping
              ./

    - name: Build headless Emscripten client in debug mode
      run: |
        export PATH=/home/runner/cmake/bin:$PATH
        source emsdk/emsdk_env.sh
        emcmake cmake -H. -G "Unix Makefiles" -B build-emscripten-headless -DCMAKE_BUILD_TYPE=Debug -DVIDEORECORDER=OFF -DVULKAN=OFF -DSERVER=OFF -DTOOLS=OFF -DHEADLESS_CLIENT=ON -DPREFER_BUNDLED_LIBS=ON
        cmake --build build-emscripten-headless --target game-client -j$(nproc)

    - name: Test headless Emscripten client
      run: |
        source emsdk/emsdk_env.sh
        emrun \
          --browser firefox \
          --browser-args=-headless \
          --port 8000 \
          --silence_timeout=120 \
          --timeout=300 \
          --kill_start \
          --kill_exit \
          build-emscripten-headless/index.html -- "quit" "#__emrun_autostart__"

    - name: Build Emscripten client in release mode
      run: |
        export PATH=/home/runner/cmake/bin:$PATH
        source emsdk/emsdk_env.sh
        # -G "Unix Makefiles" required due to https://gitlab.kitware.com/cmake/cmake/-/issues/16395
        emcmake cmake -H. -G "Unix Makefiles" -B build-emscripten-release -DCMAKE_BUILD_TYPE=Release -DVIDEORECORDER=OFF -DVULKAN=OFF -DSERVER=OFF -DTOOLS=OFF -DPREFER_BUNDLED_LIBS=ON
        cmake --build build-emscripten-release --target game-client -j$(nproc)
        cmake --build build-emscripten-release --target package_default
        mkdir artifacts
        mv build-emscripten-release/*-Emscripten.tar.xz artifacts

    - name: Upload Artifacts
      uses: actions/upload-artifact@v4
      with:
        name: ddnet-emscripten
        path: artifacts
