Fix Windows installer: handle per-user Python, make pywin32 postinstall non-fatal

The LocalSystem service account cannot access per-user Python installs
(AppData\Local\Programs\Python\...). When a per-user install is detected,
automatically install Python system-wide before proceeding.

- Detect per-user Python (AppData in path) and trigger system-wide install
- Extract system Python install logic into Install-PythonSystemWide function
- Check winget exit code before marking install successful
- Split pip install and postinstall into separate steps; pip failure is fatal,
  postinstall failure is a warning (service DLLs may already be registered)
- Use $LASTEXITCODE check on pip rather than try/catch (external process)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:27:23 +00:00
parent b19ce85d84
commit 8c962b88f4
+72 -56
View File
@@ -44,7 +44,7 @@ Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan
$pythonPath = $null
# Search for a system-wide Python first (accessible by LocalSystem)
# System-wide paths — accessible by LocalSystem service account
$systemPaths = @(
"C:\Program Files\Python313\python.exe",
"C:\Program Files\Python312\python.exe",
@@ -56,6 +56,49 @@ $systemPaths = @(
"C:\Python311\python.exe",
"C:\Python310\python.exe"
)
function Install-PythonSystemWide {
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget (system-wide)..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { $wingetOk = $true; Write-Host " done." -ForegroundColor Green }
} catch {}
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 explicitly supports Win 8.1+
Write-Host " Downloading Python 3.11 (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Error "Could not download Python. Install from https://python.org choosing 'Install for all users', then re-run."
}
Write-Host " Installing system-wide (silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# Refresh PATH after install
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
}
# ── Search for system-wide Python first ───────────────────────────────────────
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
@@ -65,7 +108,7 @@ foreach ($p in $systemPaths) {
}
}
# Fall back to PATH
# ── Fall back to PATH — but flag if it's per-user ─────────────────────────────
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3", "py")) {
try {
@@ -78,64 +121,31 @@ if (-not $pythonPath) {
}
}
if (-not $pythonPath) {
# ── If Python is per-user (AppData), install system-wide so LocalSystem can use it ──
$needsSystemPython = $false
if ($pythonPath -and ($pythonPath -match "AppData")) {
Write-Host " Found per-user Python: $pythonPath" -ForegroundColor Yellow
Write-Host " LocalSystem service needs system-wide Python. Installing..." -ForegroundColor Yellow
$needsSystemPython = $true
} elseif (-not $pythonPath) {
Write-Host " Python 3 not found. Installing system-wide..." -ForegroundColor Yellow
$needsSystemPython = $true
}
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
$wingetOk = $true
Write-Host " done." -ForegroundColor Green
} catch {}
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 is explicitly documented to support Win 8.1+
Write-Host " Downloading Python 3.11 installer (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Error "Could not download Python installer. Install Python 3.11+ from https://python.org (choose 'Install for all users') then re-run."
}
Write-Host " Running Python installer (system-wide, silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited with code $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# Refresh PATH and locate installed Python
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
if ($needsSystemPython) {
Install-PythonSystemWide
# Locate the newly installed system-wide Python
$pythonPath = $null
foreach ($p in $systemPaths) {
if (Test-Path $p) { $pythonPath = $p; break }
}
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3")) {
if (Test-Path $p) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
if (-not $pythonPath) {
Write-Error "Python 3 still not found after installation. Open a new Admin PowerShell and re-run this installer."
Write-Error "System-wide Python not found after install. Open a new Admin PowerShell and re-run."
}
}
@@ -143,13 +153,19 @@ Write-Host " Python: $pythonPath" -ForegroundColor Green
# ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
# pip install
$pipResult = & $pythonPath -m pip install --upgrade pywin32 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "pip install pywin32 failed (exit $LASTEXITCODE).`n$pipResult`nTry manually: $pythonPath -m pip install pywin32"
}
# postinstall registers service runner DLLs — non-fatal if it fails
try {
& $pythonPath -m pip install --quiet --upgrade pywin32
# Run postinstall to register service runner DLLs system-wide
& $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>$null
$postResult = & $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>&1
Write-Host " pywin32 installed." -ForegroundColor Green
} catch {
Write-Error "pip install pywin32 failed: $_`nEnsure pip is available: $pythonPath -m ensurepip"
Write-Host " pywin32 installed (postinstall skipped — service should still work)." -ForegroundColor Yellow
}
# ── Create install directory and download agent ────────────────────────────────