private/PhpVersion.ps1
class PhpVersion : System.IComparable { <# The major.minor version of PHP #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $MajorMinorVersion <# The version of PHP, without the snapshot/alpha/beta/RC state #> [string] [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] $Version <# The version of PHP, possibly including the alpha/beta/RC state #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $FullVersion <# The unstability level, if any (snapshot, alpha, beta, RC) #> [string] [ValidateNotNull()] $UnstabilityLevel <# The unstability version, if any #> [Nullable[int]] $UnstabilityVersion <# A string used to display the details of the version #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $DisplayName <# A System.Version to be used to compare PHP versions #> [ValidateNotNull()] [System.Version] hidden $ComparableVersion <# The architecture, x86 or x64 #> [ValidateNotNull()] [ValidateSet('x86', 'x64')] [string] $Architecture <# Is this version thread-safe? #> [ValidateNotNull()] [bool] $ThreadSafe <# The version of the Visual C++ Redistributables required by this PHP version #> [ValidateNotNull()] [int] $VCVersion <# Initialize the instance. Keys for $data: - Version: required - UnstabilityLevel: optional - UnstabilityVersion: optional - Architecture: required - ThreadSafe: required - VCVersion: required #> hidden PhpVersion([hashtable] $data) { if ($data.ContainsKey('UnstabilityLevel') -and $null -ne $data.UnstabilityLevel) { $this.UnstabilityLevel = $data.UnstabilityLevel } else { $this.UnstabilityLevel = '' } if ($data.ContainsKey('UnstabilityVersion') -and $null -ne $data.UnstabilityVersion -and $data.UnstabilityVersion -ne '') { if ($this.UnstabilityLevel -eq '') { throw "UnstabilityVersion provided without UnstabilityLevel" } $this.UnstabilityVersion = [int] $data.UnstabilityVersion } elseif ($this.UnstabilityLevel -ne $Script:UNSTABLEPHP_SNAPSHOT) { $this.UnstabilityLevel = $null } $this.Version = $data.Version if ($data.Version -eq 'master') { $this.MajorMinorVersion = 'master' $this.FullVersion = 'master' $this.ComparableVersion = [Version]'99.99' } else { $dv = $data.Version $cv = $data.Version switch ($this.UnstabilityLevel) { '' { $cv += '.9999999' } $Script:UNSTABLEPHP_RELEASECANDIDATE_LC { $dv += $Script:UNSTABLEPHP_RELEASECANDIDATE_LC + $this.UnstabilityVersion $cv += '.' + (8000000 + $this.UnstabilityVersion) } $Script:UNSTABLEPHP_BETA { $dv += $Script:UNSTABLEPHP_BETA + $this.UnstabilityVersion $cv += '.' + (4000000 + $this.UnstabilityVersion) } $Script:UNSTABLEPHP_ALPHA { $dv += $Script:UNSTABLEPHP_ALPHA + $this.UnstabilityVersion $cv += '.' + (2000000 + $this.UnstabilityVersion) } $Script:UNSTABLEPHP_SNAPSHOT { $dv += '-dev' $cv += '.' + (1000000 + $this.UnstabilityVersion) } default { throw 'Unrecognized UnstabilityLevel' } } $cv = [System.Version] $cv $this.MajorMinorVersion = '{0}.{1}' -f $cv.Major,$cv.Minor $this.FullVersion = $dv $this.ComparableVersion = $cv } $this.Architecture = $data.Architecture $this.ThreadSafe = $data.ThreadSafe $this.VCVersion = $data.VCVersion $dn = 'PHP ' + $this.FullVersion + ' ' + $this.Architecture if ($this.Architecture -eq $Script:ARCHITECTURE_32BITS) { $dn += ' (32-bit)' } elseif ($this.Architecture -eq $Script:ARCHITECTURE_64BITS) { $dn += ' (64-bit)' } if ($this.ThreadSafe) { $dn += ' Thread-Safe' } else { $dn += ' Non-Thread-Safe' } $this.DisplayName = $dn } [int] CompareTo($that) { if (-Not($that -is [PhpVersion])) { throw "A PhpVersion instance can be compared only to another PhpVersion instance" } if ($this.ComparableVersion -lt $that.ComparableVersion) { $cmp = -1 } elseif ($this.ComparableVersion -gt $that.ComparableVersion) { $cmp = 1 } else { if ($this.Architecture -gt $that.Architecture) { $cmp = -1 } elseif ($this.Architecture -lt $that.Architecture) { $cmp = -1 } else { if ($this.ThreadSafe -and -Not $that.ThreadSafe) { $cmp = -1 } elseif ($that.ThreadSafe -and -Not $this.ThreadSafe) { $cmp = 1 } else { $cmp = 0 } } } return $cmp } } class PhpVersionDownloadable : PhpVersion { <# The state of the release #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $ReleaseState <# The URL where the PHP version ZIP archive can be downloaded from #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $DownloadUrl <# Initialize the instance. Keys for $data: the ones of PhpVersion plus: - ReleaseState: required - DownloadUrl: required #> hidden PhpVersionDownloadable([hashtable] $data) : base($data) { $this.ReleaseState = $data.ReleaseState $this.DownloadUrl = $data.DownloadUrl } } class PhpVersionInstalled : PhpVersion { <# The folder where PHP is installed (always set) #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $Folder <# The folder where PHP is installed (always set) - May be different from Folder if Folder is a junction #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] hidden $ActualFolder <# The full path of php.exe (always set) #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] $ExecutablePath <# The full path of php.ini (always set) #> [ValidateNotNull()] [ValidateLength(1, [int]::MaxValue)] [string] hidden $IniPath <# The default path where the PHP extensions are stored (empty string if not set) #> [ValidateNotNull()] [string] $ExtensionsPath <# The API version (eg 20190902 for PHP 7.4.0) #> [ValidateNotNull()] [ValidateRange(1, [int]::MaxValue)] [int] $ApiVersion <# Initialize the instance. Keys for $data: the ones of PhpVersion plus: - ApiVersion: required - ActualFolder: required - ExecutablePath: required - IniPath: required - ExtensionsPath: optional #> hidden PhpVersionInstalled([hashtable] $data) : base($data) { $this.ApiVersion = $data.ApiVersion $this.Folder = (Split-Path -LiteralPath $data.ExecutablePath).TrimEnd([System.IO.Path]::DirectorySeparatorChar) $this.ActualFolder = $data.ActualFolder.TrimEnd([System.IO.Path]::DirectorySeparatorChar) $this.ExecutablePath = $data.ExecutablePath $this.IniPath = $data.IniPath if ($data.ContainsKey('ExtensionsPath') -and $null -ne $data.ExtensionsPath) { $this.ExtensionsPath = $data.ExtensionsPath } else { $this.ExtensionsPath = '' } } [PhpVersionInstalled] static FromPath([string] $Path) { $directorySeparator = [System.IO.Path]::DirectorySeparatorChar $data = @{} $item = Get-Item -LiteralPath $Path if ($item -is [System.IO.FileInfo]) { if ($item.Extension -ne '.exe') { return [PhpVersionInstalled]::FromPath($item.DirectoryName) } $directory = $item.Directory $data.ExecutablePath = $item.FullName } elseif ($item -is [System.IO.DirectoryInfo]) { $directory = $item $data.ExecutablePath = Join-Path -Path $item.FullName -ChildPath 'php.exe' if (-Not(Test-Path -LiteralPath $data.ExecutablePath -PathType Leaf)) { throw "Unable to find php.exe in $Path" } } else { throw "Unrecognized PHP path: $Path" } $directoryPath = $directory.FullName.TrimEnd($directorySeparator) + $directorySeparator $actualDirectoryPath = $null if ($directory.Target) { $target = $null if ($directory.Target -is [string]) { $target = $directory.Target } elseif ($directory.Target.Count -gt 0) { $target = $directory.Target[0] } if ($target) { try { $actualDirectoryPath = (Get-Item -LiteralPath $directory.Target[0]).FullName.TrimEnd($directorySeparator) + $directorySeparator } catch { Write-Debug $_ } } } if (-Not($actualDirectoryPath)) { $actualDirectoryPath = $directoryPath } $LASTEXITCODE = 0 $data.ActualFolder = $actualDirectoryPath.TrimEnd($directorySeparator) $executableResult = & $data.ExecutablePath @('-n', '-r', (@' ob_start(); phpinfo(); $phpinfo = ob_get_contents(); ob_clean(); if (!preg_match('/^PHP Extension\s*=>\s*(\d+)$/m', $phpinfo, $matches)) { fwrite(STDERR, 'Failed to find the PHP Extension API'); exit(1); } echo PHP_VERSION, chr(9), PHP_INT_SIZE * 8, chr(9), $matches[1]; '@ -replace "`r`n", ' ' -replace "`n", ' ') ) if ($LASTEXITCODE -ne 0 -or -Not($executableResult)) { throw "Failed to execute php.exe: $LASTEXITCODE" } $match = $executableResult | Select-String -Pattern "(?<version>\d+\.\d+\.\d+)(?<stabilityLevel>([Rr][Cc]\d+)?-dev|(?:$Script:UNSTABLEPHP_RX))?(?<stabilityVersion>\d+)?\t(?<bits>\d+)\t(?<apiVersion>\d+)$" if (-not($match)) { throw "Unsupported PHP version: $executableResult" } $groups = $match.Matches[0].Groups $data.Version = $groups['version'].Value if ($groups['stabilityLevel'].Value -match '-dev$') { $data.UnstabilityLevel = $Script:UNSTABLEPHP_SNAPSHOT } else { $data.UnstabilityLevel = $groups['stabilityLevel'].Value $data.UnstabilityVersion = $groups['stabilityVersion'].Value } $data.Architecture = Get-Variable -Scope Script -ValueOnly -Name $('ARCHITECTURE_' + $groups['bits'].Value + 'BITS') $data.ApiVersion = [int] $groups['apiVersion'].Value $executableResult = & $data.ExecutablePath @('-i') $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Thread Safety\s*=>\s*(\w+)' $data.ThreadSafe = $match.Matches.Groups[1].Value -eq 'enabled' $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Compiler\s*=>\s*MSVC ?([\d]{1,2})' if ($null -ne $match) { $data.VCVersion = $match.Matches.Groups[1].Value } elseif ([System.Version]$data.Version -le [System.Version]'5.2.9999') { $data.VCVersion = 6 } else { $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Compiler\s*=>\s*Visual C\+\+\s+(\d{4})(?:\s|$)' if ($null -eq $match) { throw "Failed to recognize VCVersion" } $vcYear = $match.Matches.Groups[1].Value switch ($vcYear) { '2017' { $data.VCVersion = 15 } '2019' { $data.VCVersion = 16 } '2022' { $data.VCVersion = 17 } default { throw "Failed to recognize VCVersion from Visual C++ $vcYear" } } } $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Loaded Configuration File\s*=>\s*([\S].*[\S])\s*$' $data.IniPath = '' if ($match) { $data.IniPath = $match.Matches.Groups[1].Value if ($data.IniPath -eq '(none)') { $data.IniPath = '' } else { $data.IniPath = $data.IniPath -replace '/',$directorySeparator $data.IniPath = [System.IO.Path]::Combine($actualDirectoryPath, $data.IniPath) $data.IniPath = $data.IniPath -replace [regex]::Escape("$directorySeparator.$directorySeparator"),$directorySeparator } } if ($data.IniPath -eq '') { $data.IniPath = Join-Path -Path $actualDirectoryPath -ChildPath 'php.ini' } elseif ($directoryPath -ne $actualDirectoryPath -and $data.IniPath -imatch ('^' + [regex]::Escape($directoryPath) + '.+')) { $data.IniPath = $data.IniPath -ireplace ('^' + [regex]::Escape($directoryPath)),$actualDirectoryPath } $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*extension_dir\s*=>\s*([\S].*[\S])\s*=>' $data.ExtensionsPath = '' if ($match) { $data.ExtensionsPath = $match.Matches.Groups[1].Value if ($data.ExtensionsPath -eq '(none)') { $data.ExtensionsPath = '' } else { $data.ExtensionsPath = $data.ExtensionsPath -replace '/',$directorySeparator $data.ExtensionsPath = [System.IO.Path]::Combine($actualDirectoryPath, $data.ExtensionsPath) $data.ExtensionsPath = $data.ExtensionsPath -replace [regex]::Escape("$directorySeparator.$directorySeparator"),$directorySeparator } } return [PhpVersionInstalled]::new($data) } [PhpVersionInstalled[]] static FromEnvironment() { $result = @() $envPath = $env:Path if ($null -ne $envPath -and $envPath -ne '') { $donePaths = @{} $envPaths = $envPath -split [System.IO.Path]::PathSeparator foreach ($path in $envPaths) { if ($path -ne '') { $ep = Join-Path -Path $path -ChildPath 'php.exe' if (Test-Path -Path $ep -PathType Leaf) { $ep = (Get-Item -LiteralPath $ep).FullName $key = $ep.ToLowerInvariant() if (-Not($donePaths.ContainsKey($key))) { $donePaths[$key] = $true $result += [PhpVersionInstalled]::FromPath($ep) } } } } } return $result } [PhpVersionInstalled] static FromEnvironmentOne() { $found = [PhpVersionInstalled]::FromEnvironment() $numFound = $found.Count if ($numFound -eq 0) { throw "No PHP versions found in the current PATHs: use the -Path argument to specify the location of installed PHP" } if ($numFound -eq 1) { $resultIndex = 0 } else { $resultIndex = -1 Write-Host "Multiple PHP installations have been found.`nPlease specify the PHP installation you want:" while($resultIndex -eq -1) { for ($index = 0; $index -lt $numFound; $index++) { Write-Host "$($index + 1). $($found[$index].Folder)`n $($found[$index].DisplayName)" } $choice = Read-Host "x. Cancel`n`nYour choice (1... $numFound, or x)? " if ($choice -eq 'x') { throw 'Operation aborted.' } try { $resultIndex = [int]$choice - 1 if ($resultIndex -lt 0 -or $resultIndex -ge $numFound) { $resultIndex = -1 } } catch { $resultIndex = -1 } if ($resultIndex -eq -1) { Write-Host "`nPlease enter a number between 0 and $numFound, or 'x' to abort.`n" } } } return $found[$resultIndex] } } |