<# .SYNOPSIS Pick a Python interpreter that actually works, for the CI jobs (and for local use). Prints diagnostics with Write-Host and emits ONLY the chosen interpreter path on the success stream, so a caller can capture it: $py = & './scripts/ci_find_python.ps1' -NeedPreCommit -WriteEnv .DESCRIPTION Why this file exists (2026-10-11 CI incident): The DTU_* env-var refactor moved the interpreter probe inline into .gitea/workflows/ci.yml and narrowed it to two candidates (PATH python + repo .venv). On the Windows runner BOTH are dead ends: * PATH python for the SYSTEM account is C:\msys64\ucrt64\bin\python.exe, which has no pre-commit module; * the repo .venv is gitignored, so it does not exist in a fresh runner workspace at all. The interpreter that does work (a machine-wide install such as C:\ProgramData\anaconda3\python.exe) was no longer probed, and no machine-level DTU_PRECOMMIT_PYTHON had ever been set -> every static-gate job died in "environment check" before running a single hook. So discovery lives here now: one implementation, shared by both CI jobs and runnable offline, and it keeps the well-known machine-wide install locations as fallbacks. Probe order (see the DTU_* local-tool-path plan under docs/ -- this file is kept pure ASCII on purpose: PS 5.1 decodes BOM-less files as CP936): 1. DTU_PRECOMMIT_PYTHON -- explicit override. If it is set but the file is missing, or it cannot satisfy the requirement, this script FAILS with the variable name and does NOT silently fall back. 2. python on PATH 3. \.venv\Scripts\python.exe 4. well-known machine-wide installs (Program Files / ProgramData / %LOCALAPPDATA%\Programs\Python), newest version first .PARAMETER NeedPreCommit Require the interpreter to expose the pre-commit module (static gates need it). Probed with `-s` (no user site-packages) on purpose: the runner service runs as SYSTEM, which cannot see a USER-scope site install at all, so only an interpreter that works without user-site is accepted. .PARAMETER WriteEnv Append PYTHON= to $env:GITHUB_ENV so later steps can use $env:PYTHON. Written as ASCII: Out-File's PS 5.1 default is UTF-16LE, which appends foreign-encoded lines to a file other steps also append to. .OUTPUTS The chosen python.exe path (single line, success stream). .NOTES Keep this file pure ASCII -- Windows PowerShell 5.1 decodes BOM-less files as CP936, and non-ASCII bytes break string quoting (same trap as ci.yml). Exit codes: 0 = found, 2 = no usable interpreter / bad DTU_* override. #> param( [switch]$NeedPreCommit, [switch]$WriteEnv ) # Native probes write to stderr; the runner injects $ErrorActionPreference # ='Stop', which would turn that into a terminating NativeCommandError and # abort the probe loop before the next candidate is tried. $ErrorActionPreference = 'Continue' function Test-Interpreter { param([string]$Exe, [bool]$RequirePreCommit) if ([string]::IsNullOrWhiteSpace($Exe)) { return $false } if (-not (Test-Path -LiteralPath $Exe -PathType Leaf)) { return $false } $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { if ($RequirePreCommit) { & $Exe -s -m pre_commit --version 2>&1 | Out-Null } else { & $Exe --version 2>&1 | Out-Null } return ($LASTEXITCODE -eq 0) } catch { return $false } finally { $ErrorActionPreference = $saved } } $repoRoot = Split-Path -Parent $PSScriptRoot $require = [bool]$NeedPreCommit $what = if ($require) { 'pre-commit' } else { 'a runnable interpreter' } # --- 1. explicit override: honour it or fail loudly ------------------------- $explicit = $env:DTU_PRECOMMIT_PYTHON if (-not [string]::IsNullOrWhiteSpace($explicit)) { $explicit = $explicit.Trim() if (-not (Test-Path -LiteralPath $explicit -PathType Leaf)) { Write-Host "::error::DTU_PRECOMMIT_PYTHON is set but does not exist: $explicit" Write-Host '::error::fix the variable (full path to a python.exe) or unset it to use auto-discovery' exit 2 } if (-not (Test-Interpreter -Exe $explicit -RequirePreCommit $require)) { Write-Host "::error::DTU_PRECOMMIT_PYTHON does not provide $what (probed without user-site, i.e. SYSTEM-visible): $explicit" Write-Host '::error::fix the variable, or unset it to use auto-discovery' exit 2 } Write-Host "using python (DTU_PRECOMMIT_PYTHON): $explicit" if ($WriteEnv -and $env:GITHUB_ENV) { "PYTHON=$explicit" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding ascii } Write-Output $explicit exit 0 } # --- 2..4 auto-discovery ---------------------------------------------------- $programFiles = $env:ProgramFiles $programFilesX86 = ${env:ProgramFiles(x86)} if ([string]::IsNullOrWhiteSpace($programFiles)) { $programFiles = 'C:\Program Files' } if ([string]::IsNullOrWhiteSpace($programFilesX86)) { $programFilesX86 = 'C:\Program Files (x86)' } $candidates = @() $onPath = Get-Command python -ErrorAction SilentlyContinue if ($onPath) { $candidates += $onPath.Source } $candidates += (Join-Path $repoRoot '.venv\Scripts\python.exe') # Newest first: a fresh install is the usual intent. foreach ($ver in @('Python313', 'Python312', 'Python311', 'Python310')) { $candidates += (Join-Path $programFiles "$ver\python.exe") $candidates += (Join-Path $programFilesX86 "$ver\python.exe") if ($env:LOCALAPPDATA) { $candidates += (Join-Path $env:LOCALAPPDATA "Programs\Python\$ver\python.exe") } } # Machine-wide distributions (these are what the runner actually had). if ($env:ProgramData) { foreach ($dist in @('anaconda3', 'miniconda3')) { $candidates += (Join-Path $env:ProgramData "$dist\python.exe") } } $candidates += 'C:\ProgramData\anaconda3\python.exe' $candidates += 'C:\ProgramData\miniconda3\python.exe' $tried = New-Object System.Collections.Generic.List[string] $chosen = $null foreach ($c in $candidates) { if ([string]::IsNullOrWhiteSpace($c)) { continue } if ($tried.Contains($c)) { continue } $tried.Add($c) | Out-Null if (Test-Interpreter -Exe $c -RequirePreCommit $require) { $chosen = $c; break } } if (-not $chosen) { Write-Host "::error::no Python providing $what found (tried `$DTU_PRECOMMIT_PYTHON`, PATH python, repo .venv, and the well-known machine-wide installs)" foreach ($t in $tried) { Write-Host " tried: $t" } Write-Host '::error::note: the runner service runs as SYSTEM, so a USER-scope `pip install --user pre-commit` is invisible to it' Write-Host '::error::fix: set a MACHINE-level DTU_PRECOMMIT_PYTHON (see scripts/ci_runner_bootstrap.ps1 -SetEnv), or install pre-commit into a machine-wide interpreter' exit 2 } Write-Host "using python (auto-discovered): $chosen" # Out-Host, NOT the pipeline: a native command's stdout joins the success # stream, which would pollute the single-line path the caller captures. & $chosen --version 2>&1 | Out-Host if ($require) { & $chosen -s -m pre_commit --version 2>&1 | Out-Host } if ($WriteEnv -and $env:GITHUB_ENV) { "PYTHON=$chosen" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding ascii } Write-Output $chosen exit 0