Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef2f78ff91 |
+181
-70
@@ -18,20 +18,16 @@ function Require-Environment([string]$Name) {
|
||||
}
|
||||
|
||||
function Test-PathIsWithinDirectory([string]$ParentDirectory, [string]$CandidateDirectory) {
|
||||
# [IO.Path]::GetRelativePath is not available in Windows PowerShell 5.1.
|
||||
# Compare complete paths with a separator boundary so, for example,
|
||||
# C:\workspace-other is not considered a child of C:\workspace.
|
||||
$parentPath = [IO.Path]::GetFullPath($ParentDirectory)
|
||||
$candidatePath = [IO.Path]::GetFullPath($CandidateDirectory)
|
||||
$directorySeparator = [IO.Path]::DirectorySeparatorChar.ToString()
|
||||
$alternateSeparator = [IO.Path]::AltDirectorySeparatorChar.ToString()
|
||||
$separator = [IO.Path]::DirectorySeparatorChar.ToString()
|
||||
|
||||
if ($candidatePath.Equals($parentPath, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not $parentPath.EndsWith($directorySeparator) -and -not $parentPath.EndsWith($alternateSeparator)) {
|
||||
$parentPath += $directorySeparator
|
||||
if (-not $parentPath.EndsWith($separator)) {
|
||||
$parentPath += $separator
|
||||
}
|
||||
|
||||
return $candidatePath.StartsWith($parentPath, [StringComparison]::OrdinalIgnoreCase)
|
||||
@@ -42,6 +38,7 @@ function Get-ActualArchitecture {
|
||||
if ([string]::IsNullOrWhiteSpace($architecture)) {
|
||||
$architecture = $env:PROCESSOR_ARCHITECTURE
|
||||
}
|
||||
|
||||
switch ($architecture.ToUpperInvariant()) {
|
||||
'AMD64' { return 'x86_64' }
|
||||
'X86' { return 'x86_32' }
|
||||
@@ -51,49 +48,159 @@ function Get-ActualArchitecture {
|
||||
}
|
||||
|
||||
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 "Godot $GodotVersion does not publish required $edition asset '$Asset' for Windows/$HostArch at $script:BaseUrl. Choose a supported stable release and architecture."
|
||||
foreach ($line in Get-Content -LiteralPath $ManifestPath) {
|
||||
if ($line -match '^([0-9A-Fa-f]{128})\s+\*?(.+)$' -and $Matches[2] -eq $Asset) {
|
||||
return $Matches[1].ToLowerInvariant()
|
||||
}
|
||||
return ($line -split '\s+')[0].ToLowerInvariant()
|
||||
}
|
||||
|
||||
Fail "Godot $GodotVersion does not publish required $edition asset '$Asset' for Windows/$HostArch at $script:BaseUrl. Choose a supported stable release and architecture."
|
||||
}
|
||||
|
||||
function Download-AndVerify([string]$Asset, [string]$Destination, [string]$ExpectedChecksum) {
|
||||
Invoke-WebRequest -Uri "$script:BaseUrl/$Asset" -OutFile $Destination
|
||||
$actual = (Get-FileHash -LiteralPath $Destination -Algorithm SHA512).Hash.ToLowerInvariant()
|
||||
if ($actual -ne $ExpectedChecksum) {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri "$script:BaseUrl/$Asset" -OutFile $Destination
|
||||
$actualChecksum = (Get-FileHash -LiteralPath $Destination -Algorithm SHA512).Hash.ToLowerInvariant()
|
||||
if ($actualChecksum -ne $ExpectedChecksum) {
|
||||
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 }
|
||||
function Get-CachePaths([string]$Root) {
|
||||
$editorDirectory = Join-Path $Root 'editor'
|
||||
$userHome = Join-Path $Root 'home'
|
||||
$templateDirectory = Join-Path $userHome "AppData\Roaming\Godot\export_templates\$templateVersion"
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Root = $Root
|
||||
EditorDirectory = $editorDirectory
|
||||
UserHome = $userHome
|
||||
TemplateDirectory = $templateDirectory
|
||||
Marker = Join-Path $Root '.complete'
|
||||
}
|
||||
}
|
||||
|
||||
function Get-EditorExecutable([string]$EditorDirectory) {
|
||||
$matches = @(Get-ChildItem -LiteralPath $EditorDirectory -Recurse -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -eq $expectedEditorFileName })
|
||||
|
||||
if ($matches.Count -eq 1) {
|
||||
return $matches[0].FullName
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Install-ExportTemplates([string]$StagingDirectory, [string]$TemplateDirectory, [string]$TemplateAsset, [string]$ExpectedVersion) {
|
||||
function Get-PeMachine([string]$Path) {
|
||||
try {
|
||||
$bytes = [IO.File]::ReadAllBytes($Path)
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$peOffset = [BitConverter]::ToInt32($bytes, 0x3c)
|
||||
if ($peOffset -lt 0 -or $peOffset -gt ($bytes.Length - 6)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($bytes[$peOffset] -ne 0x50 -or $bytes[$peOffset + 1] -ne 0x45 -or $bytes[$peOffset + 2] -ne 0 -or $bytes[$peOffset + 3] -ne 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [BitConverter]::ToUInt16($bytes, $peOffset + 4)
|
||||
}
|
||||
|
||||
function Test-ExpectedEditor([string]$EditorDirectory) {
|
||||
$editorExecutable = Get-EditorExecutable $EditorDirectory
|
||||
if (-not $editorExecutable) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return (Get-PeMachine $editorExecutable) -eq $expectedPeMachine
|
||||
}
|
||||
|
||||
function Assert-ExpectedEditor([string]$EditorDirectory) {
|
||||
$editorExecutable = Get-EditorExecutable $EditorDirectory
|
||||
if (-not $editorExecutable) {
|
||||
Fail "The Godot editor archive did not contain the expected console executable '$expectedEditorFileName'."
|
||||
}
|
||||
|
||||
$machine = Get-PeMachine $editorExecutable
|
||||
if ($machine -ne $expectedPeMachine) {
|
||||
Fail "The expected Godot console executable '$expectedEditorFileName' is not a valid $HostArch Windows executable."
|
||||
}
|
||||
|
||||
return $editorExecutable
|
||||
}
|
||||
|
||||
function Test-ValidCache($Paths) {
|
||||
if (-not (Test-Path -LiteralPath $Paths.Marker -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$installedVersion = Join-Path $Paths.TemplateDirectory 'version.txt'
|
||||
if (-not (Test-Path -LiteralPath $installedVersion -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ((Get-Content -LiteralPath $installedVersion -Raw).Trim() -ne $templateVersion) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return Test-ExpectedEditor $Paths.EditorDirectory
|
||||
}
|
||||
|
||||
function Install-ExportTemplates($Paths, [string]$StagingDirectory, [string]$TemplateAsset) {
|
||||
$stagedTemplates = Join-Path $StagingDirectory 'templates'
|
||||
$stagedVersion = Join-Path $stagedTemplates 'version.txt'
|
||||
if (-not (Test-Path -LiteralPath $stagedVersion -PathType Leaf)) {
|
||||
Fail "The export-template archive '$TemplateAsset' did not contain templates/version.txt."
|
||||
}
|
||||
|
||||
$actualVersion = (Get-Content -LiteralPath $stagedVersion -Raw).Trim()
|
||||
if ($actualVersion -ne $ExpectedVersion) {
|
||||
Fail "The export-template archive '$TemplateAsset' reports template version '$actualVersion', expected '$ExpectedVersion'."
|
||||
if ($actualVersion -ne $templateVersion) {
|
||||
Fail "The export-template archive '$TemplateAsset' reports template version '$actualVersion', expected '$templateVersion'."
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $TemplateDirectory | Out-Null
|
||||
Get-ChildItem -LiteralPath $stagedTemplates -Force | Copy-Item -Destination $TemplateDirectory -Recurse -Force
|
||||
Remove-Item -LiteralPath $StagingDirectory -Recurse -Force
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $TemplateDirectory 'version.txt') -PathType Leaf)) {
|
||||
Fail "The export-template archive '$TemplateAsset' could not be installed to '$TemplateDirectory'."
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Paths.TemplateDirectory | Out-Null
|
||||
Get-ChildItem -LiteralPath $stagedTemplates -Force | Copy-Item -Destination $Paths.TemplateDirectory -Recurse -Force
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Paths.TemplateDirectory 'version.txt') -PathType Leaf)) {
|
||||
Fail "The export-template archive '$TemplateAsset' could not be installed to '$($Paths.TemplateDirectory)'."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Cache($Paths) {
|
||||
New-Item -ItemType Directory -Force -Path $Paths.EditorDirectory | Out-Null
|
||||
|
||||
$manifestPath = Join-Path $Paths.Root 'SHA512-SUMS.txt'
|
||||
$editorArchive = Join-Path $Paths.Root $editorAsset
|
||||
$templatesArchive = Join-Path $Paths.Root $templatesAsset
|
||||
$templatesZipArchive = Join-Path $Paths.Root 'export-templates.zip'
|
||||
$templatesStagingDirectory = Join-Path $Paths.Root 'templates-staging'
|
||||
|
||||
Invoke-WebRequest -UseBasicParsing -Uri "$script:BaseUrl/SHA512-SUMS.txt" -OutFile $manifestPath
|
||||
$editorChecksum = Get-Checksum $manifestPath $editorAsset
|
||||
$templatesChecksum = Get-Checksum $manifestPath $templatesAsset
|
||||
Download-AndVerify $editorAsset $editorArchive $editorChecksum
|
||||
Download-AndVerify $templatesAsset $templatesArchive $templatesChecksum
|
||||
|
||||
Expand-Archive -LiteralPath $editorArchive -DestinationPath $Paths.EditorDirectory -Force
|
||||
Assert-ExpectedEditor $Paths.EditorDirectory | Out-Null
|
||||
|
||||
# Expand-Archive only recognizes .zip files. The verified .tpz payload is a ZIP archive.
|
||||
Move-Item -LiteralPath $templatesArchive -Destination $templatesZipArchive
|
||||
Expand-Archive -LiteralPath $templatesZipArchive -DestinationPath $templatesStagingDirectory -Force
|
||||
Install-ExportTemplates $Paths $templatesStagingDirectory $templatesAsset
|
||||
Remove-Item -LiteralPath $templatesStagingDirectory -Recurse -Force
|
||||
|
||||
@("release=$GodotVersion", "editor=$editorAsset", "templates=$templatesAsset") |
|
||||
Set-Content -LiteralPath $Paths.Marker -NoNewline
|
||||
}
|
||||
|
||||
$GodotVersion = Require-Environment 'GODOT_VERSION'
|
||||
$HostOs = Require-Environment 'GODOT_HOST_OS'
|
||||
$HostArch = Require-Environment 'GODOT_HOST_ARCH'
|
||||
@@ -113,7 +220,9 @@ if ($ExportMode -notin @('release', 'debug')) { Fail "export-mode must be 'relea
|
||||
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))." }
|
||||
|
||||
$actualArchitecture = Get-ActualArchitecture
|
||||
if ($HostArch -ne $actualArchitecture) { Fail "host-arch '$HostArch' does not match this Windows runner ($actualArchitecture)." }
|
||||
|
||||
$workspaceDirectory = [IO.Path]::GetFullPath($Workspace)
|
||||
if ([IO.Path]::IsPathRooted($ProjectPath)) { Fail 'project-path must be relative to the checked-out workspace.' }
|
||||
@@ -126,69 +235,71 @@ if (-not (Select-String -LiteralPath $PresetFile -SimpleMatch "name=`"$ExportPre
|
||||
|
||||
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" }
|
||||
$editorBaseName = switch ($HostArch) {
|
||||
'x86_32' { "Godot_v${GodotVersion}_mono_win32" }
|
||||
'x86_64' { "Godot_v${GodotVersion}_mono_win64" }
|
||||
'arm64' { "Godot_v${GodotVersion}_mono_windows_arm64" }
|
||||
}
|
||||
$editorAsset = "$editorBaseName.zip"
|
||||
$templatesAsset = "Godot_v${GodotVersion}_mono_export_templates.tpz"
|
||||
$templateVersion = ($GodotVersion -replace '-', '.') + '.mono'
|
||||
} 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" }
|
||||
$editorBaseName = switch ($HostArch) {
|
||||
'x86_32' { "Godot_v${GodotVersion}_win32" }
|
||||
'x86_64' { "Godot_v${GodotVersion}_win64" }
|
||||
'arm64' { "Godot_v${GodotVersion}_windows_arm64" }
|
||||
}
|
||||
$editorAsset = "$editorBaseName.exe.zip"
|
||||
$templatesAsset = "Godot_v${GodotVersion}_export_templates.tpz"
|
||||
$templateVersion = $GodotVersion -replace '-', '.'
|
||||
}
|
||||
|
||||
$expectedEditorFileName = "${editorBaseName}_console.exe"
|
||||
$expectedPeMachine = switch ($HostArch) {
|
||||
'x86_32' { [uint16]0x014c }
|
||||
'x86_64' { [uint16]0x8664 }
|
||||
'arm64' { [uint16]0xaa64 }
|
||||
}
|
||||
|
||||
$script:BaseUrl = "https://github.com/godotengine/godot/releases/download/$GodotVersion"
|
||||
$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'
|
||||
$cachePaths = Get-CachePaths $cacheDirectory
|
||||
|
||||
$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 (Test-ValidCache $cachePaths)) {
|
||||
$cacheParent = Split-Path -Parent $cacheDirectory
|
||||
$stagingDirectory = Join-Path $cacheParent ((Split-Path -Leaf $cacheDirectory) + ".staging-$PID")
|
||||
if (Test-Path -LiteralPath $stagingDirectory) { Remove-Item -LiteralPath $stagingDirectory -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path $cacheParent | Out-Null
|
||||
|
||||
if (-not $editorExecutable) {
|
||||
try {
|
||||
$stagingPaths = Get-CachePaths $stagingDirectory
|
||||
Install-Cache $stagingPaths
|
||||
if (-not (Test-ValidCache $stagingPaths)) { Fail 'Godot cache installation did not pass validation.' }
|
||||
if (Test-Path -LiteralPath $cacheDirectory) { Remove-Item -LiteralPath $cacheDirectory -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path $editorDirectory | Out-Null
|
||||
$manifestPath = Join-Path $cacheDirectory 'SHA512-SUMS.txt'
|
||||
$editorArchive = Join-Path $cacheDirectory $editorAsset
|
||||
$templatesArchive = Join-Path $cacheDirectory $templatesAsset
|
||||
$templatesZipArchive = Join-Path $cacheDirectory ($templatesAsset -replace '\.tpz$', '.zip')
|
||||
$templatesStagingDirectory = Join-Path $cacheDirectory 'templates-staging'
|
||||
Invoke-WebRequest -Uri "$script:BaseUrl/SHA512-SUMS.txt" -OutFile $manifestPath
|
||||
$editorChecksum = Get-Checksum $manifestPath $editorAsset
|
||||
$templatesChecksum = Get-Checksum $manifestPath $templatesAsset
|
||||
Download-AndVerify $editorAsset $editorArchive $editorChecksum
|
||||
Download-AndVerify $templatesAsset $templatesArchive $templatesChecksum
|
||||
Expand-Archive -LiteralPath $editorArchive -DestinationPath $editorDirectory -Force
|
||||
Move-Item -LiteralPath $templatesArchive -Destination $templatesZipArchive
|
||||
Expand-Archive -LiteralPath $templatesZipArchive -DestinationPath $templatesStagingDirectory -Force
|
||||
Install-ExportTemplates $templatesStagingDirectory $templateDirectory $templatesAsset $templateVersion
|
||||
$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
|
||||
Move-Item -LiteralPath $stagingDirectory -Destination $cacheDirectory
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $stagingDirectory) { Remove-Item -LiteralPath $stagingDirectory -Recurse -Force }
|
||||
}
|
||||
}
|
||||
|
||||
$editorExecutable = Assert-ExpectedEditor $cachePaths.EditorDirectory
|
||||
$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
|
||||
$env:USERPROFILE = $cachePaths.UserHome
|
||||
$env:APPDATA = Join-Path $cachePaths.UserHome 'AppData\Roaming'
|
||||
$env:LOCALAPPDATA = Join-Path $cachePaths.UserHome 'AppData\Local'
|
||||
|
||||
try {
|
||||
& $editorExecutable --headless --path $ProjectDirectory "--export-$ExportMode" $ExportPreset $outputFile
|
||||
} catch {
|
||||
Fail "Godot could not start '$editorExecutable': $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
if (-not (Test-Path -LiteralPath $outputFile)) { Fail "Godot completed without creating the expected output '$ExportFileName'." }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user