diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..dff8102 --- /dev/null +++ b/action.yml @@ -0,0 +1,98 @@ +name: Export Godot project +description: Download and cache Godot 4, install export templates, and export a named preset without Docker. + +inputs: + godot-version: + description: Exact Godot 4 release tag without a leading v (for example, 4.6.1-stable). + required: true + host-os: + description: "Runner operating system: linux, macos, or windows." + required: true + host-arch: + description: Runner architecture. Supported values depend on host-os. + required: true + dotnet: + description: Download the Godot .NET editor and export templates. + required: false + default: "false" + project-path: + description: Godot project directory, relative to the checked-out workspace. + required: false + default: . + export-preset: + description: Exact export preset name from export_presets.cfg. + required: true + export-mode: + description: "Export mode: release or debug." + required: false + default: release + export-file-name: + description: Output filename only, including its platform-appropriate extension. + required: true + cache: + description: Restore and save the downloaded Godot editor and export templates. + required: false + default: "true" + +outputs: + build-directory: + description: Absolute directory containing this invocation's exported build. + value: ${{ steps.export-linux.outputs.build-directory || steps.export-macos.outputs.build-directory || steps.export-windows.outputs.build-directory }} + +runs: + using: composite + steps: + - name: Restore Godot cache + if: ${{ inputs.cache == 'true' }} + uses: actions/cache@v4.1.0 + with: + path: "${{ runner.temp }}/godot-export-action/cache" + key: "godot-export-action-v1-${{ inputs.godot-version }}-${{ inputs.dotnet }}-${{ inputs.host-os }}-${{ inputs.host-arch }}" + + - name: Export Godot project on Linux + id: export-linux + if: ${{ runner.os == 'Linux' }} + shell: bash + env: + GODOT_VERSION: "${{ inputs.godot-version }}" + GODOT_HOST_OS: "${{ inputs.host-os }}" + GODOT_HOST_ARCH: "${{ inputs.host-arch }}" + GODOT_DOTNET: "${{ inputs.dotnet }}" + GODOT_PROJECT_PATH: "${{ inputs.project-path }}" + GODOT_EXPORT_PRESET: "${{ inputs.export-preset }}" + GODOT_EXPORT_MODE: "${{ inputs.export-mode }}" + GODOT_EXPORT_FILE_NAME: "${{ inputs.export-file-name }}" + GODOT_ACTION_ROOT: "${{ runner.temp }}/godot-export-action" + run: bash "$GITHUB_ACTION_PATH/export-linux.sh" + + - name: Export Godot project on macOS + id: export-macos + if: ${{ runner.os == 'macOS' }} + shell: bash + env: + GODOT_VERSION: "${{ inputs.godot-version }}" + GODOT_HOST_OS: "${{ inputs.host-os }}" + GODOT_HOST_ARCH: "${{ inputs.host-arch }}" + GODOT_DOTNET: "${{ inputs.dotnet }}" + GODOT_PROJECT_PATH: "${{ inputs.project-path }}" + GODOT_EXPORT_PRESET: "${{ inputs.export-preset }}" + GODOT_EXPORT_MODE: "${{ inputs.export-mode }}" + GODOT_EXPORT_FILE_NAME: "${{ inputs.export-file-name }}" + GODOT_ACTION_ROOT: "${{ runner.temp }}/godot-export-action" + run: bash "$GITHUB_ACTION_PATH/export-macos.sh" + + - name: Export Godot project on Windows + id: export-windows + if: ${{ runner.os == 'Windows' }} + shell: powershell + env: + GODOT_VERSION: "${{ inputs.godot-version }}" + GODOT_HOST_OS: "${{ inputs.host-os }}" + GODOT_HOST_ARCH: "${{ inputs.host-arch }}" + GODOT_DOTNET: "${{ inputs.dotnet }}" + GODOT_PROJECT_PATH: "${{ inputs.project-path }}" + GODOT_EXPORT_PRESET: "${{ inputs.export-preset }}" + GODOT_EXPORT_MODE: "${{ inputs.export-mode }}" + GODOT_EXPORT_FILE_NAME: "${{ inputs.export-file-name }}" + GODOT_ACTION_ROOT: "${{ runner.temp }}/godot-export-action" + run: "& \"$env:GITHUB_ACTION_PATH\\export-windows.ps1\"" diff --git a/export-linux.sh b/export-linux.sh new file mode 100644 index 0000000..9214930 --- /dev/null +++ b/export-linux.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fail() { + echo "::error::$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' was not found on the runner." +} + +require_environment() { + [[ -n "${!1:-}" ]] || fail "Required environment variable '$1' is not set." +} + +validate_common_inputs() { + [[ "$GODOT_VERSION" =~ ^4(\.[0-9]+){1,2}(-[0-9A-Za-z][0-9A-Za-z._-]*)?$ ]] || fail "godot-version must be a Godot 4 release tag such as 4.7.2-stable." + [[ "$GODOT_HOST_OS" == "linux" ]] || fail "host-os must be 'linux' when this action runs on Linux." + [[ "$GODOT_DOTNET" == "true" || "$GODOT_DOTNET" == "false" ]] || fail "dotnet must be 'true' or 'false'." + [[ "$GODOT_EXPORT_MODE" == "release" || "$GODOT_EXPORT_MODE" == "debug" ]] || fail "export-mode must be 'release' or 'debug'." + [[ -n "$GODOT_EXPORT_PRESET" ]] || fail "export-preset must not be empty." + [[ -n "$GODOT_EXPORT_FILE_NAME" && "$GODOT_EXPORT_FILE_NAME" != "." && "$GODOT_EXPORT_FILE_NAME" != ".." ]] || fail "export-file-name must be a filename." + [[ "$GODOT_EXPORT_FILE_NAME" != */* && "$GODOT_EXPORT_FILE_NAME" != *\\* ]] || fail "export-file-name must not contain a path separator." +} + +actual_architecture() { + case "$(uname -m)" in + x86_64|amd64) echo x86_64 ;; + i?86) echo x86_32 ;; + aarch64|arm64) echo arm64 ;; + armv7l|armv6l) echo arm32 ;; + *) fail "Unsupported Linux runner architecture: $(uname -m)." ;; + esac +} + +download_and_verify() { + local asset="$1" + local destination="$2" + local sum_file="$3" + local expected + local actual + + curl --fail --location --retry 3 --silent --show-error "$BASE_URL/$asset" --output "$destination" + expected="$(awk -v asset="$asset" '$2 == asset || $2 == "*" asset { print $1; exit }' "$sum_file")" + [[ -n "$expected" ]] || fail "Release checksum manifest does not contain $asset." + actual="$(sha512sum "$destination" | awk '{ print $1 }')" + [[ "$actual" == "$expected" ]] || fail "SHA-512 verification failed for $asset." +} + +find_editor() { + find "$EDITOR_DIR" -maxdepth 3 -type f -name 'Godot*' -print -quit +} + +install_godot() { + local sum_file="$CACHE_DIR/SHA512-SUMS.txt" + local editor_archive="$CACHE_DIR/$EDITOR_ASSET" + local templates_archive="$CACHE_DIR/$TEMPLATES_ASSET" + + rm -rf "$CACHE_DIR" + mkdir -p "$EDITOR_DIR" "$TEMPLATE_DIR" + curl --fail --location --retry 3 --silent --show-error "$BASE_URL/SHA512-SUMS.txt" --output "$sum_file" + download_and_verify "$EDITOR_ASSET" "$editor_archive" "$sum_file" + download_and_verify "$TEMPLATES_ASSET" "$templates_archive" "$sum_file" + unzip -q "$editor_archive" -d "$EDITOR_DIR" + unzip -q "$templates_archive" -d "$TEMPLATE_DIR" + EDITOR_EXECUTABLE="$(find_editor)" + [[ -n "$EDITOR_EXECUTABLE" ]] || fail "The Godot editor archive did not contain an executable." + [[ -f "$TEMPLATE_DIR/version.txt" ]] || fail "The export-template archive did not contain version.txt." + printf 'release=%s\neditor=%s\ntemplates=%s\n' "$GODOT_VERSION" "$EDITOR_ASSET" "$TEMPLATES_ASSET" > "$CACHE_DIR/.complete" +} + +require_environment GODOT_VERSION +require_environment GODOT_HOST_OS +require_environment GODOT_HOST_ARCH +require_environment GODOT_DOTNET +require_environment GODOT_PROJECT_PATH +require_environment GODOT_EXPORT_PRESET +require_environment GODOT_EXPORT_MODE +require_environment GODOT_EXPORT_FILE_NAME +require_environment GODOT_ACTION_ROOT +require_environment GITHUB_WORKSPACE +require_environment GITHUB_OUTPUT + +require_command curl +require_command unzip +require_command sha512sum +validate_common_inputs + +[[ "$(uname -s)" == "Linux" ]] || fail "The Linux script was selected on a non-Linux runner." +[[ "$GODOT_HOST_ARCH" == "x86_32" || "$GODOT_HOST_ARCH" == "x86_64" || "$GODOT_HOST_ARCH" == "arm32" || "$GODOT_HOST_ARCH" == "arm64" ]] || fail "Unsupported Linux host-arch '$GODOT_HOST_ARCH'." +[[ "$GODOT_HOST_ARCH" == "$(actual_architecture)" ]] || fail "host-arch '$GODOT_HOST_ARCH' does not match this Linux runner ($(actual_architecture))." + +[[ "$GODOT_PROJECT_PATH" != /* && "$GODOT_PROJECT_PATH" != \\* ]] || fail "project-path must be relative to the checked-out workspace." +WORKSPACE_DIR="$(cd "$GITHUB_WORKSPACE" && pwd -P)" +PROJECT_DIR="$(cd "$WORKSPACE_DIR/$GODOT_PROJECT_PATH" 2>/dev/null && pwd -P)" || fail "project-path '$GODOT_PROJECT_PATH' does not exist." +case "$PROJECT_DIR/" in + "$WORKSPACE_DIR/"*) ;; + *) fail "project-path must remain inside the checked-out workspace." ;; +esac +[[ -f "$PROJECT_DIR/project.godot" ]] || fail "No project.godot was found in project-path '$GODOT_PROJECT_PATH'." +[[ -f "$PROJECT_DIR/export_presets.cfg" ]] || fail "No export_presets.cfg was found in project-path '$GODOT_PROJECT_PATH'." +grep -Fq "name=\"$GODOT_EXPORT_PRESET\"" "$PROJECT_DIR/export_presets.cfg" || fail "Export preset '$GODOT_EXPORT_PRESET' was not found in export_presets.cfg." + +if [[ "$GODOT_DOTNET" == "true" ]]; then + EDITION=dotnet + EDITOR_ASSET="Godot_v${GODOT_VERSION}_mono_linux_${GODOT_HOST_ARCH}.zip" + TEMPLATES_ASSET="Godot_v${GODOT_VERSION}_mono_export_templates.tpz" +else + EDITION=standard + EDITOR_ASSET="Godot_v${GODOT_VERSION}_linux.${GODOT_HOST_ARCH}.zip" + TEMPLATES_ASSET="Godot_v${GODOT_VERSION}_export_templates.tpz" +fi + +BASE_URL="https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION" +TEMPLATE_VERSION="${GODOT_VERSION//-/.}" +CACHE_DIR="$GODOT_ACTION_ROOT/cache/$GODOT_VERSION/$EDITION/linux-$GODOT_HOST_ARCH" +EDITOR_DIR="$CACHE_DIR/editor" +USER_DATA_DIR="$CACHE_DIR/user-data" +TEMPLATE_DIR="$USER_DATA_DIR/godot/export_templates/$TEMPLATE_VERSION" + +if [[ -f "$CACHE_DIR/.complete" && -f "$TEMPLATE_DIR/version.txt" ]]; then + EDITOR_EXECUTABLE="$(find_editor || true)" +else + EDITOR_EXECUTABLE="" +fi +if [[ -z "$EDITOR_EXECUTABLE" ]]; then + install_godot +fi + +chmod +x "$EDITOR_EXECUTABLE" +SAFE_PRESET="$(printf '%s' "$GODOT_EXPORT_PRESET" | tr -cs '[:alnum:]_.-' '_')" +BUILD_DIR="$GODOT_ACTION_ROOT/builds/${SAFE_PRESET:-export}-$GODOT_EXPORT_MODE-$$" +OUTPUT_FILE="$BUILD_DIR/$GODOT_EXPORT_FILE_NAME" +mkdir -p "$BUILD_DIR" + +export XDG_DATA_HOME="$USER_DATA_DIR" +export XDG_CONFIG_HOME="$CACHE_DIR/config" +"$EDITOR_EXECUTABLE" --headless --path "$PROJECT_DIR" "--export-$GODOT_EXPORT_MODE" "$GODOT_EXPORT_PRESET" "$OUTPUT_FILE" +[[ -e "$OUTPUT_FILE" ]] || fail "Godot completed without creating the expected output '$GODOT_EXPORT_FILE_NAME'." + +printf 'build-directory=%s\n' "$BUILD_DIR" >> "$GITHUB_OUTPUT" diff --git a/export-macos.sh b/export-macos.sh new file mode 100644 index 0000000..8e549f4 --- /dev/null +++ b/export-macos.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fail() { + echo "::error::$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' was not found on the runner." +} + +require_environment() { + [[ -n "${!1:-}" ]] || fail "Required environment variable '$1' is not set." +} + +validate_common_inputs() { + [[ "$GODOT_VERSION" =~ ^4(\.[0-9]+){1,2}(-[0-9A-Za-z][0-9A-Za-z._-]*)?$ ]] || fail "godot-version must be a Godot 4 release tag such as 4.7.2-stable." + [[ "$GODOT_HOST_OS" == "macos" ]] || fail "host-os must be 'macos' when this action runs on macOS." + [[ "$GODOT_DOTNET" == "true" || "$GODOT_DOTNET" == "false" ]] || fail "dotnet must be 'true' or 'false'." + [[ "$GODOT_EXPORT_MODE" == "release" || "$GODOT_EXPORT_MODE" == "debug" ]] || fail "export-mode must be 'release' or 'debug'." + [[ -n "$GODOT_EXPORT_PRESET" ]] || fail "export-preset must not be empty." + [[ -n "$GODOT_EXPORT_FILE_NAME" && "$GODOT_EXPORT_FILE_NAME" != "." && "$GODOT_EXPORT_FILE_NAME" != ".." ]] || fail "export-file-name must be a filename." + [[ "$GODOT_EXPORT_FILE_NAME" != */* && "$GODOT_EXPORT_FILE_NAME" != *\\* ]] || fail "export-file-name must not contain a path separator." +} + +actual_architecture() { + case "$(uname -m)" in + x86_64) echo x86_64 ;; + arm64) echo arm64 ;; + *) fail "Unsupported macOS runner architecture: $(uname -m)." ;; + esac +} + +download_and_verify() { + local asset="$1" + local destination="$2" + local sum_file="$3" + local expected + local actual + + curl --fail --location --retry 3 --silent --show-error "$BASE_URL/$asset" --output "$destination" + expected="$(awk -v asset="$asset" '$2 == asset || $2 == "*" asset { print $1; exit }' "$sum_file")" + [[ -n "$expected" ]] || fail "Release checksum manifest does not contain $asset." + actual="$(shasum -a 512 "$destination" | awk '{ print $1 }')" + [[ "$actual" == "$expected" ]] || fail "SHA-512 verification failed for $asset." +} + +find_editor() { + find "$EDITOR_DIR" -type f -path '*.app/Contents/MacOS/*' -name 'Godot*' -print -quit +} + +install_godot() { + local sum_file="$CACHE_DIR/SHA512-SUMS.txt" + local editor_archive="$CACHE_DIR/$EDITOR_ASSET" + local templates_archive="$CACHE_DIR/$TEMPLATES_ASSET" + + rm -rf "$CACHE_DIR" + mkdir -p "$EDITOR_DIR" "$TEMPLATE_DIR" + curl --fail --location --retry 3 --silent --show-error "$BASE_URL/SHA512-SUMS.txt" --output "$sum_file" + download_and_verify "$EDITOR_ASSET" "$editor_archive" "$sum_file" + download_and_verify "$TEMPLATES_ASSET" "$templates_archive" "$sum_file" + unzip -q "$editor_archive" -d "$EDITOR_DIR" + unzip -q "$templates_archive" -d "$TEMPLATE_DIR" + EDITOR_EXECUTABLE="$(find_editor)" + [[ -n "$EDITOR_EXECUTABLE" ]] || fail "The Godot editor archive did not contain a macOS app executable." + [[ -f "$TEMPLATE_DIR/version.txt" ]] || fail "The export-template archive did not contain version.txt." + printf 'release=%s\neditor=%s\ntemplates=%s\n' "$GODOT_VERSION" "$EDITOR_ASSET" "$TEMPLATES_ASSET" > "$CACHE_DIR/.complete" +} + +require_environment GODOT_VERSION +require_environment GODOT_HOST_OS +require_environment GODOT_HOST_ARCH +require_environment GODOT_DOTNET +require_environment GODOT_PROJECT_PATH +require_environment GODOT_EXPORT_PRESET +require_environment GODOT_EXPORT_MODE +require_environment GODOT_EXPORT_FILE_NAME +require_environment GODOT_ACTION_ROOT +require_environment GITHUB_WORKSPACE +require_environment GITHUB_OUTPUT + +require_command curl +require_command unzip +require_command shasum +validate_common_inputs + +[[ "$(uname -s)" == "Darwin" ]] || fail "The macOS script was selected on a non-macOS runner." +[[ "$GODOT_HOST_ARCH" == "x86_64" || "$GODOT_HOST_ARCH" == "arm64" ]] || fail "Unsupported macOS host-arch '$GODOT_HOST_ARCH'." +[[ "$GODOT_HOST_ARCH" == "$(actual_architecture)" ]] || fail "host-arch '$GODOT_HOST_ARCH' does not match this macOS runner ($(actual_architecture))." + +[[ "$GODOT_PROJECT_PATH" != /* && "$GODOT_PROJECT_PATH" != \\* ]] || fail "project-path must be relative to the checked-out workspace." +WORKSPACE_DIR="$(cd "$GITHUB_WORKSPACE" && pwd -P)" +PROJECT_DIR="$(cd "$WORKSPACE_DIR/$GODOT_PROJECT_PATH" 2>/dev/null && pwd -P)" || fail "project-path '$GODOT_PROJECT_PATH' does not exist." +case "$PROJECT_DIR/" in + "$WORKSPACE_DIR/"*) ;; + *) fail "project-path must remain inside the checked-out workspace." ;; +esac +[[ -f "$PROJECT_DIR/project.godot" ]] || fail "No project.godot was found in project-path '$GODOT_PROJECT_PATH'." +[[ -f "$PROJECT_DIR/export_presets.cfg" ]] || fail "No export_presets.cfg was found in project-path '$GODOT_PROJECT_PATH'." +grep -Fq "name=\"$GODOT_EXPORT_PRESET\"" "$PROJECT_DIR/export_presets.cfg" || fail "Export preset '$GODOT_EXPORT_PRESET' was not found in export_presets.cfg." + +if [[ "$GODOT_DOTNET" == "true" ]]; then + EDITION=dotnet + EDITOR_ASSET="Godot_v${GODOT_VERSION}_mono_macos.universal.zip" + TEMPLATES_ASSET="Godot_v${GODOT_VERSION}_mono_export_templates.tpz" +else + EDITION=standard + EDITOR_ASSET="Godot_v${GODOT_VERSION}_macos.universal.zip" + TEMPLATES_ASSET="Godot_v${GODOT_VERSION}_export_templates.tpz" +fi + +BASE_URL="https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION" +TEMPLATE_VERSION="${GODOT_VERSION//-/.}" +CACHE_DIR="$GODOT_ACTION_ROOT/cache/$GODOT_VERSION/$EDITION/macos-$GODOT_HOST_ARCH" +EDITOR_DIR="$CACHE_DIR/editor" +USER_HOME="$CACHE_DIR/home" +TEMPLATE_DIR="$USER_HOME/Library/Application Support/Godot/export_templates/$TEMPLATE_VERSION" + +if [[ -f "$CACHE_DIR/.complete" && -f "$TEMPLATE_DIR/version.txt" ]]; then + EDITOR_EXECUTABLE="$(find_editor || true)" +else + EDITOR_EXECUTABLE="" +fi +if [[ -z "$EDITOR_EXECUTABLE" ]]; then + install_godot +fi + +chmod +x "$EDITOR_EXECUTABLE" +SAFE_PRESET="$(printf '%s' "$GODOT_EXPORT_PRESET" | tr -cs '[:alnum:]_.-' '_')" +BUILD_DIR="$GODOT_ACTION_ROOT/builds/${SAFE_PRESET:-export}-$GODOT_EXPORT_MODE-$$" +OUTPUT_FILE="$BUILD_DIR/$GODOT_EXPORT_FILE_NAME" +mkdir -p "$BUILD_DIR" + +export HOME="$USER_HOME" +"$EDITOR_EXECUTABLE" --headless --path "$PROJECT_DIR" "--export-$GODOT_EXPORT_MODE" "$GODOT_EXPORT_PRESET" "$OUTPUT_FILE" +[[ -e "$OUTPUT_FILE" ]] || fail "Godot completed without creating the expected output '$GODOT_EXPORT_FILE_NAME'." + +printf 'build-directory=%s\n' "$BUILD_DIR" >> "$GITHUB_OUTPUT" diff --git a/export-windows.ps1 b/export-windows.ps1 new file mode 100644 index 0000000..6ae13f5 --- /dev/null +++ b/export-windows.ps1 @@ -0,0 +1,152 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Fail([string]$Message) { + Write-Error "::error::$Message" + exit 1 +} + +function Require-Environment([string]$Name) { + $value = [Environment]::GetEnvironmentVariable($Name) + if ([string]::IsNullOrWhiteSpace($value)) { + Fail "Required environment variable '$Name' is not set." + } + return $value +} + +function Get-ActualArchitecture { + $architecture = $env:PROCESSOR_ARCHITEW6432 + if ([string]::IsNullOrWhiteSpace($architecture)) { + $architecture = $env:PROCESSOR_ARCHITECTURE + } + switch ($architecture.ToUpperInvariant()) { + 'AMD64' { return 'x86_64' } + 'X86' { return 'x86_32' } + 'ARM64' { return 'arm64' } + default { Fail "Unsupported Windows runner architecture: $architecture." } + } +} + +function Get-Checksum([string]$ManifestPath, [string]$Asset) { + $line = Get-Content -LiteralPath $ManifestPath | Where-Object { + $_ -match '^([0-9A-Fa-f]{128})\s+\*?(.+)$' -and $Matches[2] -eq $Asset + } | Select-Object -First 1 + if (-not $line) { + Fail "Release checksum manifest does not contain $Asset." + } + return ($line -split '\s+')[0].ToLowerInvariant() +} + +function Download-AndVerify([string]$Asset, [string]$Destination, [string]$ManifestPath) { + Invoke-WebRequest -Uri "$script:BaseUrl/$Asset" -OutFile $Destination + $expected = Get-Checksum $ManifestPath $Asset + $actual = (Get-FileHash -LiteralPath $Destination -Algorithm SHA512).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + Fail "SHA-512 verification failed for $Asset." + } +} + +function Find-Editor([string]$Directory) { + $console = Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter 'Godot*console.exe' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($console) { return $console.FullName } + $editor = Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter 'Godot*.exe' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($editor) { return $editor.FullName } + return $null +} + +$GodotVersion = Require-Environment 'GODOT_VERSION' +$HostOs = Require-Environment 'GODOT_HOST_OS' +$HostArch = Require-Environment 'GODOT_HOST_ARCH' +$Dotnet = Require-Environment 'GODOT_DOTNET' +$ProjectPath = Require-Environment 'GODOT_PROJECT_PATH' +$ExportPreset = Require-Environment 'GODOT_EXPORT_PRESET' +$ExportMode = Require-Environment 'GODOT_EXPORT_MODE' +$ExportFileName = Require-Environment 'GODOT_EXPORT_FILE_NAME' +$ActionRoot = Require-Environment 'GODOT_ACTION_ROOT' +$Workspace = Require-Environment 'GITHUB_WORKSPACE' +$GithubOutput = Require-Environment 'GITHUB_OUTPUT' + +if ($GodotVersion -notmatch '^4(\.[0-9]+){1,2}(-[0-9A-Za-z][0-9A-Za-z._-]*)?$') { Fail 'godot-version must be a Godot 4 release tag such as 4.7.2-stable.' } +if ($HostOs -ne 'windows') { Fail "host-os must be 'windows' when this action runs on Windows." } +if ($Dotnet -notin @('true', 'false')) { Fail "dotnet must be 'true' or 'false'." } +if ($ExportMode -notin @('release', 'debug')) { Fail "export-mode must be 'release' or 'debug'." } +if ([string]::IsNullOrWhiteSpace($ExportPreset)) { Fail 'export-preset must not be empty.' } +if ([string]::IsNullOrWhiteSpace($ExportFileName) -or $ExportFileName -in @('.', '..') -or $ExportFileName.IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0 -or $ExportFileName.Contains('/') -or $ExportFileName.Contains('\')) { Fail 'export-file-name must be a filename without path separators.' } +if ($HostArch -notin @('x86_32', 'x86_64', 'arm64')) { Fail "Unsupported Windows host-arch '$HostArch'." } +if ($HostArch -ne (Get-ActualArchitecture)) { Fail "host-arch '$HostArch' does not match this Windows runner ($(Get-ActualArchitecture))." } + +$workspaceDirectory = [IO.Path]::GetFullPath($Workspace) +if ([IO.Path]::IsPathRooted($ProjectPath)) { Fail 'project-path must be relative to the checked-out workspace.' } +$ProjectDirectory = [IO.Path]::GetFullPath((Join-Path $workspaceDirectory $ProjectPath)) +$projectRelativePath = [IO.Path]::GetRelativePath($workspaceDirectory, $ProjectDirectory) +if ($projectRelativePath -eq '..' -or $projectRelativePath.StartsWith("..$([IO.Path]::DirectorySeparatorChar)") -or [IO.Path]::IsPathRooted($projectRelativePath)) { Fail 'project-path must remain inside the checked-out workspace.' } +if (-not (Test-Path -LiteralPath (Join-Path $ProjectDirectory 'project.godot') -PathType Leaf)) { Fail "No project.godot was found in project-path '$ProjectPath'." } +$PresetFile = Join-Path $ProjectDirectory 'export_presets.cfg' +if (-not (Test-Path -LiteralPath $PresetFile -PathType Leaf)) { Fail "No export_presets.cfg was found in project-path '$ProjectPath'." } +if (-not (Select-String -LiteralPath $PresetFile -SimpleMatch "name=`"$ExportPreset`"" -Quiet)) { Fail "Export preset '$ExportPreset' was not found in export_presets.cfg." } + +if ($Dotnet -eq 'true') { + $edition = 'dotnet' + $editorAsset = switch ($HostArch) { + 'x86_32' { "Godot_v${GodotVersion}_mono_win32.zip" } + 'x86_64' { "Godot_v${GodotVersion}_mono_win64.zip" } + 'arm64' { "Godot_v${GodotVersion}_mono_windows_arm64.zip" } + } + $templatesAsset = "Godot_v${GodotVersion}_mono_export_templates.tpz" +} else { + $edition = 'standard' + $editorAsset = switch ($HostArch) { + 'x86_32' { "Godot_v${GodotVersion}_win32.exe.zip" } + 'x86_64' { "Godot_v${GodotVersion}_win64.exe.zip" } + 'arm64' { "Godot_v${GodotVersion}_windows_arm64.exe.zip" } + } + $templatesAsset = "Godot_v${GodotVersion}_export_templates.tpz" +} + +$script:BaseUrl = "https://github.com/godotengine/godot-builds/releases/download/$GodotVersion" +$templateVersion = $GodotVersion -replace '-', '.' +$cacheDirectory = Join-Path $ActionRoot "cache\$GodotVersion\$edition\windows-$HostArch" +$editorDirectory = Join-Path $cacheDirectory 'editor' +$userHome = Join-Path $cacheDirectory 'home' +$templateDirectory = Join-Path $userHome "AppData\Roaming\Godot\export_templates\$templateVersion" +$marker = Join-Path $cacheDirectory '.complete' + +$editorExecutable = $null +if ((Test-Path -LiteralPath $marker -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $templateDirectory 'version.txt') -PathType Leaf)) { + $editorExecutable = Find-Editor $editorDirectory +} + +if (-not $editorExecutable) { + if (Test-Path -LiteralPath $cacheDirectory) { Remove-Item -LiteralPath $cacheDirectory -Recurse -Force } + New-Item -ItemType Directory -Force -Path $editorDirectory, $templateDirectory | Out-Null + $manifestPath = Join-Path $cacheDirectory 'SHA512-SUMS.txt' + $editorArchive = Join-Path $cacheDirectory $editorAsset + $templatesArchive = Join-Path $cacheDirectory $templatesAsset + Invoke-WebRequest -Uri "$script:BaseUrl/SHA512-SUMS.txt" -OutFile $manifestPath + Download-AndVerify $editorAsset $editorArchive $manifestPath + Download-AndVerify $templatesAsset $templatesArchive $manifestPath + Expand-Archive -LiteralPath $editorArchive -DestinationPath $editorDirectory -Force + Expand-Archive -LiteralPath $templatesArchive -DestinationPath $templateDirectory -Force + $editorExecutable = Find-Editor $editorDirectory + if (-not $editorExecutable) { Fail 'The Godot editor archive did not contain an executable.' } + if (-not (Test-Path -LiteralPath (Join-Path $templateDirectory 'version.txt') -PathType Leaf)) { Fail 'The export-template archive did not contain version.txt.' } + @("release=$GodotVersion", "editor=$editorAsset", "templates=$templatesAsset") | Set-Content -LiteralPath $marker -NoNewline +} + +$safePreset = ($ExportPreset -replace '[^A-Za-z0-9_.-]', '_').Trim('_') +if ([string]::IsNullOrWhiteSpace($safePreset)) { $safePreset = 'export' } +$buildDirectory = Join-Path $ActionRoot "builds\$safePreset-$ExportMode-$PID" +$outputFile = Join-Path $buildDirectory $ExportFileName +New-Item -ItemType Directory -Force -Path $buildDirectory | Out-Null + +$env:USERPROFILE = $userHome +$env:APPDATA = Join-Path $userHome 'AppData\Roaming' +$env:LOCALAPPDATA = Join-Path $userHome 'AppData\Local' +& $editorExecutable --headless --path $ProjectDirectory "--export-$ExportMode" $ExportPreset $outputFile +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +if (-not (Test-Path -LiteralPath $outputFile)) { Fail "Godot completed without creating the expected output '$ExportFileName'." } + +"build-directory=$buildDirectory" | Add-Content -LiteralPath $GithubOutput