Author: Shivam Mathur (shivammathur)
Date: 2026-01-05T02:15:21+05:30
Commit:
https://github.com/php/web-php/commit/adeb7a1189b6a141e7def5ee0f4f169afef0f90a
Raw diff:
https://github.com/php/web-php/commit/adeb7a1189b6a141e7def5ee0f4f169afef0f90a.diff
Improve windows single line installer downloads script
Improve installation path selection
Add PHP to the path after install
Changed paths:
M include/download-instructions/windows.ps1
Diff:
diff --git a/include/download-instructions/windows.ps1
b/include/download-instructions/windows.ps1
index d6ac1bad52..a81aa16939 100644
--- a/include/download-instructions/windows.ps1
+++ b/include/download-instructions/windows.ps1
@@ -3,19 +3,27 @@
Downloads and sets up a specified PHP version on Windows.
.PARAMETER Version
-Major.minor or full version (e.g., 7.4 or 7.4.30).
-
-.PARAMETER Path
-Destination directory (defaults to C:\php<Version>).
+Major.minor or full version (e.g., 8.4 or 8.4.15).
.PARAMETER Arch
-Architecture: x64 or x86 (default: x64).
+x64 or x86 (default: x64).
.PARAMETER ThreadSafe
-ThreadSafe: download Thread Safe build (default: $False).
+Download Thread Safe build (default: $False).
.PARAMETER Timezone
date.timezone string for php.ini (default: 'UTC').
+
+.PARAMETER Scope
+Auto (default), CurrentUser, AllUsers, or Custom.
+- Auto: AllUsers if elevated, otherwise CurrentUser.
+- AllUsers: Requires elevation, installs under Program Files (or Program Files
(x86) for x86 arch).
+- CurrentUser: Installs under $env:LOCALAPPDATA.
+- Custom: Installs under -CustomPath (or prompts), adds to User PATH.
+
+.PARAMETER CustomPath
+Directory for Scope=Custom. Versions are installed under this directory and a
"current" link is created here.
+
#>
[CmdletBinding()]
@@ -23,15 +31,23 @@ param(
[Parameter(Mandatory = $true, Position=0)]
[ValidatePattern('^\d+(\.\d+)?(\.\d+)?((alpha|beta|RC)\d*)?$')]
[string]$Version,
+
[Parameter(Mandatory = $false, Position=1)]
- [string]$Path = "C:\php$Version",
- [Parameter(Mandatory = $false, Position=2)]
[ValidateSet("x64", "x86")]
[string]$Arch = "x64",
- [Parameter(Mandatory = $false, Position=3)]
+
+ [Parameter(Mandatory = $false, Position=2)]
[bool]$ThreadSafe = $False,
- [Parameter(Mandatory = $false, Position=4)]
- [string]$Timezone = 'UTC'
+
+ [Parameter(Mandatory = $false, Position=3)]
+ [string]$Timezone = 'UTC',
+
+ [Parameter(Mandatory = $false)]
+ [ValidateSet('Auto', 'CurrentUser', 'AllUsers', 'Custom')]
+ [string]$Scope = 'Auto',
+
+ [Parameter(Mandatory = $false)]
+ [string]$CustomPath
)
Function Get-File {
@@ -52,19 +68,20 @@ Function Get-File {
for ($i = 0; $i -lt $Retries; $i++) {
try {
if($OutFile -ne '') {
- Invoke-WebRequest -Uri $Url -OutFile $OutFile -TimeoutSec
$TimeoutSec
+ Invoke-WebRequest -Uri $Url -OutFile $OutFile -TimeoutSec
$TimeoutSec -UseBasicParsing -ErrorAction Stop
+ return
} else {
- Invoke-WebRequest -Uri $Url -TimeoutSec $TimeoutSec
+ return Invoke-WebRequest -Uri $Url -TimeoutSec $TimeoutSec
-UseBasicParsing -ErrorAction Stop
}
- break;
} catch {
if ($i -eq ($Retries - 1)) {
if($FallbackUrl) {
try {
if($OutFile -ne '') {
- Invoke-WebRequest -Uri $FallbackUrl -OutFile
$OutFile -TimeoutSec $TimeoutSec
+ Invoke-WebRequest -Uri $FallbackUrl -OutFile
$OutFile -TimeoutSec $TimeoutSec -UseBasicParsing -ErrorAction Stop
+ return
} else {
- Invoke-WebRequest -Uri $FallbackUrl -TimeoutSec
$TimeoutSec
+ return Invoke-WebRequest -Uri $FallbackUrl
-TimeoutSec $TimeoutSec -UseBasicParsing -ErrorAction Stop
}
} catch {
throw "Failed to download the file from $Url and
$FallbackUrl"
@@ -77,6 +94,87 @@ Function Get-File {
}
}
+Function Test-IsAdmin {
+ $p = New-Object
Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
+ return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+Function ConvertTo-BoolOrDefault {
+ param([string]$UserInput, [bool]$Default)
+ if ([string]::IsNullOrWhiteSpace($UserInput)) { return $Default }
+ switch -Regex ($UserInput.Trim().ToLowerInvariant()) {
+ '^(1|true|t|y|yes)$' { return $true }
+ '^(0|false|f|n|no)$' { return $false }
+ default { return $Default }
+ }
+}
+
+Function Edit-PathForCompare([string]$p) {
+ if ([string]::IsNullOrWhiteSpace($p)) { return '' }
+ return ($p.Trim().Trim('"').TrimEnd('\')).ToLowerInvariant()
+}
+
+Function Set-PathEntryFirst {
+ param(
+ [Parameter(Mandatory = $true)][ValidateSet('User','Machine')]
[string]$Target,
+ [Parameter(Mandatory = $true)][string]$Entry
+ )
+
+ $entryNorm = Edit-PathForCompare $Entry
+
+ $existing = [Environment]::GetEnvironmentVariable('Path', $Target)
+ if ($null -eq $existing) { $existing = '' }
+
+ $parts = @()
+ foreach ($p in ($existing -split ';')) {
+ if (-not [string]::IsNullOrWhiteSpace($p)) {
+ if ((Edit-PathForCompare $p) -ne $entryNorm) { $parts += $p }
+ }
+ }
+ $newParts = @($Entry) + $parts
+ [Environment]::SetEnvironmentVariable('Path', ($newParts -join ';'),
$Target)
+
+ $procParts = @()
+ foreach ($p in ($env:Path -split ';')) {
+ if (-not [string]::IsNullOrWhiteSpace($p)) {
+ if ((Edit-PathForCompare $p) -ne $entryNorm) { $procParts += $p }
+ }
+ }
+ $env:Path = ((@($Entry) + $procParts) -join ';')
+}
+
+function Send-EnvironmentChangeBroadcast {
+ try {
+ $sig = @'
+using System;
+using System.Runtime.InteropServices;
+public static class NativeMethods {
+ [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
+ public static extern IntPtr SendMessageTimeout(
+ IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,
+ uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
+}
+'@
+ Add-Type -TypeDefinition $sig -ErrorAction SilentlyContinue | Out-Null
+ $HWND_BROADCAST = [IntPtr]0xffff
+ $WM_SETTINGCHANGE = 0x001A
+ $SMTO_ABORTIFHUNG = 0x0002
+ [UIntPtr]$result = [UIntPtr]::Zero
+ [NativeMethods]::SendMessageTimeout($HWND_BROADCAST,
$WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000,
[ref]$result) | Out-Null
+ } catch { }
+}
+
+Function Test-EmptyDir([string]$Dir) {
+ if (-not (Test-Path -LiteralPath $Dir)) {
+ New-Item -ItemType Directory -Path $Dir -Force | Out-Null
+ return
+ }
+ $items = Get-ChildItem -LiteralPath $Dir -Force -ErrorAction
SilentlyContinue
+ if ($items -and $items.Count -gt 0) {
+ throw "The directory '$Dir' is not empty. Please choose another
location."
+ }
+}
+
Function Get-Semver {
[CmdletBinding()]
param(
@@ -85,19 +183,22 @@ Function Get-Semver {
[ValidatePattern('^\d+\.\d+$')]
[string]$Version
)
- $releases = Get-File -Url
"https://downloads.php.net/~windows/releases/releases.json" | ConvertFrom-Json
+
+ $jsonUrl = "https://downloads.php.net/~windows/releases/releases.json"
+ $releases = ((Get-File -Url $jsonUrl).Content | ConvertFrom-Json)
+
$semver = $releases.$Version.version
- if($null -eq $semver) {
- $semver = (Get-File -Url
"https://downloads.php.net/~windows/releases/archives").Links |
- Where-Object { $_.href -match "php-($Version.[0-9]+).*" } |
- ForEach-Object { $matches[1] } |
- Sort-Object { [System.Version]$_ } -Descending |
- Select-Object -First 1
- }
- if($null -eq $semver) {
- throw "Unsupported PHP version: $Version"
- }
- return $semver
+ if ($null -ne $semver) { return [string]$semver }
+
+ $html = (Get-File -Url
"https://downloads.php.net/~windows/releases/archives/").Content
+ $rx = [regex]"php-($([regex]::Escape($Version))\.[0-9]+)"
+ $found = $rx.Matches($html) | ForEach-Object { $_.Groups[1].Value } |
+ Sort-Object { [version]$_ } -Descending |
+ Select-Object -First 1
+
+ if ($null -ne $found) { return [string]$found }
+
+ throw "Unsupported PHP version series: $Version"
}
Function Get-VSVersion {
@@ -160,7 +261,7 @@ Function Get-PhpFromUrl {
$vs = Get-VSVersion $Version
$ts = if ($ThreadSafe) { "ts" } else { "nts" }
$zipName = if ($ThreadSafe) { "php-$Semver-Win32-$vs-$Arch.zip" } else {
"php-$Semver-$ts-Win32-$vs-$Arch.zip" }
- $type = Get-ReleaseType $Version
+ $type = Get-ReleaseType $Semver
$base = "https://downloads.php.net/~windows/$type"
try {
@@ -175,49 +276,147 @@ Function Get-PhpFromUrl {
}
$tempFile = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.zip')
+
try {
+ $isAdmin = Test-IsAdmin
+
+ if (-not $PSBoundParameters.ContainsKey('Arch')) {
+ Write-Host ""
+ Write-Host "What architecture would you like to install?"
+ Write-Host "Enter x64 for 64-bit"
+ Write-Host "Enter x86 for 32-bit"
+ Write-Host "Press Enter to use default ($Arch)"
+ $archSel = Read-Host "Please enter [x64/x86]"
+ if (-not [string]::IsNullOrWhiteSpace($archSel) -and @('x64','x86')
-contains $archSel.Trim()) {
+ $Arch = $archSel.Trim()
+ }
+ }
+
+ if (-not $PSBoundParameters.ContainsKey('ThreadSafe')) {
+ Write-Host ""
+ Write-Host "What ThreadSafe option would you like to use?"
+ Write-Host "Enter true for ThreadSafe"
+ Write-Host "Enter false for Non-ThreadSafe"
+ Write-Host "Press Enter to use default ($ThreadSafe)"
+ $tsSel = Read-Host "Please enter [true/false]"
+ $ThreadSafe = ConvertTo-BoolOrDefault -UserInput $tsSel -Default
$ThreadSafe
+ }
+
+ if (-not $PSBoundParameters.ContainsKey('Timezone')) {
+ Write-Host ""
+ Write-Host "What timezone would you like to set in php.ini?"
+ Write-Host "Press Enter to use default ($Timezone)"
+ $tzSel = Read-Host "Please enter timezone"
+ if (-not [string]::IsNullOrWhiteSpace($tzSel)) {
+ $Timezone = $tzSel.Trim()
+ }
+ }
+
+ if (-not $PSBoundParameters.ContainsKey('Scope')) {
+ Write-Host ""
+ Write-Host "Would you like to install PHP for:"
+ Write-Host "Enter 1 for Current user"
+ Write-Host "Enter 2 for All users (requires admin elevation)"
+ Write-Host "Enter 3 to install PHP at a custom path"
+ Write-Host "Press Enter to choose automatically"
+ $sel = Read-Host "Please enter [1-3]"
+ switch ($sel) {
+ '1' { $Scope = 'CurrentUser' }
+ '2' { $Scope = 'AllUsers' }
+ '3' { $Scope = 'Custom' }
+ default { $Scope = 'Auto' }
+ }
+ }
+
+ if ($Scope -eq 'Custom' -and -not
$PSBoundParameters.ContainsKey('CustomPath')) {
+ $defaultCustom = if ($CustomPath) { $CustomPath } else { (Join-Path
$env:LOCALAPPDATA 'Programs\PHP') }
+ Write-Host ""
+ Write-Host "Please enter the custom installation path."
+ Write-Host "Press Enter to use default ($defaultCustom)"
+ $cr = Read-Host "Please enter"
+ $CustomPath = if ([string]::IsNullOrWhiteSpace($cr)) { $defaultCustom
} else { $cr.Trim() }
+ }
+
if ($Version -match "^\d+\.\d+$") {
$Semver = Get-Semver $Version
+ $MajorMinor = $Version
} else {
$Semver = $Version
- $Semver -match '^(\d+\.\d+)' | Out-Null
- $Version = $Matches[1]
+ if ($Semver -notmatch '^(\d+\.\d+)') { throw "Could not derive
major.minor from Version '$Version'." }
+ $MajorMinor = $Matches[1]
}
- if (-not (Test-Path $Path)) {
- try {
- New-Item -ItemType Directory -Path $Path -ErrorAction Stop |
Out-Null
- } catch {
- throw "Failed to create directory $Path. $_"
+ if ([version]$MajorMinor -lt [version]'5.5' -and $Arch -eq 'x64') {
+ $Arch = 'x86'
+ Write-Host "PHP series $MajorMinor does not support x64 on Windows.
Using x86."
+ }
+
+ $EffectiveScope = $Scope
+ if ($Scope -eq 'Auto') {
+ $EffectiveScope = if ($isAdmin) { 'AllUsers' } else { 'CurrentUser' }
+ }
+
+ if ($EffectiveScope -eq 'AllUsers' -and -not $isAdmin) {
+ throw "AllUsers install selected but this session is not elevated.
Re-run as Administrator or choose CurrentUser/Custom."
+ }
+
+ $installRootDirectory = switch ($EffectiveScope) {
+ 'CurrentUser' { Join-Path $env:LOCALAPPDATA 'Programs\PHP' }
+ 'AllUsers' {
+ $pf = $env:ProgramFiles
+ if ($Arch -eq 'x86' -and ${env:ProgramFiles(x86)}) { $pf =
${env:ProgramFiles(x86)} }
+ Join-Path $pf 'PHP'
}
- } else {
- $files = Get-ChildItem -Path $Path
- if ($files.Count -gt 0) {
- throw "The directory $Path is not empty. Please provide an empty
directory."
+ 'Custom' {
+ if ([string]::IsNullOrWhiteSpace($CustomPath)) { throw
"Scope=Custom requires -CustomPath (or interactive input)." }
+ [Environment]::ExpandEnvironmentVariables($CustomPath)
}
+ default { throw "Unexpected scope: $EffectiveScope" }
}
- if($Version -lt '5.5' -and $Arch -eq 'x64') {
- $Arch = 'x86'
- Write-Host "PHP version $Version does not support x64 architecture on
Windows. Using x86 instead."
+ if (-not (Test-Path -LiteralPath $installRootDirectory)) {
+ New-Item -ItemType Directory -Path $installRootDirectory | Out-Null
}
- Write-Host "Downloading PHP $Semver to $Path"
- Get-PhpFromUrl $Version $Semver $Arch $ThreadSafe $tempFile
- Expand-Archive -Path $tempFile -DestinationPath $Path -Force -ErrorAction
Stop
+ $tsTag = if ($ThreadSafe) { 'ts' } else { 'nts' }
+ $installDirectory = Join-Path (Join-Path (Join-Path $installRootDirectory
$Semver) $tsTag) $Arch
+ $currentLink = Join-Path $installRootDirectory 'current'
+
+ Test-EmptyDir $installDirectory
+
+ Write-Host "Downloading PHP $Semver ($Arch, $tsTag) -> $installDirectory"
+ Get-PhpFromUrl $MajorMinor $Semver $Arch $ThreadSafe $tempFile
+
+ Expand-Archive -Path $tempFile -DestinationPath $installDirectory -Force
-ErrorAction Stop
- $phpIniProd = Join-Path $Path "php.ini-production"
+ $phpIniProd = Join-Path $installDirectory "php.ini-production"
if(-not(Test-Path $phpIniProd)) {
- $phpIniProd = Join-Path $Path "php.ini-recommended"
+ $phpIniProd = Join-Path $installDirectory "php.ini-recommended"
}
- $phpIni = Join-Path $Path "php.ini"
- Copy-Item $phpIniProd $phpIni -Force
- $extensionDir = Join-Path $Path "ext"
- (Get-Content $phpIni) -replace '^extension_dir = "./"', "extension_dir =
`"$extensionDir`"" | Set-Content $phpIni
- (Get-Content $phpIni) -replace ';\s?extension_dir = "ext"', "extension_dir
= `"$extensionDir`"" | Set-Content $phpIni
- (Get-Content $phpIni) -replace ';\s?date.timezone =', "date.timezone =
`"$Timezone`"" | Set-Content $phpIni
+ $phpIni = Join-Path $installDirectory "php.ini"
+ if (Test-Path $phpIniProd) {
+ Copy-Item $phpIniProd $phpIni -Force
+
+ $extensionDir = Join-Path $installDirectory "ext"
+ (Get-Content $phpIni) -replace '^extension_dir = "./"', "extension_dir
= `"$extensionDir`"" | Set-Content $phpIni
+ (Get-Content $phpIni) -replace ';\s?extension_dir = "ext"',
"extension_dir = `"$extensionDir`"" | Set-Content $phpIni
+ (Get-Content $phpIni) -replace ';\s?date.timezone =', "date.timezone =
`"$Timezone`"" | Set-Content $phpIni
+ }
+
+ if (Test-Path -LiteralPath $currentLink) {
+ Remove-Item -LiteralPath $currentLink -Force -Recurse
+ }
+ New-Item -ItemType Junction -Path $currentLink -Target $installDirectory |
Out-Null
+
+ $pathTarget = if ($EffectiveScope -eq 'AllUsers') { 'Machine' } else {
'User' }
+ Set-PathEntryFirst -Target $pathTarget -Entry $currentLink
+ Send-EnvironmentChangeBroadcast
- Write-Host "PHP $Semver downloaded to $Path"
+ Write-Host ""
+ Write-Host "Installed PHP ${Semver}: $installDirectory"
+ Write-Host "It has been linked to $currentLink and added to PATH."
+ Write-Host "Please restart any open Command Prompt/PowerShell windows or
IDEs to pick up the new PATH."
+ Write-Host "You can run 'php -v' to verify the installation in the new
window."
} catch {
Write-Error $_
Exit 1