fix: harden cross-platform quality tooling

This commit is contained in:
Rijad Zuzo
2026-07-30 13:42:38 +02:00
parent e284d3774d
commit 5511c295a5
7 changed files with 370 additions and 95 deletions
+20 -4
View File
@@ -17,23 +17,28 @@ Set up the local tools with `uv`:
```bash ```bash
uv venv .venv uv venv .venv
uv pip install --python .venv/bin/python -r requirements-dev.txt uv pip install -r requirements-dev.txt
``` ```
`uv` discovers the project-local `.venv` on both macOS and Windows, so the
same setup commands work in a shell or PowerShell.
Open `project.godot` in Godot 4.7 and run the main scene. Open `project.godot` in Godot 4.7 and run the main scene.
## Quality gate ## Quality gate
Run the complete local gate before committing: Run the complete local gate before committing.
macOS:
```bash ```bash
./tools/quality.sh ./tools/quality.sh
``` ```
On Windows: Windows PowerShell:
```powershell ```powershell
powershell -ExecutionPolicy Bypass -File .\tools\quality.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\quality.ps1
``` ```
For a faster changed-GDScript formatting and lint pass, while still running For a faster changed-GDScript formatting and lint pass, while still running
@@ -43,6 +48,17 @@ the Godot checks and scenarios:
./tools/quality.sh --changed ./tools/quality.sh --changed
``` ```
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\quality.ps1 -Changed
```
Auto-format all game-owned GDScript with `./tools/fix_format.sh` on macOS. On
Windows:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\fix_format.ps1
```
The gate enforces Godot 4.7, the pinned formatter and linter, headless project The gate enforces Godot 4.7, the pinned formatter and linter, headless project
scenarios, and the vendored GUT suite. See scenarios, and the vendored GUT suite. See
[`docs/local_quality_gate.md`](docs/local_quality_gate.md) for setup and [`docs/local_quality_gate.md`](docs/local_quality_gate.md) for setup and
+2 -2
View File
@@ -22,11 +22,11 @@ func _ready() -> void:
# Setup Display Scale # Setup Display Scale
# 0 auto, 1 75%, 2 100%, 3 125%, 4 150%, 5 175%, 6 200%, 7 custom # 0 auto, 1 75%, 2 100%, 3 125%, 4 150%, 5 175%, 6 200%, 7 custom
var es: EditorSettings = EditorInterface.get_editor_settings() var es: EditorSettings = EditorInterface.get_editor_settings()
var ds: int = es.get_setting("interface/editor/display_scale") var ds: int = es.get_setting("interface/editor/appearance/display_scale")
if ds == 0: if ds == 0:
ds = 2 ds = 2
elif ds == 7: elif ds == 7:
display_scale = es.get_setting("interface/editor/custom_display_scale") display_scale = es.get_setting("interface/editor/appearance/custom_display_scale")
else: else:
display_scale = float(ds + 2) * .25 display_scale = float(ds + 2) * .25
+56 -8
View File
@@ -6,38 +6,64 @@ parser errors, and LLM-generated garbage before committing.
## Requirements ## Requirements
- **Python 3** with the pinned `gdtoolkit` (gdscript-toolkit). The recommended - **Python 3** with the pinned `gdtoolkit` (gdscript-toolkit). The recommended
setup uses `uv` and a project-local environment: setup uses `uv` and a project-local environment. These commands are the same
in macOS shells and Windows PowerShell:
```bash ```bash
uv venv .venv uv venv .venv
uv pip install --python .venv/bin/python -r requirements-dev.txt uv pip install -r requirements-dev.txt
``` ```
On Windows, use `.venv/Scripts/python.exe` for the `--python` value.
- **Godot 4.7.x** binary on `PATH`, or set the `GODOT_BIN` environment - **Godot 4.7.x** binary on `PATH`, or set the `GODOT_BIN` environment
variable. The gate rejects other engine series before importing or testing. variable. The gate rejects other engine series before importing or testing.
- **GUT 9.7.1** is vendored under `addons/gut` for Godot 4.7. No separate - **GUT 9.7.1** is vendored under `addons/gut` for Godot 4.7. No separate
package-manager install is needed. package-manager install is needed.
- **Terrain3D 1.0.2** is vendored with macOS and Windows x86-64 debug and
release binaries. No separate native build is needed on either development
platform.
The vendored addon comes from the official The vendored addon comes from the official
[`bitwes/Gut` v9.7.1 release](https://github.com/bitwes/Gut/releases/tag/v9.7.1). [`bitwes/Gut` v9.7.1 release](https://github.com/bitwes/Gut/releases/tag/v9.7.1).
The downloaded release archive SHA-256 is The downloaded release archive SHA-256 is
`14969aa46adc84aa08cdd21b9f6d1a64addd92ae60b36f02d0521ed305aa4086`. `14969aa46adc84aa08cdd21b9f6d1a64addd92ae60b36f02d0521ed305aa4086`.
Terrain3D carries one intentional local Godot 4.7 compatibility patch in
`addons/terrain_3d/src/double_slider.gd`: its editor display-scale lookups use
the 4.7 `interface/editor/appearance/*` setting paths. Preserve or re-evaluate
that patch when upgrading the vendored addon; without it, the first isolated
import fails before a class cache exists.
The quality and format scripts discover `.venv` automatically; activating it The quality and format scripts discover `.venv` automatically; activating it
is optional. Missing `gdformat` or `gdlint` is a gate failure rather than a is optional. Missing `gdformat` or `gdlint` is a gate failure rather than a
silent skip. silent skip.
### Finding Godot
The macOS gate checks project metadata, `godot`/`godot4` on `PATH`, and the
standard `/Applications/Godot.app` location. The Windows gate checks project
metadata, `PATH`, common Program Files and Local AppData locations, and a
standard Scoop install. It prefers Godot's console executable when available.
For a downloaded or custom install, set the executable explicitly:
```bash
export GODOT_BIN="/Applications/Godot.app/Contents/MacOS/Godot"
```
```powershell
$env:GODOT_BIN = "C:\Tools\Godot\Godot_v4.7-stable_win64_console.exe"
```
## Usage ## Usage
### Full project check ### Full project check
```bash ```bash
# Linux / macOS / Git Bash
./tools/quality.sh ./tools/quality.sh
```
# Windows PowerShell ```powershell
& ./tools/quality.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\quality.ps1
``` ```
### Fast changed-files mode ### Fast changed-files mode
@@ -47,7 +73,10 @@ files. The Godot dependency check and all project scenarios still run.
```bash ```bash
./tools/quality.sh --changed ./tools/quality.sh --changed
& ./tools/quality.ps1 -Changed ```
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\quality.ps1 -Changed
``` ```
### Auto-format (not part of the gate) ### Auto-format (not part of the gate)
@@ -56,10 +85,27 @@ files. The Godot dependency check and all project scenarios still run.
./tools/fix_format.sh ./tools/fix_format.sh
``` ```
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\fix_format.ps1
```
Both gates and both formatter entry points operate on the same game-owned
roots: `player`, `simulation`, `tests`, `tools`, and `world`. Third-party
addons and demos remain outside automatic formatting.
### Add-on policy
Add an editor or runtime add-on only for a current gameplay or diagnostic
consumer. Pin its exact release, retain its license and source provenance, and
avoid auto-updates. Native add-ons must vendor macOS and Windows x86-64 debug
and release payloads; add those paths to the `platform-assets` manifest in both
quality scripts so either development platform detects an incomplete package.
## What it checks ## What it checks
| Check | Tool | What it detects | | Check | Tool | What it detects |
|---|---|---| |---|---|---|
| **platform-assets** | File manifest | Missing Terrain3D macOS or Windows native payloads |
| **godot-import** | Godot 4.7 headless importer | Missing or stale global `class_name` cache on fresh clones | | **godot-import** | Godot 4.7 headless importer | Missing or stale global `class_name` cache on fresh clones |
| **gdformat** | Game-owned GDScript roots | Formatting without rewriting addons or demos | | **gdformat** | Game-owned GDScript roots | Formatting without rewriting addons or demos |
| **gdlint** | Game-owned GDScript roots | Lint violations such as unused arguments and naming | | **gdlint** | Game-owned GDScript roots | Lint violations such as unused arguments and naming |
@@ -75,6 +121,7 @@ If everything passes:
QUALITY RESULT: PASS QUALITY RESULT: PASS
Godot: 4.7.stable.official.5b4e0cb0f Godot: 4.7.stable.official.5b4e0cb0f
platform-assets PASS
godot-import PASS godot-import PASS
gdformat PASS gdformat PASS
gdlint PASS gdlint PASS
@@ -92,6 +139,7 @@ On failure, only relevant errors and fix suggestions are shown:
QUALITY RESULT: FAIL QUALITY RESULT: FAIL
Godot: 4.7.stable.official.5b4e0cb0f Godot: 4.7.stable.official.5b4e0cb0f
platform-assets PASS
godot-import PASS godot-import PASS
gdformat FAIL gdformat FAIL
gdlint FAIL gdlint FAIL
@@ -132,7 +180,7 @@ python -m pip install -r requirements-dev.txt
The script exits non-zero on any failure, including a wrong Godot series or The script exits non-zero on any failure, including a wrong Godot series or
missing pinned formatter/linter, so it will fail the CI step. missing pinned formatter/linter, so it will fail the CI step.
The shell gate isolates Godot's cross-platform user-data paths under Both gates isolate Godot's cross-platform user-data paths under
`logs/quality/godot_profile`. Before running scenarios it imports project and `logs/quality/godot_profile`. Before running scenarios it imports project and
GUT global classes whenever GDScript changes, so fresh, renamed, and deleted GUT global classes whenever GDScript changes, so fresh, renamed, and deleted
`class_name` scripts cannot leave the ignored cache stale. Godot/GUT script `class_name` scripts cannot leave the ignored cache stale. Godot/GUT script
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env pwsh
# fix_format.ps1 — Auto-format all game-owned GDScript files with gdformat.
# Usage: & ./tools/fix_format.ps1
$ROOT = Split-Path -Parent $PSScriptRoot
$previousLocation = Get-Location
$exitCode = 0
function Resolve-GdFormat {
$localTool = Join-Path $ROOT ".venv/Scripts/gdformat.exe"
if (Test-Path $localTool) {
return [pscustomobject]@{ Command = $localTool; Prefix = @() }
}
$directTool = Get-Command gdformat -ErrorAction SilentlyContinue
if ($directTool) {
return [pscustomobject]@{ Command = $directTool.Source; Prefix = @() }
}
$localPython = Join-Path $ROOT ".venv/Scripts/python.exe"
$pythonCandidates = @(
[pscustomobject]@{ Command = $localPython; Prefix = @() },
[pscustomobject]@{ Command = "python"; Prefix = @() },
[pscustomobject]@{ Command = "python3"; Prefix = @() },
[pscustomobject]@{ Command = "py"; Prefix = @("-3") }
)
foreach ($candidate in $pythonCandidates) {
$isLocalPython = $candidate.Command -eq $localPython
if ($isLocalPython) {
if (-not (Test-Path $candidate.Command)) { continue }
$command = $candidate.Command
} else {
$resolved = Get-Command $candidate.Command -ErrorAction SilentlyContinue
if (-not $resolved) { continue }
$command = $resolved.Source
}
$prefix = @($candidate.Prefix)
$null = @(& $command @prefix -m gdtoolkit.formatter --help 2>&1)
if ($LASTEXITCODE -eq 0) {
return [pscustomobject]@{ Command = $command; Prefix = $prefix + @("-m", "gdtoolkit.formatter") }
}
}
return $null
}
try {
Set-Location $ROOT
$formatter = Resolve-GdFormat
if (-not $formatter) {
throw "gdformat not found. Install requirements-dev.txt; see docs/local_quality_gate.md."
}
Write-Host "Formatting all game-owned .gd files in $ROOT ..."
$command = $formatter.Command
$prefix = @($formatter.Prefix)
& $command @prefix player simulation tests tools world
if ($LASTEXITCODE -ne 0) {
throw "gdformat exited with code $LASTEXITCODE."
}
Write-Host "Done."
} catch {
Write-Host "ERROR: $($_.Exception.Message)"
$exitCode = 1
} finally {
Set-Location $previousLocation
}
exit $exitCode
+14 -2
View File
@@ -6,14 +6,26 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
python_formatter() {
local python_command
for python_command in python python3; do
if command -v "$python_command" &>/dev/null &&
"$python_command" -m gdtoolkit.formatter --help &>/dev/null 2>&1; then
echo "$python_command"
return 0
fi
done
return 1
}
# detect gdformat # detect gdformat
FMT=() FMT=()
if [[ -x "$ROOT/.venv/bin/gdformat" ]]; then if [[ -x "$ROOT/.venv/bin/gdformat" ]]; then
FMT=("$ROOT/.venv/bin/gdformat") FMT=("$ROOT/.venv/bin/gdformat")
elif command -v gdformat &>/dev/null; then elif command -v gdformat &>/dev/null; then
FMT=(gdformat) FMT=(gdformat)
elif python -m gdtoolkit.formatter --help &>/dev/null 2>&1; then elif PYTHON_FORMATTER="$(python_formatter)"; then
FMT=(python -m gdtoolkit.formatter) FMT=("$PYTHON_FORMATTER" -m gdtoolkit.formatter)
fi fi
if [[ ${#FMT[@]} -eq 0 ]]; then if [[ ${#FMT[@]} -eq 0 ]]; then
+138 -64
View File
@@ -5,6 +5,7 @@ param([switch]$Changed)
if ($args -contains '--changed') { $Changed = $true } if ($args -contains '--changed') { $Changed = $true }
$ROOT = Split-Path -Parent $PSScriptRoot $ROOT = Split-Path -Parent $PSScriptRoot
$script:PreviousLocation = Get-Location
Set-Location $ROOT Set-Location $ROOT
$LOG = "logs/quality/latest" $LOG = "logs/quality/latest"
@@ -16,12 +17,26 @@ $OVERALL = $false
$ERRORS = @() $ERRORS = @()
$FIXES = @() $FIXES = @()
$OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world") $OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world")
$CROSS_PLATFORM_PLUGIN_FILES = @(
"addons/terrain_3d/terrain.gdextension",
"addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug",
"addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release",
"addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll",
"addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll"
)
$REQUIRED_GODOT_SERIES = "4.7" $REQUIRED_GODOT_SERIES = "4.7"
function Exit-Quality {
param([int]$ExitCode)
Set-Location $script:PreviousLocation
exit $ExitCode
}
# -- tool detection ----------------------------------------------------------- # -- tool detection -----------------------------------------------------------
function ConvertTo-ConsoleGodotPath($Path) { function ConvertTo-ConsoleGodotPath($Path) {
if (-not $Path) { return $null } if (-not $Path) { return $null }
$normalized = $Path.Replace('/', '\').Trim('"') $separator = [IO.Path]::DirectorySeparatorChar
$normalized = $Path.Replace([char]47, $separator).Replace([char]92, $separator).Trim('"')
if ($normalized.EndsWith(".exe")) { if ($normalized.EndsWith(".exe")) {
$consolePath = $normalized -replace '\.exe$', '_console.exe' $consolePath = $normalized -replace '\.exe$', '_console.exe'
if (Test-Path $consolePath) { return $consolePath } if (Test-Path $consolePath) { return $consolePath }
@@ -43,75 +58,110 @@ function Find-Godot {
if ($env:GODOT_BIN) { return (ConvertTo-ConsoleGodotPath $env:GODOT_BIN) } if ($env:GODOT_BIN) { return (ConvertTo-ConsoleGodotPath $env:GODOT_BIN) }
$projectGodot = Find-ProjectGodot $projectGodot = Find-ProjectGodot
if ($projectGodot) { return $projectGodot } if ($projectGodot) { return $projectGodot }
try { return (Get-Command godot -ErrorAction Stop).Source } catch {} foreach ($name in @("godot", "godot4")) {
try { return (Get-Command godot4 -ErrorAction Stop).Source } catch {} $command = Get-Command $name -ErrorAction SilentlyContinue
$common = @( if (-not $command) { continue }
"$env:ProgramFiles\Godot\godot.exe", $candidate = ConvertTo-ConsoleGodotPath $command.Source
"${env:ProgramFiles(x86)}\Godot\godot.exe", if ($candidate) { return $candidate }
"$env:LOCALAPPDATA\Godot\godot.exe", }
"$env:USERPROFILE\AppData\Local\Godot\godot.exe"
) $common = @()
foreach ($p in $common) { if (Test-Path $p) { return $p } } if ($env:ProgramFiles) { $common += Join-Path $env:ProgramFiles "Godot/godot.exe" }
$bundled = Get-ChildItem "${env:ProgramFiles(x86)}\Godot" -Filter "Godot*_console.exe" -ErrorAction SilentlyContinue | if (${env:ProgramFiles(x86)}) { $common += Join-Path ${env:ProgramFiles(x86)} "Godot/godot.exe" }
if ($env:LOCALAPPDATA) {
$common += Join-Path $env:LOCALAPPDATA "Godot/godot.exe"
$common += Join-Path $env:LOCALAPPDATA "Programs/Godot/godot.exe"
}
if ($env:USERPROFILE) { $common += Join-Path $env:USERPROFILE "scoop/apps/godot/current/godot.exe" }
foreach ($path in $common) {
$candidate = ConvertTo-ConsoleGodotPath $path
if ($candidate) { return $candidate }
}
$searchRoots = @()
if ($env:ProgramFiles) { $searchRoots += Join-Path $env:ProgramFiles "Godot" }
if (${env:ProgramFiles(x86)}) { $searchRoots += Join-Path ${env:ProgramFiles(x86)} "Godot" }
if ($env:LOCALAPPDATA) {
$searchRoots += Join-Path $env:LOCALAPPDATA "Godot"
$searchRoots += Join-Path $env:LOCALAPPDATA "Programs/Godot"
}
foreach ($searchRoot in $searchRoots) {
$bundled = Get-ChildItem $searchRoot -Filter "Godot*_console.exe" -File -ErrorAction SilentlyContinue |
Sort-Object Name -Descending | Sort-Object Name -Descending |
Select-Object -First 1 Select-Object -First 1
if ($bundled) { return $bundled.FullName } if ($bundled) { return $bundled.FullName }
}
return $null return $null
} }
function Resolve-GdTool {
param(
[string]$ExecutableName,
[string]$ModuleName
)
$localTool = Join-Path $ROOT ".venv/Scripts/$ExecutableName.exe"
if (Test-Path $localTool) {
return [pscustomobject]@{ Command = $localTool; Prefix = @() }
}
$directTool = Get-Command $ExecutableName -ErrorAction SilentlyContinue
if ($directTool) {
return [pscustomobject]@{ Command = $directTool.Source; Prefix = @() }
}
$localPython = Join-Path $ROOT ".venv/Scripts/python.exe"
$pythonCandidates = @(
[pscustomobject]@{ Command = $localPython; Prefix = @() },
[pscustomobject]@{ Command = "python"; Prefix = @() },
[pscustomobject]@{ Command = "python3"; Prefix = @() },
[pscustomobject]@{ Command = "py"; Prefix = @("-3") }
)
foreach ($candidate in $pythonCandidates) {
$isLocalPython = $candidate.Command -eq $localPython
if ($isLocalPython) {
if (-not (Test-Path $candidate.Command)) { continue }
$command = $candidate.Command
} else {
$resolved = Get-Command $candidate.Command -ErrorAction SilentlyContinue
if (-not $resolved) { continue }
$command = $resolved.Source
}
$prefix = @($candidate.Prefix)
$null = @(& $command @prefix -m $ModuleName --help 2>&1)
if ($LASTEXITCODE -eq 0) {
return [pscustomobject]@{ Command = $command; Prefix = $prefix + @("-m", $ModuleName) }
}
}
return $null
}
$script:GdFormatRunner = $null
$script:GdLintRunner = $null
function Test-GdFormat { function Test-GdFormat {
if (Test-Path "$ROOT/.venv/Scripts/gdformat.exe") { return $true } $script:GdFormatRunner = Resolve-GdTool "gdformat" "gdtoolkit.formatter"
if (Get-Command gdformat -ErrorAction SilentlyContinue) { return $true } return $null -ne $script:GdFormatRunner
try {
$null = & python -m gdtoolkit.formatter --help 2>&1
if ($LASTEXITCODE -eq 0) { return $true }
} catch {}
try {
$null = & python3 -m gdtoolkit.formatter --help 2>&1
if ($LASTEXITCODE -eq 0) { return $true }
} catch {}
return $false
} }
function Test-GdLint { function Test-GdLint {
if (Test-Path "$ROOT/.venv/Scripts/gdlint.exe") { return $true } $script:GdLintRunner = Resolve-GdTool "gdlint" "gdtoolkit.linter"
if (Get-Command gdlint -ErrorAction SilentlyContinue) { return $true } return $null -ne $script:GdLintRunner
try {
$null = & python -m gdtoolkit.linter --help 2>&1
if ($LASTEXITCODE -eq 0) { return $true }
} catch {}
try {
$null = & python3 -m gdtoolkit.linter --help 2>&1
if ($LASTEXITCODE -eq 0) { return $true }
} catch {}
return $false
} }
$script:GdToolExitCode = 0 $script:GdToolExitCode = 0
function Invoke-GdFormat { function Invoke-GdFormat {
$args = $args $command = $script:GdFormatRunner.Command
$localTool = "$ROOT/.venv/Scripts/gdformat.exe" $prefix = @($script:GdFormatRunner.Prefix)
if (Test-Path $localTool) { & $command @prefix @args 2>&1
& $localTool @args 2>&1
} elseif (Get-Command gdformat -ErrorAction SilentlyContinue) {
& gdformat @args 2>&1
} else {
& python -m gdtoolkit.formatter @args 2>&1
}
$script:GdToolExitCode = $LASTEXITCODE $script:GdToolExitCode = $LASTEXITCODE
} }
function Invoke-GdLint { function Invoke-GdLint {
$args = $args $command = $script:GdLintRunner.Command
$localTool = "$ROOT/.venv/Scripts/gdlint.exe" $prefix = @($script:GdLintRunner.Prefix)
if (Test-Path $localTool) { & $command @prefix @args 2>&1
& $localTool @args 2>&1
} elseif (Get-Command gdlint -ErrorAction SilentlyContinue) {
& gdlint @args 2>&1
} else {
& python -m gdtoolkit.linter @args 2>&1
}
$script:GdToolExitCode = $LASTEXITCODE $script:GdToolExitCode = $LASTEXITCODE
} }
@@ -123,7 +173,11 @@ function Get-ChangedGd {
function To-ResPath($p) { function To-ResPath($p) {
$p = $p.Replace('\', '/') $p = $p.Replace('\', '/')
if ($p.StartsWith($ROOT)) { $p = $p.Substring($ROOT.Length + 1) } $normalizedRoot = $ROOT.Replace('\', '/').TrimEnd('/')
$rootPrefix = "$normalizedRoot/"
if ($p.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
$p = $p.Substring($rootPrefix.Length)
}
if ($p.StartsWith('./')) { $p = $p.Substring(2) } if ($p.StartsWith('./')) { $p = $p.Substring(2) }
return "res://$p" return "res://$p"
} }
@@ -146,6 +200,8 @@ function Invoke-GodotWithTimeout {
$env:LOCALAPPDATA = $GODOT_PROFILE $env:LOCALAPPDATA = $GODOT_PROFILE
try { try {
$p = [Diagnostics.Process]::Start($psi) $p = [Diagnostics.Process]::Start($psi)
} catch {
return "Failed to start Godot: $($_.Exception.Message)", $null
} finally { } finally {
$env:APPDATA = $previousAppData $env:APPDATA = $previousAppData
$env:LOCALAPPDATA = $previousLocalAppData $env:LOCALAPPDATA = $previousLocalAppData
@@ -158,7 +214,7 @@ function Invoke-GodotWithTimeout {
$p.WaitForExit() $p.WaitForExit()
$output = $outputTask.Result $output = $outputTask.Result
$err = $errorTask.Result $err = $errorTask.Result
return $output + $err, $null return ($output + $err), $null
} }
$output = $outputTask.Result $output = $outputTask.Result
$err = $errorTask.Result $err = $errorTask.Result
@@ -169,22 +225,36 @@ function Invoke-GodotWithTimeout {
$GODOT = Find-Godot $GODOT = Find-Godot
if (-not $GODOT) { if (-not $GODOT) {
Write-Host "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH." Write-Host "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
exit 1 Exit-Quality 1
} }
$godotVersionOutput = @(& $GODOT --version 2>&1) $godotVersionOutput = @(& $GODOT --version 2>&1)
$godotVersionExit = $LASTEXITCODE $godotVersionExit = $LASTEXITCODE
if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) { if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) {
Write-Host "ERROR: Could not query the Godot version from '$GODOT'." Write-Host "ERROR: Could not query the Godot version from '$GODOT'."
exit 1 Exit-Quality 1
} }
$GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim() $GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim()
$requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)' $requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)'
if ($GODOT_VERSION -notmatch $requiredVersionPattern) { if ($GODOT_VERSION -notmatch $requiredVersionPattern) {
Write-Host "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'." Write-Host "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'."
exit 1 Exit-Quality 1
} }
# -- 0. Godot import bootstrap ------------------------------------------------ # -- 0. Cross-platform plugin payload -----------------------------------------
$platformAssetsResult = "PASS"
Set-Content -Path "$LOG/platform-assets.log" -Value "" -Encoding utf8
foreach ($pluginFile in $CROSS_PLATFORM_PLUGIN_FILES) {
if (-not (Test-Path $pluginFile -PathType Leaf)) {
$platformAssetsResult = "FAIL"
Add-Content -Path "$LOG/platform-assets.log" -Value "Missing $pluginFile" -Encoding utf8
$ERRORS += "platform-assets: missing res://$pluginFile"
}
}
if ($platformAssetsResult -eq "FAIL") {
$FIXES += "Restore the complete pinned Terrain3D 1.0.2 package"
}
# -- 1. Godot import bootstrap ------------------------------------------------
$importResult = "PASS" $importResult = "PASS"
Set-Content -Path "$LOG/godot-import.log" -Value "" -Encoding utf8 Set-Content -Path "$LOG/godot-import.log" -Value "" -Encoding utf8
$classCache = Join-Path $ROOT ".godot/global_script_class_cache.cfg" $classCache = Join-Path $ROOT ".godot/global_script_class_cache.cfg"
@@ -223,7 +293,7 @@ if ($importResult -eq "FAIL") {
$FIXES += "Fix Godot import errors before running headless checks" $FIXES += "Fix Godot import errors before running headless checks"
} }
# -- 1. gdformat -------------------------------------------------------------- # -- 2. gdformat --------------------------------------------------------------
$fmtAvail = Test-GdFormat $fmtAvail = Test-GdFormat
$fmtResult = "PASS" $fmtResult = "PASS"
$fmtFailed = $false $fmtFailed = $false
@@ -275,7 +345,7 @@ if ($formatDiagnostics) {
$FIXES += "Fix the gdformat error before continuing" $FIXES += "Fix the gdformat error before continuing"
} }
# -- 2. gdlint ---------------------------------------------------------------- # -- 3. gdlint ----------------------------------------------------------------
$lintAvail = Test-GdLint $lintAvail = Test-GdLint
$lintResult = "PASS" $lintResult = "PASS"
$lintFailed = $false $lintFailed = $false
@@ -328,7 +398,7 @@ if ($lintLines) {
$FIXES += "Fix the gdlint error before continuing" $FIXES += "Fix the gdlint error before continuing"
} }
# -- 3. godot headless check -------------------------------------------------- # -- 4. godot headless check --------------------------------------------------
$godotResult = "PASS" $godotResult = "PASS"
Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8 Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8
$godotArgs = "--headless --path `"$ROOT`" --script res://tests/simulation_definitions_test.gd" $godotArgs = "--headless --path `"$ROOT`" --script res://tests/simulation_definitions_test.gd"
@@ -353,7 +423,7 @@ if ($godotLoadError) {
$FIXES += "Fix Godot parser errors" $FIXES += "Fix Godot parser errors"
} }
# -- 4. project scenario tests ------------------------------------------------ # -- 5. project scenario tests ------------------------------------------------
$scenarioResult = "PASS" $scenarioResult = "PASS"
Set-Content -Path "$LOG/scenarios.log" -Value "" -Encoding utf8 Set-Content -Path "$LOG/scenarios.log" -Value "" -Encoding utf8
$scenarioTests = Get-ChildItem "tests" -Filter "*_test.gd" | Sort-Object Name $scenarioTests = Get-ChildItem "tests" -Filter "*_test.gd" | Sort-Object Name
@@ -376,7 +446,7 @@ if ($scenarioResult -eq "FAIL") {
$FIXES += "Fix failing project scenario tests" $FIXES += "Fix failing project scenario tests"
} }
# -- 5. GUT ------------------------------------------------------------------- # -- 6. GUT -------------------------------------------------------------------
$gutResult = "PASS" $gutResult = "PASS"
Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8 Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8
if (-not (Test-Path "addons/gut/gut_cmdln.gd")) { if (-not (Test-Path "addons/gut/gut_cmdln.gd")) {
@@ -410,7 +480,9 @@ if (-not (Test-Path "addons/gut/gut_cmdln.gd")) {
} }
# -- summary ------------------------------------------------------------------ # -- summary ------------------------------------------------------------------
if ($importResult -eq 'FAIL' -or $fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL' -or $godotResult -eq 'FAIL' -or $scenarioResult -eq 'FAIL' -or $gutResult -eq 'FAIL') { if ($platformAssetsResult -eq 'FAIL' -or $importResult -eq 'FAIL' -or
$fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL' -or $godotResult -eq 'FAIL' -or
$scenarioResult -eq 'FAIL' -or $gutResult -eq 'FAIL') {
$OVERALL = $true $OVERALL = $true
} }
@@ -418,6 +490,7 @@ Write-Host ""
if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" } if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" }
Write-Host "Godot: $GODOT_VERSION" Write-Host "Godot: $GODOT_VERSION"
Write-Host "" Write-Host ""
Write-Host " platform-assets $platformAssetsResult"
Write-Host " godot-import $importResult" Write-Host " godot-import $importResult"
Write-Host " gdformat $fmtResult" Write-Host " gdformat $fmtResult"
Write-Host " gdlint $lintResult" Write-Host " gdlint $lintResult"
@@ -456,6 +529,7 @@ $summary = @"
QUALITY RESULT: $resultText QUALITY RESULT: $resultText
Godot: $GODOT_VERSION Godot: $GODOT_VERSION
platform-assets $platformAssetsResult
godot-import $importResult godot-import $importResult
gdformat $fmtResult gdformat $fmtResult
gdlint $lintResult gdlint $lintResult
@@ -465,4 +539,4 @@ Godot: $GODOT_VERSION
"@ "@
Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8 Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8
if ($OVERALL) { exit 1 } else { exit 0 } if ($OVERALL) { Exit-Quality 1 } else { Exit-Quality 0 }
+68 -12
View File
@@ -18,6 +18,13 @@ OVERALL=false
ERRORS=() ERRORS=()
FIXES=() FIXES=()
OWNED_GDSCRIPT_ROOTS=(player simulation tests tools world) OWNED_GDSCRIPT_ROOTS=(player simulation tests tools world)
CROSS_PLATFORM_PLUGIN_FILES=(
addons/terrain_3d/terrain.gdextension
addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug
addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release
addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll
addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll
)
REQUIRED_GODOT_SERIES="4.7" REQUIRED_GODOT_SERIES="4.7"
# -- tool detection ----------------------------------------------------------- # -- tool detection -----------------------------------------------------------
@@ -48,17 +55,47 @@ find_project_godot() {
} }
find_godot() { find_godot() {
if [[ -n "${GODOT_BIN:-}" ]]; then console_godot_path "$GODOT_BIN" && return 0; echo "$GODOT_BIN"; return 0; fi if [[ -n "${GODOT_BIN:-}" ]]; then
if console_godot_path "$GODOT_BIN"; then return 0; fi
echo "ERROR: GODOT_BIN does not point to a Godot executable: $GODOT_BIN" >&2
return 1
fi
find_project_godot && return 0 find_project_godot && return 0
for c in godot godot4; do command -v "$c" &>/dev/null && echo "$c" && return 0; done for c in godot godot4; do command -v "$c" &>/dev/null && echo "$c" && return 0; done
console_godot_path "/Applications/Godot.app/Contents/MacOS/Godot" && return 0 console_godot_path "/Applications/Godot.app/Contents/MacOS/Godot" && return 0
return 1 return 1
} }
has_python_module() {
local module="$1"
local python_command
for python_command in python python3; do
if command -v "$python_command" &>/dev/null &&
"$python_command" -m "$module" --help &>/dev/null 2>&1; then
return 0
fi
done
return 1
}
run_python_module() {
local module="$1"
shift
local python_command
for python_command in python python3; do
if command -v "$python_command" &>/dev/null &&
"$python_command" -m "$module" --help &>/dev/null 2>&1; then
"$python_command" -m "$module" "$@"
return $?
fi
done
return 127
}
has_gdformat() { has_gdformat() {
[[ -x "$ROOT/.venv/bin/gdformat" ]] && return 0 [[ -x "$ROOT/.venv/bin/gdformat" ]] && return 0
command -v gdformat &>/dev/null && return 0 command -v gdformat &>/dev/null && return 0
python -m gdtoolkit.formatter --help &>/dev/null 2>&1 has_python_module gdtoolkit.formatter
} }
run_gdformat() { run_gdformat() {
@@ -67,14 +104,14 @@ run_gdformat() {
elif command -v gdformat &>/dev/null; then elif command -v gdformat &>/dev/null; then
gdformat "$@" gdformat "$@"
else else
python -m gdtoolkit.formatter "$@" run_python_module gdtoolkit.formatter "$@"
fi fi
} }
has_gdlint() { has_gdlint() {
[[ -x "$ROOT/.venv/bin/gdlint" ]] && return 0 [[ -x "$ROOT/.venv/bin/gdlint" ]] && return 0
command -v gdlint &>/dev/null && return 0 command -v gdlint &>/dev/null && return 0
python -m gdtoolkit.linter --help &>/dev/null 2>&1 has_python_module gdtoolkit.linter
} }
run_gdlint() { run_gdlint() {
@@ -83,7 +120,7 @@ run_gdlint() {
elif command -v gdlint &>/dev/null; then elif command -v gdlint &>/dev/null; then
gdlint "$@" gdlint "$@"
else else
python -m gdtoolkit.linter "$@" run_python_module gdtoolkit.linter "$@"
fi fi
} }
@@ -161,7 +198,21 @@ case "$GODOT_VERSION" in
esac esac
GODOT_ENV=(env "HOME=$GODOT_PROFILE" "APPDATA=$GODOT_PROFILE" "LOCALAPPDATA=$GODOT_PROFILE") GODOT_ENV=(env "HOME=$GODOT_PROFILE" "APPDATA=$GODOT_PROFILE" "LOCALAPPDATA=$GODOT_PROFILE")
# -- 0. Godot import bootstrap ------------------------------------------------ # -- 0. Cross-platform plugin payload -----------------------------------------
platform_assets_result="PASS"
: > "$LOG/platform-assets.log"
for plugin_file in "${CROSS_PLATFORM_PLUGIN_FILES[@]}"; do
if [[ ! -f "$plugin_file" ]]; then
platform_assets_result="FAIL"
echo "Missing $plugin_file" >> "$LOG/platform-assets.log"
ERRORS+=("platform-assets: missing res://$plugin_file")
fi
done
if [[ "$platform_assets_result" == "FAIL" ]]; then
FIXES+=("Restore the complete pinned Terrain3D 1.0.2 package")
fi
# -- 1. Godot import bootstrap ------------------------------------------------
import_result="PASS" import_result="PASS"
: > "$LOG/godot-import.log" : > "$LOG/godot-import.log"
class_cache="$ROOT/.godot/global_script_class_cache.cfg" class_cache="$ROOT/.godot/global_script_class_cache.cfg"
@@ -195,7 +246,7 @@ if [[ "$import_result" == "FAIL" ]]; then
FIXES+=("Fix Godot import errors before running headless checks") FIXES+=("Fix Godot import errors before running headless checks")
fi fi
# -- 1. gdformat -------------------------------------------------------------- # -- 2. gdformat --------------------------------------------------------------
fmt_result="PASS" fmt_result="PASS"
fmt_failed=false fmt_failed=false
: > "$LOG/gdformat.log" : > "$LOG/gdformat.log"
@@ -243,7 +294,7 @@ elif $fmt_failed; then
FIXES+=("Fix the gdformat error before continuing") FIXES+=("Fix the gdformat error before continuing")
fi fi
# -- 2. gdlint ---------------------------------------------------------------- # -- 3. gdlint ----------------------------------------------------------------
lint_result="PASS" lint_result="PASS"
lint_failed=false lint_failed=false
: > "$LOG/gdlint.log" : > "$LOG/gdlint.log"
@@ -297,7 +348,7 @@ elif $lint_failed; then
FIXES+=("Fix the gdlint error before continuing") FIXES+=("Fix the gdlint error before continuing")
fi fi
# -- 3. godot headless check -------------------------------------------------- # -- 4. godot headless check --------------------------------------------------
godot_result="PASS" godot_result="PASS"
: > "$LOG/godot-check.log" : > "$LOG/godot-check.log"
if run_with_timeout 30 "${GODOT_ENV[@]}" "$GODOT" --headless --path "$ROOT" --script res://tests/simulation_definitions_test.gd >> "$LOG/godot-check.log" 2>&1; then if run_with_timeout 30 "${GODOT_ENV[@]}" "$GODOT" --headless --path "$ROOT" --script res://tests/simulation_definitions_test.gd >> "$LOG/godot-check.log" 2>&1; then
@@ -319,7 +370,7 @@ if [[ "$godot_result" == "FAIL" ]]; then
FIXES+=("Fix Godot parser errors") FIXES+=("Fix Godot parser errors")
fi fi
# -- 4. project scenario tests ------------------------------------------------ # -- 5. project scenario tests ------------------------------------------------
scenario_result="PASS" scenario_result="PASS"
: > "$LOG/scenarios.log" : > "$LOG/scenarios.log"
for test in tests/*_test.gd; do for test in tests/*_test.gd; do
@@ -337,7 +388,7 @@ if [[ "$scenario_result" == "FAIL" ]]; then
FIXES+=("Fix failing project scenario tests") FIXES+=("Fix failing project scenario tests")
fi fi
# -- 5. GUT ------------------------------------------------------------------- # -- 6. GUT -------------------------------------------------------------------
gut_result="PASS" gut_result="PASS"
: > "$LOG/gut.log" : > "$LOG/gut.log"
if [[ ! -f "addons/gut/gut_cmdln.gd" ]]; then if [[ ! -f "addons/gut/gut_cmdln.gd" ]]; then
@@ -365,7 +416,10 @@ else
fi fi
# -- summary ------------------------------------------------------------------ # -- summary ------------------------------------------------------------------
if [[ "$import_result" == "FAIL" || "$fmt_result" == "FAIL" || "$lint_result" == "FAIL" || "$godot_result" == "FAIL" || "$scenario_result" == "FAIL" || "$gut_result" == "FAIL" ]]; then if [[ "$platform_assets_result" == "FAIL" || "$import_result" == "FAIL" ||
"$fmt_result" == "FAIL" || "$lint_result" == "FAIL" ||
"$godot_result" == "FAIL" || "$scenario_result" == "FAIL" ||
"$gut_result" == "FAIL" ]]; then
OVERALL=true OVERALL=true
fi fi
@@ -377,6 +431,7 @@ else
fi fi
echo "Godot: $GODOT_VERSION" echo "Godot: $GODOT_VERSION"
echo "" echo ""
echo " platform-assets $platform_assets_result"
echo " godot-import $import_result" echo " godot-import $import_result"
echo " gdformat $fmt_result" echo " gdformat $fmt_result"
echo " gdlint $lint_result" echo " gdlint $lint_result"
@@ -415,6 +470,7 @@ echo ""
echo "QUALITY RESULT: $($OVERALL && echo FAIL || echo PASS)" echo "QUALITY RESULT: $($OVERALL && echo FAIL || echo PASS)"
echo "Godot: $GODOT_VERSION" echo "Godot: $GODOT_VERSION"
echo "" echo ""
echo " platform-assets $platform_assets_result"
echo " godot-import $import_result" echo " godot-import $import_result"
echo " gdformat $fmt_result" echo " gdformat $fmt_result"
echo " gdlint $lint_result" echo " gdlint $lint_result"