From 5511c295a57c4030083f9c88397154fa2a50c8e4 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Thu, 30 Jul 2026 13:42:38 +0200 Subject: [PATCH] fix: harden cross-platform quality tooling --- README.md | 24 ++- addons/terrain_3d/src/double_slider.gd | 4 +- docs/local_quality_gate.md | 64 +++++++- tools/fix_format.ps1 | 69 ++++++++ tools/fix_format.sh | 16 +- tools/quality.ps1 | 208 +++++++++++++++++-------- tools/quality.sh | 80 ++++++++-- 7 files changed, 370 insertions(+), 95 deletions(-) create mode 100644 tools/fix_format.ps1 diff --git a/README.md b/README.md index e5be812..9e8e74a 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,28 @@ Set up the local tools with `uv`: ```bash 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. ## Quality gate -Run the complete local gate before committing: +Run the complete local gate before committing. + +macOS: ```bash ./tools/quality.sh ``` -On Windows: +Windows 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 @@ -43,6 +48,17 @@ the Godot checks and scenarios: ./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 scenarios, and the vendored GUT suite. See [`docs/local_quality_gate.md`](docs/local_quality_gate.md) for setup and diff --git a/addons/terrain_3d/src/double_slider.gd b/addons/terrain_3d/src/double_slider.gd index 6536aef..1d4fe4c 100644 --- a/addons/terrain_3d/src/double_slider.gd +++ b/addons/terrain_3d/src/double_slider.gd @@ -22,11 +22,11 @@ func _ready() -> void: # Setup Display Scale # 0 auto, 1 75%, 2 100%, 3 125%, 4 150%, 5 175%, 6 200%, 7 custom 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: ds = 2 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: display_scale = float(ds + 2) * .25 diff --git a/docs/local_quality_gate.md b/docs/local_quality_gate.md index e24e6da..6462075 100644 --- a/docs/local_quality_gate.md +++ b/docs/local_quality_gate.md @@ -6,38 +6,64 @@ parser errors, and LLM-generated garbage before committing. ## Requirements - **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 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 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 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 [`bitwes/Gut` v9.7.1 release](https://github.com/bitwes/Gut/releases/tag/v9.7.1). The downloaded release archive SHA-256 is `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 is optional. Missing `gdformat` or `gdlint` is a gate failure rather than a 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 ### Full project check ```bash -# Linux / macOS / Git Bash ./tools/quality.sh +``` -# Windows PowerShell -& ./tools/quality.ps1 +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\quality.ps1 ``` ### Fast changed-files mode @@ -47,7 +73,10 @@ files. The Godot dependency check and all project scenarios still run. ```bash ./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) @@ -56,10 +85,27 @@ files. The Godot dependency check and all project scenarios still run. ./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 | 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 | | **gdformat** | Game-owned GDScript roots | Formatting without rewriting addons or demos | | **gdlint** | Game-owned GDScript roots | Lint violations such as unused arguments and naming | @@ -75,6 +121,7 @@ If everything passes: QUALITY RESULT: PASS Godot: 4.7.stable.official.5b4e0cb0f + platform-assets PASS godot-import PASS gdformat PASS gdlint PASS @@ -92,6 +139,7 @@ On failure, only relevant errors and fix suggestions are shown: QUALITY RESULT: FAIL Godot: 4.7.stable.official.5b4e0cb0f + platform-assets PASS godot-import PASS gdformat 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 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 GUT global classes whenever GDScript changes, so fresh, renamed, and deleted `class_name` scripts cannot leave the ignored cache stale. Godot/GUT script diff --git a/tools/fix_format.ps1 b/tools/fix_format.ps1 new file mode 100644 index 0000000..d04e25e --- /dev/null +++ b/tools/fix_format.ps1 @@ -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 diff --git a/tools/fix_format.sh b/tools/fix_format.sh index 7e16e02..873c32b 100755 --- a/tools/fix_format.sh +++ b/tools/fix_format.sh @@ -6,14 +6,26 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" 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 FMT=() if [[ -x "$ROOT/.venv/bin/gdformat" ]]; then FMT=("$ROOT/.venv/bin/gdformat") elif command -v gdformat &>/dev/null; then FMT=(gdformat) -elif python -m gdtoolkit.formatter --help &>/dev/null 2>&1; then - FMT=(python -m gdtoolkit.formatter) +elif PYTHON_FORMATTER="$(python_formatter)"; then + FMT=("$PYTHON_FORMATTER" -m gdtoolkit.formatter) fi if [[ ${#FMT[@]} -eq 0 ]]; then diff --git a/tools/quality.ps1 b/tools/quality.ps1 index 24ae760..d987185 100644 --- a/tools/quality.ps1 +++ b/tools/quality.ps1 @@ -5,6 +5,7 @@ param([switch]$Changed) if ($args -contains '--changed') { $Changed = $true } $ROOT = Split-Path -Parent $PSScriptRoot +$script:PreviousLocation = Get-Location Set-Location $ROOT $LOG = "logs/quality/latest" @@ -16,12 +17,26 @@ $OVERALL = $false $ERRORS = @() $FIXES = @() $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" +function Exit-Quality { + param([int]$ExitCode) + Set-Location $script:PreviousLocation + exit $ExitCode +} + # -- tool detection ----------------------------------------------------------- function ConvertTo-ConsoleGodotPath($Path) { 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")) { $consolePath = $normalized -replace '\.exe$', '_console.exe' if (Test-Path $consolePath) { return $consolePath } @@ -43,75 +58,110 @@ function Find-Godot { if ($env:GODOT_BIN) { return (ConvertTo-ConsoleGodotPath $env:GODOT_BIN) } $projectGodot = Find-ProjectGodot if ($projectGodot) { return $projectGodot } - try { return (Get-Command godot -ErrorAction Stop).Source } catch {} - try { return (Get-Command godot4 -ErrorAction Stop).Source } catch {} - $common = @( - "$env:ProgramFiles\Godot\godot.exe", - "${env:ProgramFiles(x86)}\Godot\godot.exe", - "$env:LOCALAPPDATA\Godot\godot.exe", - "$env:USERPROFILE\AppData\Local\Godot\godot.exe" - ) - foreach ($p in $common) { if (Test-Path $p) { return $p } } - $bundled = Get-ChildItem "${env:ProgramFiles(x86)}\Godot" -Filter "Godot*_console.exe" -ErrorAction SilentlyContinue | - Sort-Object Name -Descending | - Select-Object -First 1 - if ($bundled) { return $bundled.FullName } + foreach ($name in @("godot", "godot4")) { + $command = Get-Command $name -ErrorAction SilentlyContinue + if (-not $command) { continue } + $candidate = ConvertTo-ConsoleGodotPath $command.Source + if ($candidate) { return $candidate } + } + + $common = @() + if ($env:ProgramFiles) { $common += Join-Path $env:ProgramFiles "Godot/godot.exe" } + 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 | + Select-Object -First 1 + if ($bundled) { return $bundled.FullName } + } 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 { - if (Test-Path "$ROOT/.venv/Scripts/gdformat.exe") { return $true } - if (Get-Command gdformat -ErrorAction SilentlyContinue) { return $true } - 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 + $script:GdFormatRunner = Resolve-GdTool "gdformat" "gdtoolkit.formatter" + return $null -ne $script:GdFormatRunner } function Test-GdLint { - if (Test-Path "$ROOT/.venv/Scripts/gdlint.exe") { return $true } - if (Get-Command gdlint -ErrorAction SilentlyContinue) { return $true } - 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:GdLintRunner = Resolve-GdTool "gdlint" "gdtoolkit.linter" + return $null -ne $script:GdLintRunner } $script:GdToolExitCode = 0 function Invoke-GdFormat { - $args = $args - $localTool = "$ROOT/.venv/Scripts/gdformat.exe" - if (Test-Path $localTool) { - & $localTool @args 2>&1 - } elseif (Get-Command gdformat -ErrorAction SilentlyContinue) { - & gdformat @args 2>&1 - } else { - & python -m gdtoolkit.formatter @args 2>&1 - } + $command = $script:GdFormatRunner.Command + $prefix = @($script:GdFormatRunner.Prefix) + & $command @prefix @args 2>&1 $script:GdToolExitCode = $LASTEXITCODE } function Invoke-GdLint { - $args = $args - $localTool = "$ROOT/.venv/Scripts/gdlint.exe" - if (Test-Path $localTool) { - & $localTool @args 2>&1 - } elseif (Get-Command gdlint -ErrorAction SilentlyContinue) { - & gdlint @args 2>&1 - } else { - & python -m gdtoolkit.linter @args 2>&1 - } + $command = $script:GdLintRunner.Command + $prefix = @($script:GdLintRunner.Prefix) + & $command @prefix @args 2>&1 $script:GdToolExitCode = $LASTEXITCODE } @@ -123,7 +173,11 @@ function Get-ChangedGd { function To-ResPath($p) { $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) } return "res://$p" } @@ -146,6 +200,8 @@ function Invoke-GodotWithTimeout { $env:LOCALAPPDATA = $GODOT_PROFILE try { $p = [Diagnostics.Process]::Start($psi) + } catch { + return "Failed to start Godot: $($_.Exception.Message)", $null } finally { $env:APPDATA = $previousAppData $env:LOCALAPPDATA = $previousLocalAppData @@ -158,7 +214,7 @@ function Invoke-GodotWithTimeout { $p.WaitForExit() $output = $outputTask.Result $err = $errorTask.Result - return $output + $err, $null + return ($output + $err), $null } $output = $outputTask.Result $err = $errorTask.Result @@ -169,22 +225,36 @@ function Invoke-GodotWithTimeout { $GODOT = Find-Godot if (-not $GODOT) { 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) $godotVersionExit = $LASTEXITCODE if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) { Write-Host "ERROR: Could not query the Godot version from '$GODOT'." - exit 1 + Exit-Quality 1 } $GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim() $requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)' if ($GODOT_VERSION -notmatch $requiredVersionPattern) { 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" Set-Content -Path "$LOG/godot-import.log" -Value "" -Encoding utf8 $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" } -# -- 1. gdformat -------------------------------------------------------------- +# -- 2. gdformat -------------------------------------------------------------- $fmtAvail = Test-GdFormat $fmtResult = "PASS" $fmtFailed = $false @@ -275,7 +345,7 @@ if ($formatDiagnostics) { $FIXES += "Fix the gdformat error before continuing" } -# -- 2. gdlint ---------------------------------------------------------------- +# -- 3. gdlint ---------------------------------------------------------------- $lintAvail = Test-GdLint $lintResult = "PASS" $lintFailed = $false @@ -328,7 +398,7 @@ if ($lintLines) { $FIXES += "Fix the gdlint error before continuing" } -# -- 3. godot headless check -------------------------------------------------- +# -- 4. godot headless check -------------------------------------------------- $godotResult = "PASS" Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8 $godotArgs = "--headless --path `"$ROOT`" --script res://tests/simulation_definitions_test.gd" @@ -353,7 +423,7 @@ if ($godotLoadError) { $FIXES += "Fix Godot parser errors" } -# -- 4. project scenario tests ------------------------------------------------ +# -- 5. project scenario tests ------------------------------------------------ $scenarioResult = "PASS" Set-Content -Path "$LOG/scenarios.log" -Value "" -Encoding utf8 $scenarioTests = Get-ChildItem "tests" -Filter "*_test.gd" | Sort-Object Name @@ -376,7 +446,7 @@ if ($scenarioResult -eq "FAIL") { $FIXES += "Fix failing project scenario tests" } -# -- 5. GUT ------------------------------------------------------------------- +# -- 6. GUT ------------------------------------------------------------------- $gutResult = "PASS" Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8 if (-not (Test-Path "addons/gut/gut_cmdln.gd")) { @@ -410,7 +480,9 @@ if (-not (Test-Path "addons/gut/gut_cmdln.gd")) { } # -- 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 } @@ -418,6 +490,7 @@ Write-Host "" if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" } Write-Host "Godot: $GODOT_VERSION" Write-Host "" +Write-Host " platform-assets $platformAssetsResult" Write-Host " godot-import $importResult" Write-Host " gdformat $fmtResult" Write-Host " gdlint $lintResult" @@ -456,6 +529,7 @@ $summary = @" QUALITY RESULT: $resultText Godot: $GODOT_VERSION + platform-assets $platformAssetsResult godot-import $importResult gdformat $fmtResult gdlint $lintResult @@ -465,4 +539,4 @@ Godot: $GODOT_VERSION "@ 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 } diff --git a/tools/quality.sh b/tools/quality.sh index b76ddd4..71a2148 100755 --- a/tools/quality.sh +++ b/tools/quality.sh @@ -18,6 +18,13 @@ OVERALL=false ERRORS=() FIXES=() 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" # -- tool detection ----------------------------------------------------------- @@ -48,17 +55,47 @@ find_project_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 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 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() { [[ -x "$ROOT/.venv/bin/gdformat" ]] && 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() { @@ -67,14 +104,14 @@ run_gdformat() { elif command -v gdformat &>/dev/null; then gdformat "$@" else - python -m gdtoolkit.formatter "$@" + run_python_module gdtoolkit.formatter "$@" fi } has_gdlint() { [[ -x "$ROOT/.venv/bin/gdlint" ]] && 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() { @@ -83,7 +120,7 @@ run_gdlint() { elif command -v gdlint &>/dev/null; then gdlint "$@" else - python -m gdtoolkit.linter "$@" + run_python_module gdtoolkit.linter "$@" fi } @@ -161,7 +198,21 @@ case "$GODOT_VERSION" in esac 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" : > "$LOG/godot-import.log" 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") fi -# -- 1. gdformat -------------------------------------------------------------- +# -- 2. gdformat -------------------------------------------------------------- fmt_result="PASS" fmt_failed=false : > "$LOG/gdformat.log" @@ -243,7 +294,7 @@ elif $fmt_failed; then FIXES+=("Fix the gdformat error before continuing") fi -# -- 2. gdlint ---------------------------------------------------------------- +# -- 3. gdlint ---------------------------------------------------------------- lint_result="PASS" lint_failed=false : > "$LOG/gdlint.log" @@ -297,7 +348,7 @@ elif $lint_failed; then FIXES+=("Fix the gdlint error before continuing") fi -# -- 3. godot headless check -------------------------------------------------- +# -- 4. godot headless check -------------------------------------------------- godot_result="PASS" : > "$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 @@ -319,7 +370,7 @@ if [[ "$godot_result" == "FAIL" ]]; then FIXES+=("Fix Godot parser errors") fi -# -- 4. project scenario tests ------------------------------------------------ +# -- 5. project scenario tests ------------------------------------------------ scenario_result="PASS" : > "$LOG/scenarios.log" for test in tests/*_test.gd; do @@ -337,7 +388,7 @@ if [[ "$scenario_result" == "FAIL" ]]; then FIXES+=("Fix failing project scenario tests") fi -# -- 5. GUT ------------------------------------------------------------------- +# -- 6. GUT ------------------------------------------------------------------- gut_result="PASS" : > "$LOG/gut.log" if [[ ! -f "addons/gut/gut_cmdln.gd" ]]; then @@ -365,7 +416,10 @@ else fi # -- 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 fi @@ -377,6 +431,7 @@ else fi echo "Godot: $GODOT_VERSION" echo "" +echo " platform-assets $platform_assets_result" echo " godot-import $import_result" echo " gdformat $fmt_result" echo " gdlint $lint_result" @@ -415,6 +470,7 @@ echo "" echo "QUALITY RESULT: $($OVERALL && echo FAIL || echo PASS)" echo "Godot: $GODOT_VERSION" echo "" + echo " platform-assets $platform_assets_result" echo " godot-import $import_result" echo " gdformat $fmt_result" echo " gdlint $lint_result"