Skip to content

fix(powershell): probe python3 before selecting it in Get-Python3Command - #4151

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/ps-probe-python3
Open

fix(powershell): probe python3 before selecting it in Get-Python3Command#4151
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/ps-probe-python3

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

Get-Python3Command's first branch returns @('python3') on mere Get-Command presence, with no execution probe — while its own second and third branches do probe:

if (Get-Command python3 -ErrorAction SilentlyContinue) { return @('python3') }   # <-- no probe
if (Get-Command python  -ErrorAction SilentlyContinue) {
    $ver = & python --version 2>&1
    if ($ver -match 'Python 3') { return @('python') }
}

Its own docstring promises "a usable Python 3 executable".

On Windows, python3 almost always resolves to the Microsoft Store App Execution Alias stub, which Get-Command finds but which fails at runtime:

found=True
source=C:\Users\...\AppData\Local\Microsoft\WindowsApps\python3.exe
ver=[Python was not found; run without arguments to install from the Microsoft Store, ...]
LASTEXITCODE=9009
match=False

Note the existing probe already rejects it correctly — the first branch just never runs one.

Precedent in this repo

scripts/bash/common.sh documents this exact hazard by name and defends against it:

"on Windows python3 commonly resolves to the Microsoft Store App Execution Alias stub, which passes command -v but fails at runtime (exit 49), so an availability-gated elif would pick python3, swallow its failure, and never reach the fallback — leaving feature.json unreadable even though it is valid (issue #3304)."

Its _python3_command probes all three candidates. The PowerShell twin probes only the last two.

Second half of the same root cause

The existing probes use & python --version 2>&1. In Windows PowerShell, redirecting a native command's stderr into the success stream wraps each line in an ErrorRecord — so under the $ErrorActionPreference = 'Stop' that every caller sets, the probe raises a terminating NativeCommandError rather than simply failing the match. Reaching branch 2 therefore crashes outright when python is also a Store alias (the Windows 11 default). One function, one fix.

Reproduction — shimmed interpreters on PATH, against upstream/main

python3 = dead stub, python = working  ->  RESULT=[python3]          <-- picks the dead stub
python  = dead stub, nothing else      ->  THREW: RemoteException    <-- probe crashes

Callers (Resolve-TemplateContentsetup-plan.ps1, create-new-feature.ps1) then invoke the dead stub, whose stderr becomes a terminating error under 'Stop', aborting the script instead of falling through to a working interpreter.

With the fix:

python3 = dead stub, python = working  ->  RESULT=[python]
python  = dead stub, nothing else      ->  RESULT=[]        (clean $null, no throw)
python3 + python dead, py -3 working   ->  RESULT=[py -3]

Fix

Extract the probe into Test-Python3Command and apply it to all three branches. The probe saves and restores $ErrorActionPreference around the invocation so a candidate writing to stderr fails the match instead of throwing.

Match semantics are unchanged — still -match 'Python 3', exactly as branches 2 and 3 already did.

Verification

  • Fail-before / pass-after: 3 new-vs-baseline failures with common.ps1 reverted to upstream/main3 passed with the fix.
  • Tests are self-contained: they shim a fake Store stub (stderr + exit 9009) and a working interpreter onto PATH, so they do not depend on the runner actually having a Store alias. Gated on HAS_POWERSHELL.
  • Broader PowerShell surface (9 script/parity test modules): 80 passed, 96 skipped, plus one failure — test_setup_tasks_ps_core_template_resolved — which is pre-existing on clean main, fails identically with and without this change, and has an unrelated cause (JSONDecodeError: Invalid control character in emitted stdout). I am not claiming to fix it.
  • common.ps1 remains ASCII-only (verified byte-wise: 0 non-ASCII bytes), per this repo's .ps1 encoding rule and tests/test_ps1_encoding.py, which passes.
  • uvx ruff@0.15.0 check src tests → clean

No breaking change. A genuinely working python3 still passes the probe and is still selected first; only candidates that cannot actually run are now skipped — which is what the function already promised.


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

Get-Python3Command's first branch returned @('python3') on mere Get-Command
presence, with NO execution probe -- while its own second and third branches
(python, py -3) DO probe with --version and match 'Python 3'. Its docstring
promises "a usable Python 3 executable".

On Windows, python3 almost always resolves to the Microsoft Store App
Execution Alias stub, which Get-Command finds but which fails at runtime:

  found=True
  source=C:\...\AppData\Local\Microsoft\WindowsApps\python3.exe
  ver=[Python was not found; run without arguments to install from the
       Microsoft Store...]
  LASTEXITCODE=9009
  match=False

This is the same hazard scripts/bash/common.sh documents by name (issue
github#3304) and defends against: its _python3_command probes all three
candidates, not just the last two.

Second half of the same root cause: the existing probes use
`& python --version 2>&1`. In Windows PowerShell, redirecting a native
command's stderr into the success stream wraps each line in an ErrorRecord,
so under the `$ErrorActionPreference = 'Stop'` every caller sets, the probe
raised a terminating NativeCommandError instead of simply failing the match.

Verified against upstream/main with shimmed interpreters on PATH:

  python3 = dead stub, python = working  ->  RESULT=[python3]   (dead)
  python  = dead stub, nothing else      ->  THREW: RemoteException

With the fix:

  python3 = dead stub, python = working  ->  RESULT=[python]
  python  = dead stub, nothing else      ->  RESULT=[]
  python3 + python dead, py -3 working   ->  RESULT=[py -3]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner August 15, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant