SDL: Port SDL3 release scripts to SDL2

From 8291b1be363ceada6ad64844a4467a5e09763fdf Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Mon, 16 Sep 2024 22:56:12 +0200
Subject: [PATCH] Port SDL3 release scripts to SDL2

[skip ci]
---
 .github/workflows/release.yml                 | 406 +++++++++++
 .gitignore                                    |   1 +
 VisualC/SDL/SDL.vcxproj                       |  16 -
 VisualC/pkg-support/cmake/sdl2-config.cmake   |   4 +-
 build-scripts/build-release.py                | 640 ++++++++++++++++++
 .../cmake-toolchain-mingw64-i686.cmake        |  18 +
 .../cmake-toolchain-mingw64-x86_64.cmake      |  18 +
 build-scripts/create-release.py               |  41 ++
 cmake/test/CMakeLists.txt                     |  25 +
 cmake/test/sdltest.c                          |   9 +
 mingw/pkg-support/INSTALL.txt                 |  18 +
 mingw/pkg-support/Makefile                    |  30 +
 .../cmake/sdl2-config-version.cmake           |   4 +-
 mingw/pkg-support/cmake/sdl2-config.cmake     |   4 +-
 14 files changed, 1213 insertions(+), 21 deletions(-)
 create mode 100644 .github/workflows/release.yml
 create mode 100755 build-scripts/build-release.py
 create mode 100644 build-scripts/cmake-toolchain-mingw64-i686.cmake
 create mode 100644 build-scripts/cmake-toolchain-mingw64-x86_64.cmake
 create mode 100755 build-scripts/create-release.py
 create mode 100644 cmake/test/sdltest.c
 create mode 100644 mingw/pkg-support/INSTALL.txt
 create mode 100644 mingw/pkg-support/Makefile

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000000..f6f0c56665a8d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,406 @@
+name: 'release'
+run-name: 'Create SDL release artifacts for ${{ inputs.commit }}'
+
+on:
+  workflow_dispatch:
+    inputs:
+      commit:
+        description: 'Commit of SDL'
+        required: true
+
+jobs:
+
+  src:
+    runs-on: ubuntu-latest
+    outputs:
+      project: ${{ steps.releaser.outputs.project }}
+      version: ${{ steps.releaser.outputs.version }}
+      src-tar-gz: ${{ steps.releaser.outputs.src-tar-gz }}
+      src-tar-xz: ${{ steps.releaser.outputs.src-tar-xz }}
+      src-zip: ${{ steps.releaser.outputs.src-zip }}
+    steps:
+      - name: 'Set up Python'
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.11'
+      - name: 'Fetch build-release.py'
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: 'build-scripts/build-release.py'
+      - name: 'Set up SDL sources'
+        uses: actions/checkout@v4
+        with:
+          path: 'SDL'
+          fetch-depth: 0
+      - name: 'Build Source archive'
+        id: releaser
+        shell: bash
+        run: |
+          python build-scripts/build-release.py \
+            --create source \
+            --commit ${{ inputs.commit }} \
+            --project SDL2 \
+            --root "${{ github.workspace }}/SDL" \
+            --github \
+            --debug
+      - name: 'Store source archives'
+        uses: actions/upload-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace}}/dist'
+
+  linux-verify:
+    needs: [src]
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Unzip ${{ needs.src.outputs.src-zip }}'
+        id: zip
+        run: |
+          mkdir /tmp/zipdir
+          cd /tmp/zipdir
+          unzip "${{ github.workspace }}/${{ needs.src.outputs.src-zip }}"
+          echo "path=/tmp/zipdir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}'
+        id: tar
+        run: |
+          mkdir -p /tmp/tardir
+          tar -C /tmp/tardir -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}"
+          echo "path=/tmp/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Compare contents of ${{ needs.src.outputs.src-zip }} and  ${{ needs.src.outputs.src-tar-gz }}'
+        run: |
+          diff /tmp/zipdir /tmp/tardir
+      - name: 'Test versioning'
+        shell: bash
+        run: |
+          ${{ steps.tar.outputs.path }}/build-scripts/test-versioning.sh
+      - name: 'CMake (configure + build + tests + examples)'
+        run: |
+          cmake -S ${{ steps.tar.outputs.path }} -B /tmp/build -DSDL_TEST_LIBRARY=TRUE -DSDL_TESTS=TRUE -DSDL_EXAMPLES=TRUE
+          cmake --build /tmp/build --verbose
+          ctest --test-dir /tmp/build --no-tests=error --output-on-failure
+
+  dmg:
+    needs: [src]
+    runs-on: macos-latest
+    outputs:
+      dmg: ${{ steps.releaser.outputs.dmg }}
+    steps:
+      - name: 'Set up Python'
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.10'
+      - name: 'Fetch build-release.py'
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: 'build-scripts/build-release.py'
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}'
+        id: tar
+        run: |
+          mkdir -p "${{ github.workspace }}/tardir"
+          tar -C "${{ github.workspace }}/tardir" -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}"
+          echo "path=${{ github.workspace }}/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Build SDL2.dmg'
+        id: releaser
+        shell: bash
+        run: |
+          python build-scripts/build-release.py \
+            --create framework \
+            --commit ${{ inputs.commit }} \
+            --project SDL2 \
+            --root "${{ steps.tar.outputs.path }}" \
+            --github \
+            --debug
+      - name: 'Store DMG image file'
+        uses: actions/upload-artifact@v4
+        with:
+          name: dmg
+          path: '${{ github.workspace }}/dist'
+
+  dmg-verify:
+    needs: [dmg, src]
+    runs-on: macos-latest
+    steps:
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Download ${{ needs.dmg.outputs.dmg }}'
+        uses: actions/download-artifact@v4
+        with:
+          name: dmg
+          path: '${{ github.workspace }}'
+      - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}'
+        id: src
+        run: |
+          mkdir -p /tmp/tardir
+          tar -C /tmp/tardir -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}"
+          echo "path=/tmp/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Mount ${{ needs.dmg.outputs.dmg }}'
+        id: mount
+        run: |
+          hdiutil attach '${{ github.workspace }}/${{ needs.dmg.outputs.dmg }}'
+          mount_point="/Volumes/${{ needs.src.outputs.project }}"
+          if [ ! -d "$mount_point/${{ needs.src.outputs.project }}.framework" ]; then
+            echo "Cannot find ${{ needs.src.outputs.project }}.framework!"
+            exit 1
+          fi
+          echo "mount_point=$mount_point">>$GITHUB_OUTPUT
+      - name: 'CMake (configure + build) Darwin'
+        run: |
+          set -e
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"               \
+              -DTEST_FULL=FALSE                                             \
+              -DTEST_STATIC=FALSE                                           \
+              -DTEST_TEST=FALSE                                             \
+              -DCMAKE_PREFIX_PATH="${{ steps.mount.outputs.mount_point }}"  \
+              -DCMAKE_SYSTEM_NAME=Darwin                                    \
+              -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"                      \
+              -Werror=dev                                                   \
+              -B build_darwin
+          cmake --build build_darwin --config Release --verbose
+
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"               \
+              -DTEST_FULL=FALSE                                             \
+              -DTEST_STATIC=FALSE                                           \
+              -DTEST_TEST=FALSE                                             \
+              -DCMAKE_PREFIX_PATH="${{ steps.mount.outputs.mount_point }}"  \
+              -DCMAKE_SYSTEM_NAME=Darwin                                    \
+              -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"                      \
+              -Werror=dev                                                   \
+              -B build_darwin_2
+          cmake --build build_darwin --config Release --verbose
+
+  msvc:
+    needs: [src]
+    runs-on: windows-2019
+    outputs:
+      VC-x86: ${{ steps.releaser.outputs.VC-x86 }}
+      VC-x64: ${{ steps.releaser.outputs.VC-x64 }}
+      VC-devel: ${{ steps.releaser.outputs.VC-devel }}
+    steps:
+      - name: 'Set up Python'
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.10'
+      - name: 'Fetch build-release.py'
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: 'build-scripts/build-release.py'
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Unzip ${{ needs.src.outputs.src-zip }}'
+        id: zip
+        run: |
+          New-Item C:\temp -ItemType Directory -ErrorAction SilentlyContinue
+          cd C:\temp
+          unzip "${{ github.workspace }}/${{ needs.src.outputs.src-zip }}"
+          echo "path=C:\temp\${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$Env:GITHUB_OUTPUT
+      - name: 'Build MSVC binary archives'
+        id: releaser
+        run: |
+          python build-scripts/build-release.py     `
+            --create win32                          `
+            --commit ${{ inputs.commit }}           `
+            --project SDL2                          `
+            --root "${{ steps.zip.outputs.path }}"  `
+            --github                                `
+            --debug
+      - name: 'Store MSVC archives'
+        uses: actions/upload-artifact@v4
+        with:
+          name: win32
+          path: '${{ github.workspace }}/dist'
+
+  msvc-verify:
+    needs: [msvc, src]
+    runs-on: windows-latest
+    steps:
+      - name: 'Fetch .github/actions/setup-ninja/action.yml'
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: '.github/actions/setup-ninja/action.yml'
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Download MSVC binaries'
+        uses: actions/download-artifact@v4
+        with:
+          name: win32
+          path: '${{ github.workspace }}'
+      - name: 'Unzip ${{ needs.src.outputs.src-zip }}'
+        id: src
+        run: |
+          mkdir '${{ github.workspace }}/sources'
+          cd '${{ github.workspace }}/sources'
+          unzip "${{ github.workspace }}/${{ needs.src.outputs.src-zip }}"
+          echo "path=${{ github.workspace }}/sources/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$env:GITHUB_OUTPUT
+      - name: 'Unzip ${{ needs.msvc.outputs.VC-devel }}'
+        id: bin
+        run: |
+          mkdir '${{ github.workspace }}/vc'
+          cd '${{ github.workspace }}/vc'
+          unzip "${{ github.workspace }}/${{ needs.msvc.outputs.VC-devel }}"
+          echo "path=${{ github.workspace }}/vc/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$env:GITHUB_OUTPUT
+      - name: Set up ninja
+        uses: ./.github/actions/setup-ninja
+      - name: 'Configure vcvars x86'
+        uses: ilammy/msvc-dev-cmd@v1
+        with:
+          arch: x64_x86
+      - name: 'CMake (configure + build + tests) x86'
+        run: |
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"     `
+              -B build_x86                                        `
+              -GNinja                                             `
+              -DCMAKE_BUILD_TYPE=Debug                            `
+              -Werror=dev                                         `
+              -DTEST_FULL=FALSE                                   `
+              -DTEST_STATIC=FALSE                                 `
+              -DTEST_SHARED=TRUE                                  `
+              -DTEST_TEST=TRUE                                    `
+              -DCMAKE_SUPPRESS_REGENERATION=TRUE                  `
+              -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}"
+          Start-Sleep -Seconds 2
+          cmake --build build_x86 --config Release --verbose
+          #ctest --test-dir build_x86 --no-tests=error -C Release --output-on-failure
+      - name: 'Configure vcvars x64'
+        uses: ilammy/msvc-dev-cmd@v1
+        with:
+          arch: x64
+      - name: 'CMake (configure + build + tests) x64'
+        run: |
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"     `
+              -B build_x64                                        `
+              -GNinja                                             `
+              -DCMAKE_BUILD_TYPE=Debug                            `
+              -Werror=dev                                         `
+              -DTEST_FULL=FALSE                                   `
+              -DTEST_STATIC=FALSE                                 `
+              -DTEST_SHARED=TRUE                                  `
+              -DTEST_TEST=TRUE                                    `
+              -DCMAKE_SUPPRESS_REGENERATION=TRUE                  `
+              -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}"
+          Start-Sleep -Seconds 2
+          cmake --build build_x64 --config Release --verbose
+          #ctest --test-dir build_x64 --no-tests=error -C Release --output-on-failure
+
+  mingw:
+    needs: [src]
+    runs-on: ubuntu-24.04  # FIXME: current ubuntu-latest ships an outdated mingw, replace with ubuntu-latest once 24.04 becomes the new default
+    outputs:
+      mingw-devel-tar-gz: ${{ steps.releaser.outputs.mingw-devel-tar-gz }}
+      mingw-devel-tar-xz: ${{ steps.releaser.outputs.mingw-devel-tar-xz }}
+    steps:
+      - name: 'Set up Python'
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.10'
+      - name: 'Fetch build-release.py'
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: 'build-scripts/build-release.py'
+      - name: 'Install Mingw toolchain'
+        run: |
+          sudo apt-get update -y
+          sudo apt-get install -y gcc-mingw-w64 g++-mingw-w64 ninja-build
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}'
+        id: tar
+        run: |
+          mkdir -p /tmp/tardir
+          tar -C /tmp/tardir -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}"
+          echo "path=/tmp/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Build MinGW binary archives'
+        id: releaser
+        run: |
+          python build-scripts/build-release.py     \
+            --create mingw                          \
+            --commit ${{ inputs.commit }}           \
+            --project SDL2                          \
+            --root "${{ steps.tar.outputs.path }}"  \
+            --github                                \
+            --debug
+      - name: 'Store MinGW archives'
+        uses: actions/upload-artifact@v4
+        with:
+          name: mingw
+          path: '${{ github.workspace }}/dist'
+
+  mingw-verify:
+    needs: [mingw, src]
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Install Mingw toolchain'
+        run: |
+          sudo apt-get update -y
+          sudo apt-get install -y gcc-mingw-w64 g++-mingw-w64 ninja-build
+      - name: 'Download source archives'
+        uses: actions/download-artifact@v4
+        with:
+          name: sources
+          path: '${{ github.workspace }}'
+      - name: 'Download MinGW binaries'
+        uses: actions/download-artifact@v4
+        with:
+          name: mingw
+          path: '${{ github.workspace }}'
+      - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}'
+        id: src
+        run: |
+          mkdir -p /tmp/tardir
+          tar -C /tmp/tardir -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}"
+          echo "path=/tmp/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'Untar ${{ needs.mingw.outputs.mingw-devel-tar-gz }}'
+        id: bin
+        run: |
+          mkdir -p /tmp/mingw-tardir
+          tar -C /tmp/mingw-tardir -v -x -f "${{ github.workspace }}/${{ needs.mingw.outputs.mingw-devel-tar-gz }}"
+          echo "path=/tmp/mingw-tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT
+      - name: 'CMake (configure + build) i686'
+        run: |
+          set -e
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"                                                           \
+              -DCMAKE_BUILD_TYPE="Release"                                                                              \
+              -DTEST_FULL=FALSE                                                                                         \
+              -DTEST_STATIC=TRUE                                                                                        \
+              -DTEST_TEST=TRUE                                                                                          \
+              -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}"                                                       \
+              -DCMAKE_TOOLCHAIN_FILE="${{ steps.src.outputs.path }}/build-scripts/cmake-toolchain-mingw64-i686.cmake"   \
+              -DCMAKE_C_FLAGS="-DSDL_DISABLE_SSE4_2"                                                                    \
+              -Werror=dev                                                                                               \
+              -B build_x86
+          cmake --build build_x86 --config Release --verbose
+      - name: 'CMake (configure + build) x86_64'
+        run: |
+          set -e
+          cmake -S "${{ steps.src.outputs.path }}/cmake/test"                                                           \
+              -DCMAKE_BUILD_TYPE="Release"                                                                              \
+              -DTEST_FULL=FALSE                                                                                         \
+              -DTEST_STATIC=TRUE                                                                                        \
+              -DTEST_TEST=TRUE                                                                                          \
+              -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}"                                                       \
+              -DCMAKE_TOOLCHAIN_FILE="${{ steps.src.outputs.path }}/build-scripts/cmake-toolchain-mingw64-x86_64.cmake" \
+              -DCMAKE_C_FLAGS="-DSDL_DISABLE_SSE4_2"                                                                    \
+              -Werror=dev                                                                                               \
+              -B build_x64
+          cmake --build build_x64 --config Release --verbose
diff --git a/.gitignore b/.gitignore
index cced2f8ed8b53..0af2680e36b56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ gen
 Build
 buildbot
 /VERSION.txt
+dist
 
 *.so
 *.so.*
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index a798514a1b5e0..b068b3330c326 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -44,22 +44,6 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
   <ImportGroup Label="ExtensionSettings">
   </ImportGroup>
-  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
-    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
-    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC70.props" />
-  </ImportGroup>
-  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
-    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
-    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC70.props" />
-  </ImportGroup>
-  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
-    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
-    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC70.props" />
-  </ImportGroup>
-  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
-    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
-    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC70.props" />
-  </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
     <_ProjectFileVersion>10.0.40219.1</_ProjectFileVersion>
diff --git a/VisualC/pkg-support/cmake/sdl2-config.cmake b/VisualC/pkg-support/cmake/sdl2-config.cmake
index e54f470008e31..411b78a5152d0 100644
--- a/VisualC/pkg-support/cmake/sdl2-config.cmake
+++ b/VisualC/pkg-support/cmake/sdl2-config.cmake
@@ -79,6 +79,8 @@ endif()
 unset(_sdl2_library)
 unset(_sdl2_dll_library)
 
+set(SDL2_SDL2-static_FOUND FALSE)
+
 set(_sdl2main_library "${SDL2_LIBDIR}/SDL2main.lib")
 if(EXISTS "${_sdl2main_library}")
     if(NOT TARGET SDL2::SDL2main)
@@ -110,7 +112,7 @@ if(EXISTS "${_sdl2test_library}")
     endif()
     set(SDL2_SDL2test_FOUND TRUE)
 else()
-    set(SDL2_SDL2_FOUND FALSE)
+    set(SDL2_SDL2test_FOUND FALSE)
 endif()
 unset(_sdl2test_library)
 
diff --git a/build-scripts/build-release.py b/build-scripts/build-release.py
new file mode 100755
index 0000000000000..3e94b66fc549a
--- /dev/null
+++ b/build-scripts/build-release.py
@@ -0,0 +1,640 @@
+#!/usr/bin/env python
+
+import argparse
+import collections
+import contextlib
+import datetime
+import glob
+import io
+import json
+import logging
+import os
+from pathlib import Path
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import textwrap
+import typing
+import zipfile
+
+logger = logging.getLogger(__name__)
+
+
+VcArchDevel = collections.namedtuple("VcArchDevel", ("dll", "pdb", "imp", "main", "test"))
+GIT_HASH_FILENAME = ".git-hash"
+
+ANDROID_AVAILABLE_ABIS = [
+    "armeabi-v7a",
+    "arm64-v8a",
+    "x86",
+    "x86_64",
+]
+ANDROID_MINIMUM_API = 19
+ANDROID_TARGET_API = 29
+ANDROID_MINIMUM_NDK = 21
+
+
+class Executer:
+    def __init__(self, root: Path, dry: bool=False):
+        self.root = root
+        self.dry = dry
+
+    def run(self, cmd, stdout=False, dry_out=None, force=False):
+        sys.stdout.flush()
+        logger.info("Executing args=%r", cmd)
+        if self.dry and not force:
+            if stdout:
+                return subprocess.run(["echo", "-E", dry_out or ""], stdout=subprocess.PIPE if stdout else None, text=True, check=True, cwd=self.root)
+        else:
+            return subprocess.run(cmd, stdout=subprocess.PIPE if stdout else None, text=True, check=True, cwd=self.root)
+
+
+class SectionPrinter:
+    @contextlib.contextmanager
+    def group(self, title: str):
+        print(f"{title}:")
+        yield
+
+
+class GitHubSectionPrinter(SectionPrinter):
+    def __init__(self):
+        super().__init__()
+        self.in_group = False
+
+    @contextlib.contextmanager
+    def group(self, title: str):
+        print(f"::group::{title}")
+        assert not self.in_group, "Can enter a group only once"
+        self.in_group = True
+        yield
+        self.in_group = False
+        print("::endgroup::")
+
+
+class VisualStudio:
+    def __init__(self, executer: Executer, year: typing.Optional[str]=None):
+        self.executer = executer
+        self.vsdevcmd = self.find_vsdevcmd(year)
+        self.msbuild = self.find_msbuild()
+
+    @property
+    def dry(self) -> bool:
+        return self.executer.dry
+
+    VS_YEAR_TO_VERSION = {
+        "2022": 17,
+        "2019": 16,
+        "2017": 15,
+        "2015": 14,
+        "2013": 12,
+    }
+
+    def find_vsdevcmd(self, year: typing.Optional[str]=None) -> typing.Optional[Path]:
+        vswhere_spec = ["-latest"]
+        if year is not None:
+            try:
+                version = self.VS_YEAR_TO_VERSION[year]
+            except KeyError:
+                logger.error("Invalid Visual Studio year")
+                return None
+            vswhere_spec.extend(["-version", f"[{version},{version+1})"])
+        vswhere_cmd = ["vswhere"] + vswhere_spec + ["-property", "installationPath"]
+        vs_install_path = Path(self.executer.run(vswhere_cmd, stdout=True, dry_out="/tmp").stdout.strip())
+        logger.info("VS install_path = %s", vs_install_path)
+        assert vs_install_path.is_dir(), "VS installation path does not exist"
+        vsdevcmd_path = vs_install_path / "Common7/Tools/vsdevcmd.bat"
+        logger.info("vsdevcmd path = %s", vsdevcmd_path)
+        if self.dry:
+            vsdevcmd_path.parent.mkdir(parents=True, exist_ok=True)
+            vsdevcmd_path.touch(exist_ok=True)
+        assert vsdevcmd_path.is_file(), "vsdevcmd.bat batch file does not exist"
+        return vsdevcmd_path
+
+    def find_msbuild(self) -> typing.Optional[Path]:
+        vswhere_cmd = ["vswhere", "-latest", "-requires", "Microsoft.Component.MSBuild", "-find", r"MSBuild\**\Bin\MSBuild.exe"]
+        msbuild_path = Path(self.executer.run(vswhere_cmd, stdout=True, dry_out="/tmp/MSBuild.exe").stdout.strip())
+        logger.info("MSBuild path = %s", msbuild_path)
+        if self.dry:
+            msbuild_path.parent.mkdir(parents=True, exist_ok=True)
+            msbuild_path.touch(exist_ok=True)
+        assert msbuild_path.is_file(), "MSBuild.exe does not exist"
+        return msbuild_path
+
+    def build(self, arch: str, platform: str, configuration: str, projects: list[Path]):
+        assert projects, "Need at least one project to build"
+
+        vsdev_cmd_str = f"\"{self.vsdevcmd}\" -arch={arch}"
+        msbuild_cmd_str = " && ".join([f"\"{self.msbuild}\" \"{project}\" /m /p:BuildInParallel=true /p:Platform={platform} /p:Configuration={configuration}" for project in projects])
+        bat_contents = f"{vsdev_cmd_str} && {msbuild_cmd_str}\n"
+        bat_path = Path(tempfile.gettempdir()) / "cmd.bat"
+        with bat_path.open("w") as f:
+            f.write(bat_contents)
+
+        logger.info("Running cmd.exe script (%s): %s", bat_path, bat_contents)
+        cmd = ["cmd.exe", "/D", "/E:ON", "/V:OFF", "/S", "/C", f"CALL {str(bat_path)}"]
+        self.executer.run(cmd)
+
+
+class Releaser:
+    def __init__(self, project: str, commit: str, root: Path, dist_path: Path, section_printer: SectionPrinter, executer: Executer, cmake_generator: str):
+        self.project = project
+        self.version = self.extract_sdl_version(root=root, project=project)
+        self.root = root
+        self.commit = commit
+        self.dist_path = dist_path
+        self.section_printer = section_printer
+        self.executer = executer
+        self.cmake_generator = cmake_generator
+
+        self.artifacts: dict[str, Path] = {}
+
+    @property
+    def dry(self) -> bool:
+        return self.executer.dry
+
+    def prepare(self):
+        logger.debug("Creating dist folder")
+        self.dist_path.mkdir(parents=True, exist_ok=True)
+
+    TreeItem = collections.namedtuple("TreeItem", ("path", "mode", "data", "time"))
+    def _get_file_times(self, paths: tuple[str, ...]) -> dict[str, datetime.datetime]:
+        dry_out = textwrap.dedent("""\
+            time=2024-03-14T15:40:25-07:00
+
+            M\tCMakeLists.txt
+        """)
+        git_log_out = self.executer.run(["git", "log", "--name-status", '--pretty=time=%cI', self.commit], stdout=True, dry_out=dry_out).stdout.splitlines(keepends=False)
+        current_time = None
+        set_paths = set(paths)
+        path_times: dict[str, datetime.datetime] = {}
+        for line in git_log_out:
+            if not line:
+                continue
+            if line.startswith("time="):
+                current_time = datetime.datetime.fromisoformat(line.removeprefix("time="))
+                continue
+            mod_type, file_paths = line.split(maxsplit=1)
+            assert current_time is not None
+            for file_path in file_paths.split("\t"):
+                if file_path in set_paths and file_path not in path_times:
+                    path_times[file_path] = current_time
+        assert set(path_times.keys()) == set_paths
+        return path_times
+
+    @staticmethod
+    def _path_filter(path: str):
+        if path.startswith(".git"):
+            return False
+        return True
+
+    def _get_git_contents(self) -> dict[str, TreeItem]:
+        contents_tgz = subprocess.check_output(["git", "archive", "--format=tar.gz", self.commit, "-o", "/dev/stdout"], text=False)
+        contents = tarfile.open(fileobj=io.BytesIO(contents_tgz), mode="r:gz")
+        filenames = tuple(m.name for m in contents if m.isfile())
+        assert "src/SDL.c" in filenames
+        assert "include/SDL.h" in filenames
+        file_times = self._get_file_times(filenames)
+        git_contents = {}
+        for ti in contents:
+            if not ti.isfile():
+                continue
+            if not self._path_filter(ti.name):
+                continue
+            contents_file = contents.extractfile(ti.name)
+            assert contents_file, f"{ti.name} is not a file"
+            git_contents[ti.name] = self.TreeItem(path=ti.name, mode=ti.mode, data=contents_file.read(), time=file_times[ti.name])
+        return git_contents
+
+    def create_source_archives(self) -> None:
+        archive_base = f"{self.project}-{self.version}"
+
+        git_contents = self._get_git_contents()
+        git_files = list(git_contents.values())
+        assert len(git_contents) == len(git_files)
+
+        latest_mod_time = max(item.time for item in git_files)
+
+        git_files.append(self.TreeItem(path="VERSION.txt", data=f"{self.version}\n".encode(), mode=0o100644, time=latest_mod_time))
+        git_files.append(self.TreeItem(path=GIT_HASH_FILENAME, data=f"{self.commit}\n".encode(), mode=0o100644, time=latest_mod_time))
+
+        git_files.so

(Patch may be truncated, please check the link at the top of this post.)