From e19b6c0b95893b4e86737a44e9236d6cddd116f0 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Wed, 2 Aug 2023 05:41:02 +0200
Subject: [PATCH] Add releaser script + workflow
---
.github/cmake/CMakeLists.txt | 2 +-
.github/workflows/release.yml | 416 ++++++++++++++++++++++
build-scripts/releaser.py | 641 ++++++++++++++++++++++++++++++++++
3 files changed, 1058 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/release.yml
create mode 100755 build-scripts/releaser.py
diff --git a/.github/cmake/CMakeLists.txt b/.github/cmake/CMakeLists.txt
index 39ebaf8b8f32a..48757010a674b 100644
--- a/.github/cmake/CMakeLists.txt
+++ b/.github/cmake/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.0)
+cmake_minimum_required(VERSION 3.0...3.5)
project(ci_utils C CXX)
set(txt "CC=${CMAKE_C_COMPILER}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000000..fed1fa657f1ea
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,416 @@
+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.10'
+ - name: 'Fetch releaser.py'
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: 'build-scripts/releaser.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/releaser.py \
+ --create source \
+ --commit ${{ inputs.commit }} \
+ --project SDL3 \
+ --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)'
+ run: |
+ cmake -S ${{ steps.tar.outputs.path }} -B /tmp/build -DSDL_TEST_LIBRARY=TRUE -DSDL_TESTS=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 releaser.py'
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: 'build-scripts/releaser.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 /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 SDL3.dmg'
+ id: releaser
+ shell: bash
+ run: |
+ python build-scripts/releaser.py \
+ --create xcframework \
+ --commit ${{ inputs.commit }} \
+ --project SDL3 \
+ --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 }}.xcframework" ]; then
+ echo "Cannot find ${{ needs.src.outputs.project }}.xcframework!"
+ exit 1
+ fi
+ echo "mount_point=$mount_point">>$GITHUB_OUTPUT
+ - name: 'CMake (configure + build) Darwin'
+ run: |
+ 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" \
+ -B build_darwin
+ cmake --build build_darwin --config Release --verbose
+ - name: 'CMake (configure + build) iOS'
+ run: |
+ 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=iOS \
+ -DCMAKE_OSX_ARCHITECTURES="arm64" \
+ -B build_ios
+ cmake --build build_ios --config Release --verbose
+ - name: 'CMake (configure + build) tvOS'
+ run: |
+ 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=tvOS \
+ -DCMAKE_OSX_ARCHITECTURES="arm64" \
+ -B build_tvos
+ cmake --build build_tvos --config Release --verbose
+ - name: 'CMake (configure + build) iOS simulator'
+ run: |
+ sysroot=$(xcodebuild -version -sdk iphonesimulator Path)
+ echo "sysroot=$sysroot"
+ 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=iOS \
+ -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
+ -DCMAKE_OSX_SYSROOT="${sysroot}" \
+ -B build_ios_simulator
+ cmake --build build_ios_simulator --config Release --verbose
+ - name: 'CMake (configure + build) tvOS simulator'
+ run: |
+ sysroot=$(xcodebuild -version -sdk appletvsimulator Path)
+ echo "sysroot=$sysroot"
+ 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=tvOS \
+ -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
+ -DCMAKE_OSX_SYSROOT="${sysroot}" \
+ -B build_tvos_simulator
+ cmake --build build_tvos_simulator --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 releaser.py'
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: 'build-scripts/releaser.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: |
+ mkdir C:\zipdir
+ cd C:\zipdir
+ unzip "${{ github.workspace }}/${{ needs.src.outputs.src-zip }}"
+ echo "path=C:\zipdir\${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$Env:GITHUB_OUTPUT
+ - name: 'Build MSVC binary archives'
+ id: releaser
+ run: |
+ python build-scripts/releaser.py `
+ --create win32 `
+ --commit ${{ inputs.commit }} `
+ --project SDL3 `
+ --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: '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: 'CMake (configure + build + tests) x86'
+ run: |
+ $env:PATH += ";${{ steps.bin.outputs.path }}/x86"
+ cmake -S "${{ steps.src.outputs.path }}/cmake/test" `
+ -DTEST_FULL=TRUE `
+ -DTEST_STATIC=FALSE `
+ -DTEST_TEST=TRUE `
+ -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}" `
+ -B build_x86 -A win32
+ cmake --build build_x86 --config Release --verbose
+ ctest --test-dir build_x86 --no-tests=error -C Release --output-on-failure
+ - name: 'CMake (configure + build + tests) x64'
+ run: |
+ $env:PATH += ";${{ steps.bin.outputs.path }}/x86"
+ cmake -S "${{ steps.src.outputs.path }}/cmake/test" `
+ -DTEST_FULL=TRUE `
+ -DTEST_STATIC=FALSE `
+ -DTEST_TEST=TRUE `
+ -DCMAKE_PREFIX_PATH="${{ steps.bin.outputs.path }}" `
+ -B build_x64 -A x64
+ 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-latest
+ 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 releaser.py'
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: 'build-scripts/releaser.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/releaser.py \
+ --create mingw \
+ --commit ${{ inputs.commit }} \
+ --project SDL3 \
+ --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: |
+ cmake -S "${{ steps.src.outputs.path }}/cmake/test" \
+ -DCMAKE_BUILD_TYPE="Release" \
+ -DTEST_FULL=TRUE \
+ -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" \
+ -B build_x86
+ cmake --build build_x86 --config Release --verbose
+ - name: 'CMake (configure + build) x86_64'
+ run: |
+ cmake -S "${{ steps.src.outputs.path }}/cmake/test" \
+ -DCMAKE_BUILD_TYPE="Release" \
+ -DTEST_FULL=TRUE \
+ -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" \
+ -B build_x64
+ cmake --build build_x64 --config Release --verbose
diff --git a/build-scripts/releaser.py b/build-scripts/releaser.py
new file mode 100755
index 0000000000000..b85b0635f1e9f
--- /dev/null
+++ b/build-scripts/releaser.py
@@ -0,0 +1,641 @@
+#!/usr/bin/env python
+
+import argparse
+import collections
+import contextlib
+import datetime
+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", "imp", "test"))
+GIT_HASH_FILENAME = ".git-hash"
+
+
+def itertools_batched(iterator: typing.Iterable, count: int):
+ iterator = iter(iterator)
+ while True:
+ items = []
+ for _ in range(count):
+ obj = next(iterator, None)
+ if obj is None:
+ yield tuple(items)
+ return
+ items.append(obj)
+ yield tuple(items)
+
+
+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):
+ 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 = cls.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", "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 = {}
+
+ @property
+ def dry(self):
+ return self.executer.dry
+
+ def prepare(self):
+ logger.debug("Creating dist folder")
+ self.dist_path.mkdir(parents=True, exist_ok=True)
+
+ GitLsTreeResult = collections.namedtuple("GitLsTreeResult", ("path", "mode", "object_type", "object_name"))
+ def _git_ls_tree(self, commit) -> dict[str, GitLsTreeResult]:
+ logger.debug("Getting source listing from git")
+ dry_out = textwrap.dedent("""\
+ "CMakeLists.txt": {"object_name": "9e5e4bcf094bfbde94f19c3f314808031ec8f141", "mode": "100644", "type": "blob"},
+ """)
+
+ last_key = "zzzzzz"
+ dict_tree_items = "{" + self.executer.run(["git", "ls-tree", "-r", """--format="%(path)": {"object_name": "%(objectname)", "mode": "%(objectmode)", "type": "%(objecttype)"},""", commit], stdout=True, dry_out=dry_out).stdout + f'"{last_key}": null' + "}"
+ with open("/tmp/a.txt", "w") as f:
+ f.write(dict_tree_items)
+ f.write("\n")
+ dict_tree_items = json.loads(dict_tree_items)
+ del dict_tree_items[last_key]
+
+ tree_items = {path: self.GitLsTreeResult(path=path, mode=int(v["mode"], 8), object_type=v["type"], object_name=v["object_name"]) for path, v in dict_tree_items.items()}
+ assert all(item.object_type == "blob" for item in tree_items.values())
+ return tree_items
+
+ def _git_cat_file(self, tree_items: dict[str, GitLsTreeResult]) -> dict[str, bytes]:
+ logger.debug("Getting source binary data from git")
+ if self.dry:
+ return {
+ "CMakeLists.txt": b"cmake_minimum_required(VERSION 3.20)\nproject(SDL)\n",
+ }
+ git_cat = subprocess.Popen(["git", "cat-file", "--batch"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=False, bufsize=50 * 1024 * 1024)
+ data_tree = {}
+ batch_size = 60
+ for batch in itertools_batched(tree_items.items(), batch_size):
+ for object_path, tree_item in batch:
+ logger.debug("Requesting data of file '%s' (object=%s)...", object_path, tree_item.object_name)
+ git_cat.stdin.write(f"{tree_item.object_name}\n".encode())
+ git_cat.stdin.flush()
+ for object_path, tree_item in batch:
+ header = git_cat.stdout.readline().decode()
+ object_name, object_type, obj_size = header.strip().split(maxsplit=3)
+ assert tree_item.object_name == object_name
+ assert tree_item.object_type == object_type
+ obj_size = int(obj_size)
+ data_tree[object_path] = git_cat.stdout.read(obj_size)
+ logger.debug("File data received '%s'", object_path)
+ assert git_cat.stdout.readline() == b"\n"
+
+ assert len(data_tree) == len(tree_items)
+
+ logger.debug("No more file!")
+ git_cat.stdin.close()
+ git_cat.wait()
+ assert git_cat.returncode == 0
+ logger.debug("All data received!")
+ return data_tree
+
+ def _get_file_times(self, tree_items: dict[str, GitLsTreeResult]) -> 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'], stdout=True, dry_out=dry_out).stdout.splitlines(keepends=False)
+ current_time = None
+ tree_paths = {item.path for item in tree_items.values()}
+ path_times = {}
+ 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, paths = line.split(maxsplit=1)
+ assert current_time is not None
+ for path in paths.split():
+ if path in tree_paths and path not in path_times:
+ path_times[path] = current_time
+ assert set(path_times.keys()) == tree_paths
+ return path_times
+
+ @staticmethod
+ def _path_filter(path: str):
+ if path.startswith(".git"):
+ return False
+ return True
+
+ TreeItem = collections.namedtuple("TreeItem", ("path", "mode", "data", "time"))
+
+ def _get_git_contents(self) -> dict[str, (TreeItem, bytes, datetime.datetime)]:
+ commit_file_tree = self._git_ls_tree(self.commit)
+ git_datas = self._git_cat_file(commit_file_tree)
+ git_times = self._get_file_times(commit_file_tree)
+ git_contents = {path: self.TreeItem(path=path, data=git_datas[path], mode=item.mode, time=git_times[path]) for path, item in commit_file_tree.items() if self._path_filter(path)}
+ return git_contents
+
+ def create_source_archives(self):
+ 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.sort(key=lambda v: v.time)
+
+ zip_path = self.dist_path / f"{archive_base}.zip"
+ logger.info("Creating .zip source archive (%s)...", zip_path)
+ if self.dry:
+ zip_path.touch()
+ else:
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as
(Patch may be truncated, please check the link at the top of this post.)