diff --git a/.github/actions/collect-artifacts/action.yml b/.github/actions/collect-artifacts/action.yml new file mode 100644 index 00000000000..d160adfb158 --- /dev/null +++ b/.github/actions/collect-artifacts/action.yml @@ -0,0 +1,21 @@ +name: Collect Artifacts +description: Stage compiler outputs for one game. +inputs: + game: {required: true} + preset: {required: true} +runs: + using: composite + steps: + - shell: bash + run: | + base="build/${{ inputs.preset }}" + [[ '${{ inputs.preset }}' == win32* ]] && config=$([[ '${{ inputs.preset }}' == *debug* ]] && echo Debug || echo Release) || config='' + game='${{ inputs.game }}' + [[ $game == Generals || $game == GeneralsMD ]] || { echo "Unknown game: $game" >&2; exit 1; } + destination="$base/$game/artifacts"; mkdir -p "$destination" + sources=("$base/Core" "$base/$game"); [[ -n $config ]] && sources=("$base/Core/$config" "$base/$game/$config") + found=false + for source in "${sources[@]}"; do + for file in "$source"/*.{exe,dll,pdb}; do [[ -f $file ]] && cp -f "$file" "$destination/" && found=true; done + done + $found || { echo "No artifacts found for $game" >&2; exit 1; } diff --git a/.github/actions/configure-build/action.yml b/.github/actions/configure-build/action.yml new file mode 100644 index 00000000000..c841b4273f9 --- /dev/null +++ b/.github/actions/configure-build/action.yml @@ -0,0 +1,36 @@ +name: Configure Build +description: Configure one game with the selected CMake preset. +inputs: + game: {required: true} + preset: {required: true} + tools: {required: true} + extras: {required: true} + ffmpeg: {required: true} + retail-compatible: {required: true} +runs: + using: composite + steps: + - shell: bash + run: | + onoff() { [[ "$1" == true ]] && echo ON || echo OFF; } + game='${{ inputs.game }}'; preset='${{ inputs.preset }}' + case "$game" in + Generals) zero_hour=false; generals=true ;; + GeneralsMD) zero_hour=true; generals=false ;; + *) echo "Unknown game: $game" >&2; exit 1 ;; + esac + flags=("-DRTS_BUILD_ZEROHOUR=$(onoff "$zero_hour")" + "-DRTS_BUILD_GENERALS=$(onoff "$generals")" + "-DRTS_BUILD_CORE_TOOLS=$(onoff '${{ inputs.tools }}')" + "-DRTS_BUILD_CORE_EXTRAS=$(onoff '${{ inputs.extras }}')" + "-DRTS_BUILD_OPTION_FFMPEG=$(onoff '${{ inputs.ffmpeg }}')") + [[ $game == Generals ]] && flags+=("-DRTS_BUILD_GENERALS_TOOLS=$(onoff '${{ inputs.tools }}')" "-DRTS_BUILD_GENERALS_EXTRAS=$(onoff '${{ inputs.extras }}')") + [[ $game == GeneralsMD ]] && flags+=("-DRTS_BUILD_ZEROHOUR_TOOLS=$(onoff '${{ inputs.tools }}')" "-DRTS_BUILD_ZEROHOUR_EXTRAS=$(onoff '${{ inputs.extras }}')") + case '${{ inputs.retail-compatible }}' in + retail) compatibility=ON ;; + noretail) compatibility=OFF ;; + *) echo 'Unknown compatibility mode' >&2; exit 1 ;; + esac + flags+=("-DRTS_BUILD_OPTION_RETAIL_COMPATIBLE_GAME=$compatibility") + [[ -n ${SCCACHE_PATH:-} ]] && flags+=("-DCMAKE_C_COMPILER_LAUNCHER=$SCCACHE_PATH" "-DCMAKE_CXX_COMPILER_LAUNCHER=$SCCACHE_PATH") + cmake --preset "$preset" "${flags[@]}" diff --git a/.github/actions/setup-sccache/action.yml b/.github/actions/setup-sccache/action.yml new file mode 100644 index 00000000000..7bbef85b3a4 --- /dev/null +++ b/.github/actions/setup-sccache/action.yml @@ -0,0 +1,13 @@ +name: Set Up sccache +description: Install the pinned sccache compiler wrapper and enable its GitHub Actions cache backend. +runs: + using: composite + steps: + - name: Install sccache + uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: v0.17.0 + + - name: Enable GitHub Actions Cache Backend + shell: bash + run: echo 'SCCACHE_GHA_ENABLED=true' >> "$GITHUB_ENV" diff --git a/.github/actions/setup-vc6/action.yml b/.github/actions/setup-vc6/action.yml new file mode 100644 index 00000000000..a94dcd033c2 --- /dev/null +++ b/.github/actions/setup-vc6/action.yml @@ -0,0 +1,26 @@ +name: Set Up VC6 +description: Restore or install the verified VC6 toolchain and export its environment. +runs: + using: composite + steps: + - name: Cache VC6 Installation + id: cache-vc6 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ${{ runner.tool_cache }}/MSVC600/001c4bafdcf2ef4b474d693acccd35a91e848f40 + key: msvc600-permanent-cache-v2 + + - name: Install Verified VC6 Toolchain + if: ${{ steps.cache-vc6.outputs.cache-hit != 'true' }} + env: + VC6_SCRIPT: ${{ github.action_path }}/setup.py + VC6_ROOT: ${{ runner.tool_cache }}/MSVC600/001c4bafdcf2ef4b474d693acccd35a91e848f40 + shell: bash + run: python "$VC6_SCRIPT" --install --root "$VC6_ROOT" + + - name: Set Up VC6 Environment + env: + VC6_SCRIPT: ${{ github.action_path }}/setup.py + VC6_ROOT: ${{ runner.tool_cache }}/MSVC600/001c4bafdcf2ef4b474d693acccd35a91e848f40 + shell: bash + run: python "$VC6_SCRIPT" --export-environment --root "$VC6_ROOT" diff --git a/.github/actions/setup-vc6/setup.py b/.github/actions/setup-vc6/setup.py new file mode 100644 index 00000000000..a7e3fe51d21 --- /dev/null +++ b/.github/actions/setup-vc6/setup.py @@ -0,0 +1,86 @@ +"""Install the pinned portable VC6 toolchain and export its build environment.""" + +import argparse +import hashlib +import os +from pathlib import Path, PureWindowsPath +import shutil +import tempfile +import urllib.request +import zipfile + + +COMMIT = "001c4bafdcf2ef4b474d693acccd35a91e848f40" +EXPECTED_HASH = "D0EE1F6DCEF7DB3AD703120D9FB4FAD49EBCA28F44372E40550348B1C00CA583" +def environment(base, current=None): + """Return VC6 variables, retaining the runner's existing search paths.""" + current = {key.upper(): value for key, value in (os.environ if current is None else current).items()} + base = PureWindowsPath(base) / "VC6SP6" + common = base / "Common" + dev = common / "msdev98" + vc = base / "VC98" + return { + "VSCommonDir": str(common), + "MSDevDir": str(dev), + "MSVCDir": str(vc), + "VcOsDir": "WINNT", + "PATH": ";".join(map(str, [dev / "BIN", vc / "BIN", common / "TOOLS" / "WINNT", common / "TOOLS", current.get("PATH", "")])), + "INCLUDE": ";".join(map(str, [vc / "ATL" / "INCLUDE", vc / "INCLUDE", vc / "MFC" / "INCLUDE", current.get("INCLUDE", "")])), + "LIB": ";".join(map(str, [vc / "LIB", vc / "MFC" / "LIB", current.get("LIB", "")])), + } + + +def install(root): + """Verify the download before extracting any files into the install root.""" + root = Path(root).resolve() + target = root / "VC6SP6" + extracted = root / f"MSVC600-{COMMIT}" + if target.exists() or extracted.exists(): + raise FileExistsError(f"Refusing to overwrite an existing VC6 installation in {root}") + with tempfile.NamedTemporaryFile( + suffix=".zip", delete=False, dir=os.environ.get("RUNNER_TEMP")) as temporary: + archive = Path(temporary.name) + try: + print("Downloading VC6 Portable Installation", flush=True) + urllib.request.urlretrieve(f"https://github.com/itsmattkc/MSVC600/archive/{COMMIT}.zip", archive) + digest = hashlib.sha256() + with archive.open("rb") as hash_source: + for chunk in iter(lambda: hash_source.read(1024 * 1024), b""): + digest.update(chunk) + actual_hash = digest.hexdigest().upper() + print(f"Downloaded file SHA256: {actual_hash}") + if actual_hash != EXPECTED_HASH: + raise ValueError(f"VC6 hash verification failed. Expected {EXPECTED_HASH}") + with zipfile.ZipFile(archive) as zip_source: + for member in zip_source.infolist(): + name = PureWindowsPath(member.filename) + destination = (root / member.filename).resolve() + if (name.is_absolute() or name.drive or ".." in name.parts + or ":" in member.filename or not destination.is_relative_to(root) + or not name.parts or name.parts[0] != extracted.name): + raise ValueError(f"Unsafe VC6 archive path: {member.filename}") + zip_source.extractall(root) + shutil.move(str(extracted), str(target)) + finally: + archive.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + operation = parser.add_mutually_exclusive_group(required=True) + operation.add_argument("--install", action="store_true") + operation.add_argument("--export-environment", action="store_true") + parser.add_argument("--root", type=Path, required=True) + args = parser.parse_args() + if args.install: + install(args.root) + else: + with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as output: + for key, value in environment(args.root).items(): + if "\n" in value or "\r" in value: + raise ValueError(f"Invalid newline in environment variable {key}") + output.write(f"{key}={value}\n") + + +if __name__ == "__main__": + main() diff --git a/.github/actions/test-replays/action.yml b/.github/actions/test-replays/action.yml new file mode 100644 index 00000000000..43864c31cb3 --- /dev/null +++ b/.github/actions/test-replays/action.yml @@ -0,0 +1,58 @@ +name: Test Replays +description: Run GeneralsMD replay compatibility tests against isolated trimmed game data +inputs: + userdata: + description: Repository path containing Replays and Maps directories + required: true + build-artifacts-directory: + description: Directory containing the collected GeneralsMD build artifacts + required: true + runtime-directory: + description: Isolated directory in which to assemble and run the game + required: true + r2-access-key-id: + description: R2 access key used only on a game data cache miss + required: false + default: '' + r2-secret-access-key: + description: R2 secret key used only on a game data cache miss + required: false + default: '' + r2-endpoint-url: + description: R2 endpoint used only on a game data cache miss + required: false + default: '' +runs: + using: composite + steps: + - name: Cache Trimmed Game Data + id: cache-game-data + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ${{ runner.temp }}/generals-replay-data + key: gamedata-permanent-cache-v4 + + - name: Download Trimmed Game Data + if: steps.cache-game-data.outputs.cache-hit != 'true' + shell: bash + env: + REPLAY_DATA_SCRIPT: ${{ github.action_path }}/prepare.py + GAME_DATA_DIRECTORY: ${{ runner.temp }}/generals-replay-data + AWS_ACCESS_KEY_ID: ${{ inputs.r2-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.r2-secret-access-key }} + AWS_ENDPOINT_URL: ${{ inputs.r2-endpoint-url }} + run: python "$REPLAY_DATA_SCRIPT" --game-data-directory "$GAME_DATA_DIRECTORY" + + - name: Stage and Run Replay Compatibility Tests + shell: bash + env: + REPLAY_SCRIPT: ${{ github.action_path }}/run.py + USERDATA: ${{ inputs.userdata }} + BUILD_ARTIFACTS_DIRECTORY: ${{ inputs.build-artifacts-directory }} + RUNTIME_DIRECTORY: ${{ inputs.runtime-directory }} + GAME_DATA_DIRECTORY: ${{ runner.temp }}/generals-replay-data + run: | + python "$REPLAY_SCRIPT" --userdata "$USERDATA" \ + --build-artifacts-directory "$BUILD_ARTIFACTS_DIRECTORY" \ + --runtime-directory "$RUNTIME_DIRECTORY" \ + --game-data-directory "$GAME_DATA_DIRECTORY" diff --git a/.github/actions/test-replays/prepare.py b/.github/actions/test-replays/prepare.py new file mode 100644 index 00000000000..e07aed5d7a0 --- /dev/null +++ b/.github/actions/test-replays/prepare.py @@ -0,0 +1,50 @@ +"""Download and verify trimmed game assets on a replay cache miss.""" + +import argparse +import hashlib +import os +from pathlib import Path +import subprocess +import tempfile + + +ARCHIVES = ( + ("Generals", "generals108_gamedata_trimmed.7z", + "37A351AA430199D1F05DEB9E404857DCE7B461A6AC272C5D4A0B5652CDB06372"), + ("GeneralsMD", "zerohour104_gamedata_trimmed.7z", + "6837FE1E3009A4C239406C39B1598216C0943EE8ED46BB10626767029AC05E21"), +) + + +def prepare_game_data(game_data_directory): + if not all(os.environ.get(key) for key in ( + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_ENDPOINT_URL")): + raise RuntimeError("Game data cache is cold and required R2 credentials are unavailable. " + "Replay validation cannot run.") + game_data_directory = Path(game_data_directory) + with tempfile.TemporaryDirectory(prefix="replay-download-", dir=os.environ.get("RUNNER_TEMP")) as temporary: + for game, filename, expected_hash in ARCHIVES: + archive = Path(temporary) / filename + print(f"Downloading trimmed game data for {game}.") + subprocess.run(["aws", "s3", "cp", f"s3://github-ci/{filename}", str(archive), + "--endpoint-url", os.environ["AWS_ENDPOINT_URL"]], check=True) + digest = hashlib.sha256() + with archive.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + if digest.hexdigest().upper() != expected_hash: + raise RuntimeError(f"Hash verification failed for {game} game data") + destination = game_data_directory / game + destination.mkdir(parents=True, exist_ok=True) + subprocess.run(["7z", "x", str(archive), f"-o{destination}", "-y"], check=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--game-data-directory", type=Path, required=True) + args = parser.parse_args() + prepare_game_data(args.game_data_directory) + + +if __name__ == "__main__": + main() diff --git a/.github/actions/test-replays/run.py b/.github/actions/test-replays/run.py new file mode 100644 index 00000000000..7e7260d22b8 --- /dev/null +++ b/.github/actions/test-replays/run.py @@ -0,0 +1,106 @@ +"""Stage an isolated Zero Hour runtime and check replay compatibility.""" + +import argparse +import os +from pathlib import Path +import shutil +import subprocess + + +def register_generals(generals_directory): + """Zero Hour resolves base-game assets through this per-user registry key.""" + import winreg + + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, + r"SOFTWARE\Electronic Arts\EA Games\Generals") as key: + winreg.SetValueEx(key, "InstallPath", 0, winreg.REG_SZ, + str(generals_directory.resolve()) + "\\") + + +def stage_runtime(userdata, build_artifacts_directory, runtime_directory, + game_data_directory, user_directory=None): + userdata = Path(userdata) + build_artifacts_directory = Path(build_artifacts_directory) + runtime_directory = Path(runtime_directory).resolve() + game_data_directory = Path(game_data_directory) + for directory in (game_data_directory / "Generals", game_data_directory / "GeneralsMD", + build_artifacts_directory, userdata / "Replays", userdata / "Maps"): + if not directory.is_dir(): + raise ValueError(f"Required replay source directory not found: {directory}") + if not (build_artifacts_directory / "generalszh.exe").is_file(): + raise ValueError(f"Replay executable missing from build artifacts: {build_artifacts_directory}") + if not any(p.is_file() and p.suffix.lower() == ".rep" for p in (userdata / "Replays").iterdir()): + raise ValueError(f"No replay fixtures found in: {userdata / 'Replays'}") + if runtime_directory.exists() and (not runtime_directory.is_dir() or any(runtime_directory.iterdir())): + raise ValueError(f"Replay runtime must be empty to avoid stale binaries or logs: {runtime_directory}") + + # Cached proprietary data stays immutable. Built binaries always win. + shutil.copytree(game_data_directory / "GeneralsMD", runtime_directory, dirs_exist_ok=True) + shutil.copytree(build_artifacts_directory, runtime_directory, dirs_exist_ok=True) + register_generals(game_data_directory / "Generals") + if user_directory is None: + user_directory = Path(os.environ["USERPROFILE"]) / "Documents" / "Command and Conquer Generals Zero Hour Data" + for fixture_type in ("Replays", "Maps"): + shutil.copytree(userdata / fixture_type, Path(user_directory) / fixture_type, dirs_exist_ok=True) + return runtime_directory + + +def run_replays(runtime_directory, timeout_seconds=600): + if timeout_seconds <= 0: + raise ValueError("Replay timeout must be positive") + runtime_directory = Path(runtime_directory).resolve() + executable = runtime_directory / "generalszh.exe" + if not executable.is_file(): + raise ValueError(f"Replay executable not found: {executable}") + logs = [runtime_directory / "stdout.log", runtime_directory / "stderr.log"] + kwargs = {} + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + kwargs.update(startupinfo=startupinfo, creationflags=subprocess.CREATE_NO_WINDOW) + try: + with logs[0].open("w") as stdout, logs[1].open("w") as stderr: + process = subprocess.Popen( + [str(executable), "-jobs", "4", "-headless", "-replay", "*.rep"], + cwd=runtime_directory, stdout=stdout, stderr=stderr, **kwargs) + try: + exit_code = process.wait(timeout=timeout_seconds) + except subprocess.TimeoutExpired: + try: + if os.name == "nt": + subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"], + check=True, creationflags=subprocess.CREATE_NO_WINDOW) + else: + process.kill() + finally: + # Ensure the parent is reaped even when taskkill reports a race. + if process.poll() is None: + process.kill() + process.wait() + raise RuntimeError(f"Replay tests timed out after {timeout_seconds} seconds") from None + if exit_code != 0: + raise RuntimeError(f"Replay tests failed with exit code {exit_code}") + print("Replay compatibility tests passed.") + finally: + for log in logs: + print(f"=== {log.name} ===") + if log.exists(): + print(log.read_text(errors="replace")) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--userdata", required=True, type=Path) + parser.add_argument("--build-artifacts-directory", required=True, type=Path) + parser.add_argument("--runtime-directory", required=True, type=Path) + parser.add_argument("--game-data-directory", type=Path, required=True) + parser.add_argument("--timeout-seconds", type=int, default=600) + args = parser.parse_args() + runtime = stage_runtime(args.userdata, args.build_artifacts_directory, + args.runtime_directory, args.game_data_directory) + run_replays(runtime, args.timeout_seconds) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/_build-toolchain.yml b/.github/workflows/_build-toolchain.yml new file mode 100644 index 00000000000..a1f0d2c58a4 --- /dev/null +++ b/.github/workflows/_build-toolchain.yml @@ -0,0 +1,179 @@ +name: Build Toolchain + +# WARNING: Do not declare permissions in this reusable workflow. +# Declare the required permissions in each calling job: +# contents: read - required for all builds. +# packages: write - required for vcpkg binary-cache uploads only. + +on: + workflow_call: + inputs: + game: + required: true + type: string + description: "Game to build (Generals or GeneralsMD)" + preset: + required: true + type: string + description: "CMake preset" + tools: + required: false + default: true + type: boolean + description: "Build tools" + extras: + required: false + default: false + type: boolean + description: "Build extras" + ffmpeg: + required: false + default: false + type: boolean + description: "Build with FFmpeg support" + retail-compatible: + required: false + default: DEFAULT + type: string + description: "Compatibility mode (retail or noretail)" + run-replay-tests: + required: false + default: false + type: boolean + description: "Run Zero Hour replay compatibility checks after publishing build artifacts" + +jobs: + build: + name: Build + runs-on: windows-2022 + timeout-minutes: ${{ inputs.run-replay-tests && 45 || 30 }} + + env: + BUILD_GAME: ${{ inputs.game }} + BUILD_PRESET: ${{ inputs.preset }} + BUILD_TOOLS: ${{ inputs.tools }} + BUILD_EXTRAS: ${{ inputs.extras }} + BUILD_FFMPEG: ${{ inputs.ffmpeg }} + BUILD_RETAIL_COMPATIBLE: ${{ inputs.retail-compatible }} + # Fork pull requests get a read-only GITHUB_TOKEN, so they restore from the + # feed but must not attempt to upload to it. + VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json,${{ github.event.pull_request.head.repo.fork && 'read' || 'readwrite' }}" + VCPKG_NUGET_REPOSITORY: "https://github.com/${{ github.repository }}.git" + VCPKG_FEATURE_FLAGS: manifests,versions,binarycaching + # Metrics make every vcpkg run open its own exe exclusively to copy itself into + # TEMP for the telemetry upload, which randomly blocks the next vcpkg launch. + VCPKG_DISABLE_METRICS: "1" + + steps: + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: ${{ inputs.run-replay-tests }} + + - name: Set Up VC6 + if: startsWith(inputs.preset, 'vc6') + uses: ./.github/actions/setup-vc6 + + - name: Cache CMake Dependencies + id: cache-cmake-deps + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: build\${{ inputs.preset }}\_deps + key: cmake-deps-${{ inputs.preset }}-${{ hashFiles('CMakePresets.json','cmake/**/*.cmake','**/CMakeLists.txt') }} + + - name: Set Up VC2022 Environment + if: startsWith(inputs.preset, 'win32') + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + with: + arch: x86 + + - name: Set Up Shared sccache + if: startsWith(inputs.preset, 'win32') + uses: ./.github/actions/setup-sccache + + - name: Setup vcpkg + if: contains(inputs.preset, 'vcpkg') + uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + with: + runVcpkgInstall: false + doNotCache: true + + - name: Configure NuGet Source for vcpkg Binary Cache + if: contains(inputs.preset, 'vcpkg') + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + nuget="$("$VCPKG_ROOT/vcpkg.exe" fetch nuget | tail -n 1 | tr -d '\"')" + source="https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" + "$nuget" sources add -Source "$source" -StorePasswordInClearText -Name GitHubPackages -UserName "${{ github.repository_owner }}" -Password "$GITHUB_TOKEN" + "$nuget" setapikey "$GITHUB_TOKEN" -Source "$source" + echo 'VCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions' >> "$GITHUB_ENV" + + - name: Configure ${{ inputs.game }} with CMake Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}${{ inputs.ffmpeg && '+ffmpeg' || '' }} Preset + uses: ./.github/actions/configure-build + with: + game: ${{ inputs.game }} + preset: ${{ inputs.preset }} + tools: ${{ inputs.tools }} + extras: ${{ inputs.extras }} + ffmpeg: ${{ inputs.ffmpeg }} + retail-compatible: ${{ inputs.retail-compatible }} + + - name: Build ${{ inputs.game }} with CMake Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Preset + shell: bash + run: | + cmake --build --preset "$BUILD_PRESET" + + - name: Report sccache Statistics + if: always() && startsWith(inputs.preset, 'win32') + shell: bash + run: '"$SCCACHE_PATH" --show-stats' + + - name: Collect ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact + uses: ./.github/actions/collect-artifacts + with: + game: ${{ inputs.game }} + preset: ${{ inputs.preset }} + + # Publish compiler output before introducing private replay data. + - name: Upload Generals Artifact + if: inputs.game == 'Generals' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: Generals-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}${{ inputs.retail-compatible == 'noretail' && '-noretail' || '' }} + path: build\${{ inputs.preset }}\Generals\artifacts + retention-days: 30 + if-no-files-found: error + + - name: Upload GeneralsMD Artifact + if: inputs.game == 'GeneralsMD' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: GeneralsMD-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}${{ inputs.retail-compatible == 'noretail' && '-noretail' || '' }} + path: build\${{ inputs.preset }}\GeneralsMD\artifacts + retention-days: 30 + if-no-files-found: error + + - name: Test Replays + if: inputs.run-replay-tests + uses: ./.github/actions/test-replays + with: + userdata: GeneralsReplays/GeneralsZH/1.04 + build-artifacts-directory: build/${{ inputs.preset }}/GeneralsMD/artifacts + runtime-directory: ${{ runner.temp }}/replay-runtime + r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} + r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} + r2-endpoint-url: ${{ secrets.R2_ENDPOINT_URL }} + + - name: Upload Replay Debug Log + if: always() && inputs.run-replay-tests + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: Replay-Debug-Log-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}${{ inputs.retail-compatible == 'noretail' && '-noretail' || '' }} + path: | + ${{ runner.temp }}/replay-runtime/DebugLogFile*.txt + ${{ runner.temp }}/replay-runtime/stdout.log + ${{ runner.temp }}/replay-runtime/stderr.log + retention-days: 30 + if-no-files-found: ignore diff --git a/.github/workflows/build-historical.yml b/.github/workflows/build-historical.yml index 1d916e90783..f83db20f27a 100644 --- a/.github/workflows/build-historical.yml +++ b/.github/workflows/build-historical.yml @@ -1,7 +1,7 @@ name: Build Historical permissions: - contents: write + contents: read on: workflow_dispatch: @@ -134,74 +134,40 @@ jobs: echo "::endgroup::" done - - name: Cache VC6 Installation - id: cache-vc6 - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + # Historical targets may predate these helpers. Use the workflow's exact + # revision, separately from the source being built and any cherry-picked PRs. + - name: Checkout Build Helpers + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - path: C:\VC6 - key: vc6-permanent-cache-v2 - - - name: Download VC6 Portable - if: ${{ steps.cache-vc6.outputs.cache-hit != 'true' }} - env: - EXPECTED_HASH: "D0EE1F6DCEF7DB3AD703120D9FB4FAD49EBCA28F44372E40550348B1C00CA583" - VC6_COMMIT: "001c4bafdcf2ef4b474d693acccd35a91e848f40" - shell: pwsh - run: | - Invoke-WebRequest -Uri https://github.com/itsmattkc/MSVC600/archive/$env:VC6_COMMIT.zip -OutFile VS6_VisualStudio6.zip + ref: ${{ github.workflow_sha }} + path: .ci-workflow + persist-credentials: false + sparse-checkout: | + .github/actions/setup-vc6 + .github/actions/collect-artifacts - $fileHash = (Get-FileHash -Path VS6_VisualStudio6.zip -Algorithm SHA256).Hash - if ($fileHash -ne $env:EXPECTED_HASH) { - Write-Error "Hash verification failed!" - exit 1 - } - - Expand-Archive -Path VS6_VisualStudio6.zip -DestinationPath C:\VC6 - Move-Item -Path C:\VC6\MSVC600-$env:VC6_COMMIT -Destination C:\VC6\VC6SP6 - Remove-Item VS6_VisualStudio6.zip - - - name: Set Up VC6 Environment - shell: pwsh - run: | - $VSCommonDir = "C:\VC6\VC6SP6\Common" - $MSDevDir = "C:\VC6\VC6SP6\Common\msdev98" - $MSVCDir = "C:\VC6\VC6SP6\VC98" - $VcOsDir = "WINNT" - - "VSCommonDir=$VSCommonDir" >> $env:GITHUB_ENV - "MSDevDir=$MSDevDir" >> $env:GITHUB_ENV - "MSVCDir=$MSVCDir" >> $env:GITHUB_ENV - "VcOsDir=$VcOsDir" >> $env:GITHUB_ENV - "PATH=$MSDevDir\BIN;$MSVCDir\BIN;$VSCommonDir\TOOLS\$VcOsDir;$VSCommonDir\TOOLS;$env:PATH" >> $env:GITHUB_ENV - "INCLUDE=$MSVCDir\ATL\INCLUDE;$MSVCDir\INCLUDE;$MSVCDir\MFC\INCLUDE;$env:INCLUDE" >> $env:GITHUB_ENV - "LIB=$MSVCDir\LIB;$MSVCDir\MFC\LIB;$env:LIB" >> $env:GITHUB_ENV + - name: Set Up VC6 + uses: ./.ci-workflow/.github/actions/setup-vc6 - name: Configure with CMake - shell: pwsh + shell: bash run: | - $buildFlags = @( - "-DRTS_BUILD_ZEROHOUR=${{ matrix.game == 'GeneralsMD' && 'ON' || 'OFF' }}", - "-DRTS_BUILD_GENERALS=${{ matrix.game == 'Generals' && 'ON' || 'OFF' }}", - "-DRTS_BUILD_CORE_TOOLS=ON", - "-DRTS_BUILD_${{ matrix.game == 'Generals' && 'GENERALS' || 'ZEROHOUR' }}_TOOLS=ON" - ) - cmake --preset vc6 $buildFlags + game='${{ matrix.game }}' + prefix=$([[ $game == Generals ]] && echo GENERALS || echo ZEROHOUR) + cmake --preset vc6 \ + "-DRTS_BUILD_ZEROHOUR=$([[ $game == GeneralsMD ]] && echo ON || echo OFF)" \ + "-DRTS_BUILD_GENERALS=$([[ $game == Generals ]] && echo ON || echo OFF)" \ + -DRTS_BUILD_CORE_TOOLS=ON "-DRTS_BUILD_${prefix}_TOOLS=ON" - name: Build with CMake - shell: pwsh - run: | - cmake --build --preset vc6 + shell: bash + run: cmake --build --preset vc6 - name: Collect Artifacts - shell: pwsh - run: | - $buildDir = "build\vc6" - $artifactsDir = New-Item -ItemType Directory -Force -Path "$buildDir\${{ matrix.game }}\artifacts" - - $files = Get-ChildItem -Path "$buildDir\Core","$buildDir\${{ matrix.game }}" -File | - Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } - - $files | Move-Item -Destination $artifactsDir -Force + uses: ./.ci-workflow/.github/actions/collect-artifacts + with: + game: ${{ matrix.game }} + preset: vc6 - name: Upload Artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -215,6 +181,8 @@ jobs: needs: [prepare, build] if: ${{ github.event.inputs.create_release == 'true' }} runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Download Generals Artifact if: ${{ github.event.inputs.game == 'Generals' || github.event.inputs.game == 'Both' }} diff --git a/.github/workflows/build-toolchain.yml b/.github/workflows/build-toolchain.yml deleted file mode 100644 index 55441827fb7..00000000000 --- a/.github/workflows/build-toolchain.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: Build Toolchain - -# No permissions here - callers grant contents: read, and packages: write if using vcpkg. - -on: - workflow_call: - inputs: - game: - required: true - type: string - description: "Game to build (Generals, GeneralsMD)" - preset: - required: true - type: string - description: "CMake preset" - tools: - required: false - default: true - type: boolean - description: "Build tools" - extras: - required: false - default: false - type: boolean - description: "Build extras" - ffmpeg: - required: false - default: false - type: boolean - description: "Build with FFmpeg support" - -jobs: - build: - name: ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} - runs-on: windows-2022 - timeout-minutes: 30 - - env: - # Fork pull requests get a read-only GITHUB_TOKEN, so they restore from the - # feed but must not attempt to upload to it. - VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json,${{ github.event.pull_request.head.repo.fork && 'read' || 'readwrite' }}" - VCPKG_NUGET_REPOSITORY: "https://github.com/${{ github.repository }}.git" - VCPKG_FEATURE_FLAGS: manifests,versions,binarycaching - # Metrics make every vcpkg run open its own exe exclusively to copy itself into - # TEMP for the telemetry upload, which randomly blocks the next vcpkg launch. - VCPKG_DISABLE_METRICS: "1" - - steps: - - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Cache VC6 Installation - if: startsWith(inputs.preset, 'vc6') - id: cache-vc6 - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 - with: - path: C:\VC6 - key: vc6-permanent-cache-v2 - - - name: Cache CMake Dependencies - id: cache-cmake-deps - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 - with: - path: build\${{ inputs.preset }}\_deps - key: cmake-deps-${{ inputs.preset }}-${{ hashFiles('CMakePresets.json','cmake/**/*.cmake','**/CMakeLists.txt') }} - - - name: Download VC6 Portable from itsmattkc repo - if: ${{ startsWith(inputs.preset, 'vc6') && steps.cache-vc6.outputs.cache-hit != 'true' }} - env: - EXPECTED_HASH: "D0EE1F6DCEF7DB3AD703120D9FB4FAD49EBCA28F44372E40550348B1C00CA583" - COMMIT: "001c4bafdcf2ef4b474d693acccd35a91e848f40" - shell: pwsh - run: | - Write-Host "Downloading VC6 Portable Installation" -ForegroundColor Cyan - Invoke-WebRequest -Uri https://github.com/itsmattkc/MSVC600/archive/$env:COMMIT.zip -OutFile VS6_VisualStudio6.zip - - Write-Host "Verifying File Integrity" -ForegroundColor Cyan - $fileHash = (Get-FileHash -Path VS6_VisualStudio6.zip -Algorithm SHA256).Hash - Write-Host "Downloaded file SHA256: $fileHash" - Write-Host "Expected file SHA256: $env:EXPECTED_HASH" - if ($fileHash -ne $env:EXPECTED_HASH) { - Write-Error "Hash verification failed! File may be corrupted or tampered with." - exit 1 - } - - Write-Host "Extracting Archive" -ForegroundColor Cyan - & Expand-Archive -Path VS6_VisualStudio6.zip -DestinationPath C:\VC6 - Move-Item -Path C:\VC6\MSVC600-$env:COMMIT -Destination C:\VC6\VC6SP6 - Remove-Item VS6_VisualStudio6.zip -Verbose - - - name: Set Up VC6 Environment - if: startsWith(inputs.preset, 'vc6') - shell: pwsh - run: | - # Define the base directories as local variables first - $VSCommonDir = "C:\VC6\VC6SP6\Common" - $MSDevDir = "C:\VC6\VC6SP6\Common\msdev98" - $MSVCDir = "C:\VC6\VC6SP6\VC98" - $VcOsDir = "WINNT" - - # Set the variables in GitHub environment - "VSCommonDir=$VSCommonDir" >> $env:GITHUB_ENV - "MSDevDir=$MSDevDir" >> $env:GITHUB_ENV - "MSVCDir=$MSVCDir" >> $env:GITHUB_ENV - "VcOsDir=$VcOsDir" >> $env:GITHUB_ENV - "PATH=$MSDevDir\BIN;$MSVCDir\BIN;$VSCommonDir\TOOLS\$VcOsDir;$VSCommonDir\TOOLS;$env:PATH" >> $env:GITHUB_ENV - "INCLUDE=$MSVCDir\ATL\INCLUDE;$MSVCDir\INCLUDE;$MSVCDir\MFC\INCLUDE;$env:INCLUDE" >> $env:GITHUB_ENV - "LIB=$MSVCDir\LIB;$MSVCDir\MFC\LIB;$env:LIB" >> $env:GITHUB_ENV - - - name: Set Up VC2022 Environment - if: startsWith(inputs.preset, 'win32') - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 - with: - arch: x86 - - - name: Setup vcpkg - if: contains(inputs.preset, 'vcpkg') - uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 - with: - runVcpkgInstall: false - doNotCache: true - - - name: Configure NuGet Source for vcpkg Binary Cache - if: contains(inputs.preset, 'vcpkg') - shell: pwsh - run: | - $nuget = & "$env:VCPKG_ROOT\vcpkg.exe" fetch nuget | Select-Object -Last 1 - & $nuget sources add ` - -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" ` - -StorePasswordInClearText ` - -Name GitHubPackages ` - -UserName "${{ github.repository_owner }}" ` - -Password "${{ secrets.GITHUB_TOKEN }}" - & $nuget setapikey "${{ secrets.GITHUB_TOKEN }}" ` - -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - "VCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions" >> $env:GITHUB_ENV - - - name: Configure ${{ inputs.game }} with CMake Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }}${{ inputs.ffmpeg && '+ffmpeg' || '' }} Preset - shell: pwsh - run: | - $buildFlags = @( - "-DRTS_BUILD_ZEROHOUR=${{ inputs.game == 'GeneralsMD' && 'ON' || 'OFF' }}", - "-DRTS_BUILD_GENERALS=${{ inputs.game == 'Generals' && 'ON' || 'OFF' }}" - ) - - $gamePrefix = "${{ inputs.game == 'Generals' && 'GENERALS' || 'ZEROHOUR' }}" - $buildFlags += "-DRTS_BUILD_CORE_TOOLS=${{ inputs.tools && 'ON' || 'OFF' }}" - $buildFlags += "-DRTS_BUILD_${gamePrefix}_TOOLS=${{ inputs.tools && 'ON' || 'OFF' }}" - $buildFlags += "-DRTS_BUILD_CORE_EXTRAS=${{ inputs.extras && 'ON' || 'OFF' }}" - $buildFlags += "-DRTS_BUILD_${gamePrefix}_EXTRAS=${{ inputs.extras && 'ON' || 'OFF' }}" - $buildFlags += "-DRTS_BUILD_OPTION_FFMPEG=${{ inputs.ffmpeg && 'ON' || 'OFF' }}" - - Write-Host "Build flags: $($buildFlags -join ' | ')" - cmake --preset ${{ inputs.preset }} $buildFlags - - - name: Build ${{ inputs.game }} with CMake Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Preset - shell: pwsh - run: | - cmake --build --preset ${{ inputs.preset }} - - - name: Collect ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact - shell: pwsh - run: | - $buildDir = "build\${{ inputs.preset }}" - $artifactsDir = New-Item -ItemType Directory -Force -Path "$buildDir\${{ inputs.game }}\artifacts" -Verbose - - if ("${{ inputs.preset }}" -like "win32*") { - $configToUse = if ("${{ inputs.preset }}" -match "debug") { "Debug" } else { "Release" } - $files = Get-ChildItem -Path "$buildDir\Core\$configToUse","$buildDir\${{ inputs.game }}\$configToUse" -File | - Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } -Verbose - } else { - $files = Get-ChildItem -Path "$buildDir\Core","$buildDir\${{ inputs.game }}" -File | - Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } -Verbose - } - - $files | Move-Item -Destination $artifactsDir -Verbose -Force - - - name: Upload ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ inputs.game }}-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} - path: build\${{ inputs.preset }}\${{ inputs.game }}\artifacts - retention-days: 30 - if-no-files-found: error diff --git a/.github/workflows/check-replays.yml b/.github/workflows/check-replays.yml deleted file mode 100644 index 5f27c600bef..00000000000 --- a/.github/workflows/check-replays.yml +++ /dev/null @@ -1,242 +0,0 @@ -name: Check Replays - -permissions: - contents: read - pull-requests: write - -on: - workflow_call: - inputs: - game: - required: true - type: string - description: "Game to check (only GeneralsMD for now)" - userdata: - required: true - type: string - description: "Path to folder with replays and maps" - preset: - required: true - type: string - description: "CMake preset" - -jobs: - build: - name: ${{ inputs.preset }} - runs-on: windows-2022 - timeout-minutes: 15 - env: - GAME_PATH: C:\GameData - GENERALS_PATH: C:\GameData\Generals - GENERALSMD_PATH: C:\GameData\GeneralsMD - steps: - - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - submodules: true - - - name: Download Game Artifact - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ inputs.game }}-${{ inputs.preset }} - path: build - - - name: Cache Game Data - id: cache-gamedata - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 - with: - path: ${{ env.GAME_PATH }} - key: gamedata-permanent-cache-v4 - - - name: Download Game Data from Cloudflare R2 - if: ${{ steps.cache-gamedata.outputs.cache-hit != 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} - AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }} - EXPECTED_HASH_GENERALS: "37A351AA430199D1F05DEB9E404857DCE7B461A6AC272C5D4A0B5652CDB06372" - EXPECTED_HASH_GENERALSMD: "6837FE1E3009A4C239406C39B1598216C0943EE8ED46BB10626767029AC05E21" - shell: pwsh - run: | - # Download trimmed gamedata of both Generals 1.08 and Generals Zero Hour 1.04. - # This data cannot be used for playing because it's - # missing textures, audio and gui files. But it's enough for replay checking. - # It's also encrypted because it's not allowed to distribute these files. - - if (-not $env:AWS_ACCESS_KEY_ID -or -not $env:AWS_SECRET_ACCESS_KEY -or -not $env:AWS_ENDPOINT_URL) { - $ok1 = [bool]$env:AWS_ACCESS_KEY_ID - $ok2 = [bool]$env:AWS_SECRET_ACCESS_KEY - $ok3 = [bool]$env:AWS_ENDPOINT_URL - Write-Host "One or more required secrets are not set or are empty. R2_ACCESS_KEY_ID: $ok1, R2_SECRET_ACCESS_KEY: $ok2, R2_ENDPOINT_URL: $ok3" - exit 1 - } - - # Download Generals Game Files - # The archive contains these files: - # BINKW32.DLL - # English.big - # INI.big - # Maps.big - # mss32.dll - # W3D.big - # Data\Scripts\MultiplayerScripts.scb - # Data\Scripts\SkirmishScripts.scb - - Write-Host "Downloading Game Data for Generals" -ForegroundColor Cyan - aws s3 cp s3://github-ci/generals108_gamedata_trimmed.7z generals108_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL - - Write-Host "Verifying File Integrity" -ForegroundColor Cyan - $fileHash = (Get-FileHash -Path generals108_gamedata_trimmed.7z -Algorithm SHA256).Hash - Write-Host "Downloaded file SHA256: $fileHash" - Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALS" - if ($fileHash -ne $env:EXPECTED_HASH_GENERALS) { - Write-Error "Hash verification failed! File may be corrupted or tampered with." - exit 1 - } - - Write-Host "Extracting Archive" -ForegroundColor Cyan - $extractPath = $env:GENERALS_PATH - & 7z x generals108_gamedata_trimmed.7z -o"$extractPath" - Remove-Item generals108_gamedata_trimmed.7z -Verbose - - # Download GeneralsMD (ZH) Game Files - # The archive contains these files: - # BINKW32.DLL - # INIZH.big - # MapsZH.big - # mss32.dll - # W3DZH.big - # Data\Scripts\MultiplayerScripts.scb - # Data\Scripts\Scripts.ini - # Data\Scripts\SkirmishScripts.scb - - Write-Host "Downloading Game Data for GeneralsMD" -ForegroundColor Cyan - aws s3 cp s3://github-ci/zerohour104_gamedata_trimmed.7z zerohour104_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL - - Write-Host "Verifying File Integrity" -ForegroundColor Cyan - $fileHash = (Get-FileHash -Path zerohour104_gamedata_trimmed.7z -Algorithm SHA256).Hash - Write-Host "Downloaded file SHA256: $fileHash" - Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALSMD" - if ($fileHash -ne $env:EXPECTED_HASH_GENERALSMD) { - Write-Error "Hash verification failed! File may be corrupted or tampered with." - exit 1 - } - - Write-Host "Extracting Archive" -ForegroundColor Cyan - $extractPath = $env:GENERALSMD_PATH - & 7z x zerohour104_gamedata_trimmed.7z -o"$extractPath" - Remove-Item zerohour104_gamedata_trimmed.7z -Verbose - - - name: Set Up Game Data - shell: pwsh - run: | - $source = "$env:GAME_PATH\${{ inputs.game }}" - $destination = "build" - Copy-Item -Path $source\* -Destination $destination -Recurse -Force - - - name: Set Generals InstallPath in Registry - shell: pwsh - run: | - # Zero Hour loads some Generals files and needs this registry key to find the - # Generals data files. - - $regPath = "HKCU:\SOFTWARE\Electronic Arts\EA Games\Generals" - $installPath = "$env:GENERALS_PATH\" - - # Ensure the key exists - if (-not (Test-Path $regPath)) { - New-Item -Path $regPath -Force | Out-Null - } - - # Set the InstallPath value - Set-ItemProperty -Path $regPath -Name InstallPath -Value $installPath -Type String - Write-Host "Registry key set: $regPath -> InstallPath = $installPath" - - - name: Move Replays and Maps to User Dir - shell: pwsh - run: | - # These files are expected in the user dir, so we move them here. - - $source = "${{ inputs.userdata }}\Replays" - $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Replays" - Write-Host "Move replays to $destination" - New-Item -ItemType Directory -Path $destination -Force | Out-Null - Move-Item -Path "$source\*" -Destination $destination -Force - - $source = "${{ inputs.userdata }}\Maps" - $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Maps" - Write-Host "Move maps to $destination" - New-Item -ItemType Directory -Path $destination -Force | Out-Null - Move-Item -Path "$source\*" -Destination $destination -Force - - - name: Run Replay Compatibility Tests - shell: pwsh - run: | - $exePath = "build/generalszh.exe" - $arguments = "-jobs 4 -headless -replay *.rep" - $timeoutSeconds = 10*60 - $stdoutPath = "stdout.log" - $stderrPath = "stderr.log" - - if (-not (Test-Path $exePath)) { - Write-Host "ERROR: Executable not found at $exePath" - exit 1 - } - - # Note that the game is a gui application. That means we need to redirect console output to a file - # in order to retrieve it. - # Clean previous logs - Remove-Item $stdoutPath, $stderrPath -ErrorAction SilentlyContinue - - # Start the process - Write-Host "Run $exePath $arguments" - $process = Start-Process -FilePath $exePath ` - -ArgumentList $arguments ` - -RedirectStandardOutput $stdoutPath ` - -RedirectStandardError $stderrPath ` - -PassThru - - # Wait with timeout - $exited = $process.WaitForExit($timeoutSeconds * 1000) - - if (-not $exited) { - Write-Host "ERROR: Process still running after $timeoutSeconds seconds. Killing process..." - Stop-Process -Id $process.Id -Force - } - - # Read output - Write-Host "=== STDOUT ===" - Get-Content $stdoutPath - - if ((Test-Path $stderrPath) -and (Get-Item $stderrPath).Length -gt 0) { - Write-Host "`n=== STDERR ===" - Get-Content $stderrPath - } - - if (-not $exited) { - exit 1 - } - - # Check exit code - $exitCode = $process.ExitCode - - # The above doesn't work on all Windows versions. If not, try this: (see https://stackoverflow.com/a/16018287) - #$process.HasExited | Out-Null # Needs to be called for the command below to work correctly - #$exitCode = $process.GetType().GetField('exitCode', 'NonPublic, Instance').GetValue($process) - #Write-Host "exit code $exitCode" - - if ($exitCode -ne 0) { - Write-Host "ERROR: Process failed with exit code $exitCode" - exit $exitCode - } - - Write-Host "Success!" - - - name: Upload Debug Log - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: Replay-Debug-Log-${{ inputs.preset }} - path: build/DebugLogFile*.txt - retention-days: 30 - if-no-files-found: ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5534a95ad29..5324755703c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,190 +1,81 @@ -name: GenCI - -permissions: - contents: read +name: CI on: push: - branches: - - main + branches: [main] pull_request: - branches: - - main - workflow_dispatch: + branches: [main] concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - detect-changes: - name: Detect File Changes - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - generals: ${{ steps.filter.outputs.generals }} - generalsmd: ${{ steps.filter.outputs.generalsmd }} - shared: ${{ steps.filter.outputs.shared }} - steps: - - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Filter Changed Paths - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: filter - with: - token: '' - filters: | - generals: - - 'Generals/**' - generalsmd: - - 'GeneralsMD/**' - shared: - - '.github/workflows/**' - - 'CMakeLists.txt' - - 'CMakePresets.json' - - 'cmake/**' - - 'Core/**' - - 'Dependencies/**' - - 'vcpkg.json' - - - name: Changes Summary - run: | - echo "### 🔍 File Changes Summary" >> $GITHUB_STEP_SUMMARY - echo "- Generals: ${{ steps.filter.outputs.generals == 'true' && '✅' || '❌' }}" >> $GITHUB_STEP_SUMMARY - echo "- GeneralsMD: ${{ steps.filter.outputs.generalsmd == 'true' && '✅' || '❌' }}" >> $GITHUB_STEP_SUMMARY - echo "- Shared: ${{ steps.filter.outputs.shared == 'true' && '✅' || '❌' }}" >> $GITHUB_STEP_SUMMARY - - build-generals: - name: Build Generals${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} + build: + name: ${{ matrix.game }} ${{ matrix.preset }} ${{ matrix.mode }}${{ matrix.tools && ' tools' || ' no-tools' }}${{ matrix.extras && ' extras' || ' no-extras' }}${{ matrix.ffmpeg && ' ffmpeg' || '' }} permissions: contents: read - packages: write strategy: - matrix: - include: - - preset: "vc6" - tools: true - extras: true - - preset: "vc6-profile" - tools: true - extras: true - - preset: "vc6-debug" - tools: true - extras: true - - preset: "win32" - tools: true - extras: true - - preset: "win32-profile" - tools: true - extras: true - - preset: "win32-debug" - tools: true - extras: true - - preset: "win32-vcpkg" - tools: true - extras: true - - preset: "win32-vcpkg-profile" - tools: true - extras: true - - preset: "win32-vcpkg-debug" - tools: true - extras: true fail-fast: false - uses: ./.github/workflows/build-toolchain.yml - with: - game: "Generals" - preset: ${{ matrix.preset }} - tools: ${{ matrix.tools }} - extras: ${{ matrix.extras }} - secrets: inherit - - # Note build-generalsmd is split into two jobs for vc6 and win32 because replaycheck-generalsmd - # only requires the vc6 build and compiling vc6 is much faster than win32 - build-generalsmd-vc6: - name: Build GeneralsMD${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - strategy: matrix: include: - - preset: "vc6" - tools: true - extras: true - - preset: "vc6-profile" - tools: true - extras: true - - preset: "vc6-debug" - tools: true - extras: true - - preset: "vc6-releaselog" - tools: true - extras: true - fail-fast: false - uses: ./.github/workflows/build-toolchain.yml + - { game: Generals, preset: vc6, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: vc6, mode: retail, tools: true, extras: true, ffmpeg: false, replays: true } + - { game: Generals, preset: vc6-profile, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: vc6-profile, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: vc6-debug, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: vc6-debug, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: vc6-releaselog, mode: retail, tools: true, extras: true, ffmpeg: false, replays: true } + - { game: Generals, preset: win32, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: win32, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: win32-profile, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32-profile, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: win32-profile, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32-profile, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: win32-debug, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32-debug, mode: noretail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: Generals, preset: win32-debug, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + - { game: GeneralsMD, preset: win32-debug, mode: retail, tools: true, extras: true, ffmpeg: false, replays: false } + uses: ./.github/workflows/_build-toolchain.yml with: - game: "GeneralsMD" + game: ${{ matrix.game }} preset: ${{ matrix.preset }} tools: ${{ matrix.tools }} extras: ${{ matrix.extras }} + ffmpeg: ${{ matrix.ffmpeg }} + retail-compatible: ${{ matrix.mode }} + run-replay-tests: ${{ matrix.replays }} secrets: inherit - build-generalsmd-win32: - name: Build GeneralsMD${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + build-vcpkg: + name: ${{ matrix.game }} ${{ matrix.preset }} ${{ matrix.mode }}${{ matrix.tools && ' tools' || ' no-tools' }}${{ matrix.extras && ' extras' || ' no-extras' }}${{ matrix.ffmpeg && ' ffmpeg' || '' }} permissions: contents: read packages: write strategy: + fail-fast: false matrix: include: - - preset: "win32" - tools: true - extras: true - - preset: "win32-profile" - tools: true - extras: true - - preset: "win32-debug" - tools: true - extras: true - - preset: "win32-vcpkg" - tools: true - extras: true - ffmpeg: true - - preset: "win32-vcpkg-profile" - tools: true - extras: true - - preset: "win32-vcpkg-debug" - tools: true - extras: true - fail-fast: false - uses: ./.github/workflows/build-toolchain.yml + - { game: Generals, preset: win32-vcpkg, mode: noretail, tools: true, extras: true, ffmpeg: false } + - { game: Generals, preset: win32-vcpkg, mode: retail, tools: true, extras: true, ffmpeg: false } + - { game: GeneralsMD, preset: win32-vcpkg, mode: noretail, tools: true, extras: true, ffmpeg: true } + - { game: GeneralsMD, preset: win32-vcpkg, mode: retail, tools: true, extras: true, ffmpeg: true } + - { game: Generals, preset: win32-vcpkg-profile, mode: noretail, tools: true, extras: true, ffmpeg: false } + - { game: GeneralsMD, preset: win32-vcpkg-profile, mode: noretail, tools: true, extras: true, ffmpeg: false } + - { game: Generals, preset: win32-vcpkg-profile, mode: retail, tools: true, extras: true, ffmpeg: false } + - { game: GeneralsMD, preset: win32-vcpkg-profile, mode: retail, tools: true, extras: true, ffmpeg: false } + - { game: Generals, preset: win32-vcpkg-debug, mode: noretail, tools: true, extras: true, ffmpeg: false } + - { game: GeneralsMD, preset: win32-vcpkg-debug, mode: noretail, tools: true, extras: true, ffmpeg: false } + - { game: Generals, preset: win32-vcpkg-debug, mode: retail, tools: true, extras: true, ffmpeg: false } + - { game: GeneralsMD, preset: win32-vcpkg-debug, mode: retail, tools: true, extras: true, ffmpeg: false } + uses: ./.github/workflows/_build-toolchain.yml with: - game: "GeneralsMD" + game: ${{ matrix.game }} preset: ${{ matrix.preset }} tools: ${{ matrix.tools }} extras: ${{ matrix.extras }} - ffmpeg: ${{ matrix.ffmpeg || false}} - secrets: inherit - - replaycheck-generalsmd: - name: Replay Check GeneralsMD${{ matrix.preset && '' }} - needs: build-generalsmd-vc6 - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} - strategy: - matrix: - include: - - preset: "vc6+t+e" - - preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here. - fail-fast: false - uses: ./.github/workflows/check-replays.yml - with: - game: "GeneralsMD" - userdata: "GeneralsReplays/GeneralsZH/1.04" - preset: ${{ matrix.preset }} + ffmpeg: ${{ matrix.ffmpeg }} + retail-compatible: ${{ matrix.mode }} secrets: inherit diff --git a/.github/workflows/weekly-release.yml b/.github/workflows/weekly-release.yml index 52f7bdb8be2..7789d858a00 100644 --- a/.github/workflows/weekly-release.yml +++ b/.github/workflows/weekly-release.yml @@ -97,7 +97,7 @@ jobs: extras: false release: true fail-fast: false - uses: ./.github/workflows/build-toolchain.yml + uses: ./.github/workflows/_build-toolchain.yml with: game: "Generals" preset: ${{ matrix.preset }} @@ -118,7 +118,7 @@ jobs: extras: false release: true fail-fast: false - uses: ./.github/workflows/build-toolchain.yml + uses: ./.github/workflows/_build-toolchain.yml with: game: "GeneralsMD" preset: ${{ matrix.preset }}