2424
2525$ErrorActionPreference = " Stop"
2626
27- $Script :InstallerVersion = " 1.5 .0"
27+ $Script :InstallerVersion = " 1.8 .0"
2828
2929# Force UTF-8 console output so winget's progress glyphs and other tool output
3030# are not rendered as mojibake in legacy code-page consoles.
@@ -162,19 +162,6 @@ function New-ManagedVenv($PythonInfo) {
162162 }
163163
164164 $venvPython = Join-Path $VenvPath " Scripts\python.exe"
165- if ((Test-Path - LiteralPath $venvPython ) -and (Test-IsUnsupportedWindowsArm64Python $venvPython @ ())) {
166- Invoke-Step " Removing native ARM64 managed venv: $VenvPath " {
167- Remove-Item - LiteralPath $VenvPath - Recurse - Force
168- }
169- Invoke-Step " Creating managed X-Tester venv: $VenvPath " {
170- $venvArgs = @ ()
171- if ($PythonInfo.Arguments ) { $venvArgs += $PythonInfo.Arguments }
172- $venvArgs += @ (" -m" , " venv" , $VenvPath )
173- Invoke-External $PythonInfo.FilePath $venvArgs
174- }
175- $venvPython = Join-Path $VenvPath " Scripts\python.exe"
176- }
177-
178165 if (-not (Test-Path - LiteralPath $venvPython )) {
179166 # Microsoft Store / UWP Python redirects writes under %LOCALAPPDATA% into
180167 # %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.*\LocalCache\Local\<rest>.
@@ -369,22 +356,34 @@ function Expand-Clients {
369356
370357function Test-PythonExecutable ([string ]$FilePath , [string []]$Arguments ) {
371358 if (-not $FilePath ) { return $null }
359+ $label = if ($Arguments -and $Arguments.Count -gt 0 ) { " $FilePath $ ( $Arguments -join ' ' ) " } else { " $FilePath " }
372360 try {
373361 $output = & $FilePath @Arguments -- version 2>&1
374362 }
375363 catch {
364+ Warn " Skipping ${label} : launcher threw $ ( $_.Exception.Message ) "
365+ return $null
366+ }
367+ if ($LASTEXITCODE -ne 0 ) {
368+ Warn " Skipping ${label} : '--version' exited with code $LASTEXITCODE "
376369 return $null
377370 }
378- if ($LASTEXITCODE -ne 0 ) { return $null }
379371 $text = ($output | Out-String ).Trim()
380372 if ([string ]::IsNullOrWhiteSpace($text )) { return $null }
381- if ($text -notmatch " Python\s+(\d+)\.(\d+)\.(\d+)" ) { return $null }
373+ if ($text -notmatch " Python\s+(\d+)\.(\d+)\.(\d+)" ) {
374+ Warn " Skipping ${label} : unrecognised version output '$text '"
375+ return $null
376+ }
382377 $major = [int ]$Matches [1 ]
383378 $minor = [int ]$Matches [2 ]
384- if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 11 )) { return $null }
379+ if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 11 )) {
380+ Warn " Skipping ${label} : $text is older than Python 3.11"
381+ return $null
382+ }
385383
386384 if (Test-IsUnsupportedWindowsArm64Python $FilePath $Arguments ) {
387- return $null
385+ Warn " ${label} : native ARM64 Python detected. Modern wheels for cryptography/keyring/artifacts-keyring exist on win_arm64; if pip later falls back to a source build, re-run with x64 Python 3.12."
386+ # Fall through and accept the candidate.
388387 }
389388
390389 # Reject Microsoft Store Python distributions. They virtualize writes under
@@ -395,6 +394,7 @@ function Test-PythonExecutable([string]$FilePath, [string[]]$Arguments) {
395394 if ($LASTEXITCODE -eq 0 ) {
396395 $sysExeText = ($sysExe | Out-String ).Trim()
397396 if ($sysExeText -match " \\WindowsApps\\" -or $sysExeText -match " \\Packages\\PythonSoftwareFoundation\.Python\." ) {
397+ Warn " Skipping ${label} : Microsoft Store Python at '$sysExeText ' redirects venv writes"
398398 return $null
399399 }
400400 }
@@ -407,6 +407,41 @@ function Test-PythonExecutable([string]$FilePath, [string[]]$Arguments) {
407407 }
408408}
409409
410+ function Get-RegistryPythonInstalls {
411+ # PEP 514: Python distributions register themselves under
412+ # HKCU/HKLM\Software\Python\<Company>\<Tag>\InstallPath
413+ # The (default) value of InstallPath is the install directory.
414+ $results = @ ()
415+ $roots = @ (
416+ " HKCU:\Software\Python" ,
417+ " HKLM:\Software\Python" ,
418+ " HKLM:\Software\WOW6432Node\Python"
419+ )
420+ foreach ($root in $roots ) {
421+ if (-not (Test-Path - LiteralPath $root )) { continue }
422+ $companies = Get-ChildItem - LiteralPath $root - ErrorAction SilentlyContinue
423+ foreach ($company in $companies ) {
424+ # Skip ContinuumAnalytics (Anaconda) etc; only PythonCore is the canonical CPython.
425+ if ($company.PSChildName -ne " PythonCore" ) { continue }
426+ $tags = Get-ChildItem - LiteralPath $company.PSPath - ErrorAction SilentlyContinue
427+ foreach ($tag in $tags ) {
428+ $installPathKey = Join-Path $tag.PSPath " InstallPath"
429+ if (-not (Test-Path - LiteralPath $installPathKey )) { continue }
430+ try {
431+ $props = Get-ItemProperty - LiteralPath $installPathKey - ErrorAction Stop
432+ $dir = $props ." (default)"
433+ if (-not $dir ) { $dir = $props ." ExecutablePath" | Split-Path - Parent - ErrorAction SilentlyContinue }
434+ if ($dir -and (Test-Path - LiteralPath $dir )) {
435+ $exe = Join-Path $dir " python.exe"
436+ if (Test-Path - LiteralPath $exe ) { $results += $exe }
437+ }
438+ } catch { }
439+ }
440+ }
441+ }
442+ return ($results | Select-Object - Unique)
443+ }
444+
410445function Find-PythonCandidate {
411446 $candidates = @ ()
412447 # Skip the Microsoft Store WindowsApps stub which prints a help message and exits 9009.
@@ -434,6 +469,12 @@ function Find-PythonCandidate {
434469 }
435470 }
436471
472+ # PEP 514 registry-based discovery is the most reliable source on Windows;
473+ # it works even when winget installed Python but did not refresh session PATH.
474+ foreach ($exe in (Get-RegistryPythonInstalls )) {
475+ $candidates += , @ ($exe , @ ())
476+ }
477+
437478 # Common install locations for python.org / winget Python.Python.3.x packages.
438479 $installRoots = @ (
439480 (Join-Path $env: LOCALAPPDATA " Programs\Python" ),
@@ -445,7 +486,7 @@ function Find-PythonCandidate {
445486 if (-not $root ) { continue }
446487 if (-not (Test-Path - LiteralPath $root )) { continue }
447488 $found = Get-ChildItem - LiteralPath $root - Directory - ErrorAction SilentlyContinue |
448- Where-Object { $_.Name -match " ^Python3(1[1-9]|[2-9][0-9])$" } |
489+ Where-Object { $_.Name -match " ^Python3(1[1-9]|[2-9][0-9])(x64|-x64)? $" } |
449490 Sort-Object Name - Descending
450491 foreach ($dir in $found ) {
451492 $exe = Join-Path $dir.FullName " python.exe"
@@ -476,7 +517,7 @@ function Find-PythonCandidate {
476517
477518 if ($candidates.Count -eq 0 ) {
478519 Warn " No Python candidates were discovered."
479- Info " Scanned: PATH (python, python3), py.exe launcher, %LOCALAPPDATA%\Programs\Python, %ProgramFiles%\Python, %ProgramFiles%, %ProgramFiles(x86)%, %LOCALAPPDATA%\Microsoft\WinGet\Packages\Python.Python.3.*, %LOCALAPPDATA%\Microsoft\WinGet\Links."
520+ Info " Scanned: PATH (python, python3), py.exe launcher, HKCU/HKLM\Software\Python\PythonCore registry, %LOCALAPPDATA%\Programs\Python, %ProgramFiles%\Python, %ProgramFiles%, %ProgramFiles(x86)%, %LOCALAPPDATA%\Microsoft\WinGet\Packages\Python.Python.3.*, %LOCALAPPDATA%\Microsoft\WinGet\Links."
480521 }
481522 foreach ($pair in $candidates ) {
482523 $result = Test-PythonExecutable $pair [0 ] $pair [1 ]
@@ -486,12 +527,16 @@ function Find-PythonCandidate {
486527}
487528
488529function Install-PythonViaWinget {
530+ param (
531+ [switch ]$ForceX64SideBySide
532+ )
489533 $winget = Get-Command " winget" - ErrorAction SilentlyContinue
490534 if (-not $winget ) {
491535 Warn " winget is not available; cannot auto-install Python."
492536 return $false
493537 }
494- Invoke-Step " Installing Python 3.12 via winget (Python.Python.3.12)" {
538+ $stepLabel = if ($ForceX64SideBySide ) { " Installing x64 Python 3.12 alongside existing ARM64 (Python.Python.3.12 --architecture x64 --force)" } else { " Installing Python 3.12 via winget (Python.Python.3.12)" }
539+ Invoke-Step $stepLabel {
495540 # Use --scope user so we do not require an elevation prompt; winget returns
496541 # non-zero for "already installed" too, so we tolerate any non-fatal exit.
497542 # --disable-interactivity suppresses winget's animated progress bar so the
@@ -501,6 +546,10 @@ function Install-PythonViaWinget {
501546 if (Test-IsWindowsArm64Host ) {
502547 $wingetArgs += @ (" --architecture" , " x64" )
503548 }
549+ if ($ForceX64SideBySide ) {
550+ $sideBySideRoot = Join-Path $env: LOCALAPPDATA " Programs\Python\Python312x64"
551+ $wingetArgs += @ (" --force" , " --location" , $sideBySideRoot )
552+ }
504553 $wingetArgs += @ (
505554 " --accept-source-agreements" , " --accept-package-agreements" , " --silent" ,
506555 " --disable-interactivity"
@@ -528,7 +577,7 @@ function Install-PythonViaWinget {
528577 $localProgramsPython = Join-Path $env: LOCALAPPDATA " Programs\Python"
529578 if (Test-Path - LiteralPath $localProgramsPython ) {
530579 Get-ChildItem - LiteralPath $localProgramsPython - Directory - ErrorAction SilentlyContinue |
531- Where-Object { $_.Name -match " ^Python3(1[1-9]|[2-9][0-9])$" } |
580+ Where-Object { $_.Name -match " ^Python3(1[1-9]|[2-9][0-9])(x64|-x64)? $" } |
532581 ForEach-Object {
533582 $extra += $_.FullName
534583 $extra += (Join-Path $_.FullName " Scripts" )
@@ -548,23 +597,26 @@ function Resolve-Python {
548597 if ($installed ) {
549598 $found = Find-PythonCandidate
550599 if ($found ) { return $found }
600+ # On ARM64 hosts a previously-installed native ARM64 Python.Python.3.12
601+ # makes winget skip the install with NO_APPLICABLE_UPGRADE_FOUND, so try
602+ # forcing a side-by-side x64 install before giving up.
603+ if (Test-IsWindowsArm64Host ) {
604+ Warn " Existing Python.Python.3.12 appears to be ARM64; forcing a side-by-side x64 install."
605+ $forced = Install-PythonViaWinget - ForceX64SideBySide
606+ if ($forced ) {
607+ $found = Find-PythonCandidate
608+ if ($found ) { return $found }
609+ }
610+ }
551611 Warn " Python was installed but is not yet visible to this session."
552612 Info " Open a new PowerShell window and re-run this installer."
553613 }
554614
555615 Fail " No working Python 3.11+ interpreter was found."
556- Info " Tried: python, python3, py -3.12, py -3.11, py -3.13, py -3.14, py -3 plus %LOCALAPPDATA%\Programs\Python, %ProgramFiles%\Python, %ProgramFiles%, %ProgramFiles(x86)%, %LOCALAPPDATA%\Microsoft\WinGet\Packages\Python.Python.3.*, %LOCALAPPDATA%\Microsoft\WinGet\Links."
616+ Info " Tried: python, python3, py -3.12, py -3.11, py -3.13, py -3.14, py -3, HKCU/HKLM\Software\Python\PythonCore registry, %LOCALAPPDATA%\Programs\Python, %ProgramFiles%\Python, %ProgramFiles%, %ProgramFiles(x86)%, %LOCALAPPDATA%\Microsoft\WinGet\Packages\Python.Python.3.*, %LOCALAPPDATA%\Microsoft\WinGet\Links."
557617 Info " The Microsoft Store stub at WindowsApps\python.exe is intentionally ignored."
558- if (Test-IsWindowsArm64Host ) {
559- Info " Native ARM64 Python is intentionally ignored because required auth-helper wheels can fall back to cryptography/OpenSSL source builds."
560- Info " Use x64 Python 3.12 on Windows ARM64 for this installer."
561- }
562618 Info " Install Python manually with one of:"
563- if (Test-IsWindowsArm64Host ) {
564- Info " winget install -e --id Python.Python.3.12 --architecture x64"
565- } else {
566- Info " winget install -e --id Python.Python.3.12"
567- }
619+ Info " winget install -e --id Python.Python.3.12"
568620 Info " choco install python --version=3.12"
569621 Info " https://www.python.org/downloads/"
570622 Info " Then open a new terminal and re-run this installer."
0 commit comments