ChocolateyGet.psm1
# ChocolateyGet is a PackageManagement provider. It does package management operations such as find, install, # uninstall packages from https://www.chocolatey.org. It essentially a wrapper around choco.exe. # Import the localized Data Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename ChocolateyGet.Resource.psd1 #region Local variable definitions # Define provider related variables $script:ProviderName = "ChocolateyGet" $script:PackageSourceName = "Chocolatey" $script:PackageSource = "https://www.chocolatey.org" $script:additionalArguments = "AdditionalArguments" $script:AllVersions = "AllVersions" # Define choco related variables $script:ChocoExeName = 'choco.exe' $script:ChocoExePath = $null $script:firstTime = $true # Utility variables $script:PackageRegex = "(?<name>[^\s]*)(\s*)(?<version>[^\s]*)" $script:PackageReportRegex="^[0-9]*(\s*)(packages installed)" $script:FastReferenceRegex = "(?<name>[^#]*)#(?<version>[^\s]*)" function IsCoreCLR { $PSVariable = Get-Variable -Name IsCoreCLR -ErrorAction Ignore; return ($PSVariable -and $PSVariable.Value) } # Check if this is nano server. [System.Runtime.Loader.AssemblyLoadContext] is only available on NanoServer try { [System.Runtime.Loader.AssemblyLoadContext] $script:isNanoServer = $true } catch { $script:isNanoServer = $false } $script:FindPackageId = 10 $script:InstallPackageId = 11 $script:UnInstallPackageId = 12 $script:InstalledPackageId = 15 $script:InstallChocoId = 16 #endregion #region Provider APIs Implementation # Mandatory function for the PackageManagement providers. It returns the name of your provider. function Get-PackageProviderName { return $script:ProviderName } # Mandatory function for the PackageManagement providers. It initializes your provider before performing any actions. function Initialize-Provider { Write-Debug ($LocalizedData.ProviderDebugMessage -f ('Initialize-Provider')) } # Defines PowerShell dynamic parameters so that a user can pass in parameters via OneGet to the provider function Get-DynamicOptions { param ( [Microsoft.PackageManagement.MetaProvider.PowerShell.OptionCategory] $category ) Write-Debug ($LocalizedData.ProviderDebugMessage -f ('Get-DynamicOptions')) switch($category) { Package { Write-Output -InputObject (New-DynamicOption -Category $category -Name $script:additionalArguments -ExpectedType String -IsRequired $false) } Install { Write-Output -InputObject (New-DynamicOption -Category $category -Name $script:additionalArguments -ExpectedType String -IsRequired $false) } } } # This function gets called during find-package, install-package, get-packagesource etc. # OneGet uses this method to identify which provider can handle the packages from a particular source location. function Resolve-PackageSource { Write-Debug ($LocalizedData.ProviderDebugMessage -f ('Resolve-PackageSource')) $isTrusted = $false $isRegistered = $false $isValidated = $true $location = $script:PackageSource foreach($Name in @($request.PackageSources)) { if($Name -eq $script:PackageSourceName) { write-debug ($LocalizedData.ProviderDebugMessage -f ('Resolve-PackageSources to $location')) New-PackageSource $Name $location $isTrusted $isRegistered $isValidated } } } # Finds packages by given name and version information. function Find-Package { param( [string] $Name, [string] $RequiredVersion, [string] $MinimumVersion, [string] $MaximumVersion ) Write-Debug ($LocalizedData.ProviderDebugMessage -f ('Find-Package')) $ValidationResult = Validate-VersionParameters -Name $Name ` -MinimumVersion $MinimumVersion ` -MaximumVersion $MaximumVersion ` -RequiredVersion $RequiredVersion ` -AllVersions:$request.Options.ContainsKey($script:AllVersions) if(-not $ValidationResult) { # Return now as the version validation failed already return } $force = Get-ForceProperty if(-not (Install-ChocoBinaries -Force $force)) { return } # For some reason, we have to convert it to array to make the following choco.exe cmd to work $additionalArgs = Get-AdditionalArguments $args = if($additionalArgs) {$additionalArgs.Split(' ')} $nameContainWildCard = $false $filterRequired = $false $options = $request.Options foreach( $o in $options.Keys ) { Write-Debug ( "$script:PackageSourceName - OPTION: {0} => {1}" -f ($o, $options[$o]) ) } if (-not $name) { # a user does not provide name, search the entire repo Write-Error ( $LocalizedData.SearchingEntireRepo) return } # a user specifies -Name $progress = 5 Write-Progress -Activity $LocalizedData.SearchingForPackage -PercentComplete $progress -Id $script:FindPackageId if(Test-WildcardPattern -Name $Name) { # name contains wildcard $nameContainWildCard = $true Write-Debug ("$script:ChocoExePath search $name $additionalArgs") $packages = & $script:ChocoExePath search $name $args } elseif((-not $requiredVersion) -and (-not $minimumVersion) -and (-not $maximumVersion)) { # a user does not provide version, return the latest version Write-Debug ("$script:ChocoExePath search $name $additionalArgs") $packages = & $script:ChocoExePath search $name $args #--exact } elseif($options.ContainsKey($script:AllVersions)) { # a user provides allversion Write-Debug ("$script:ChocoExePath search $name --allversions $additionalArgs") $packages = & $script:ChocoExePath search $name --allversions $args } else { # a user provides any of these: $requiredVersion, $minimumVersion, $maximumVersion. # as choco does not support version search, we will find all allversions first and # will perform filter later Write-Debug ("$script:ChocoExePath search $name --allversions $additionalArgs") $packages = & $script:ChocoExePath search $name --allversions $args $filterRequired = $true } foreach ($pkg in $packages) { $progress += 5 $progress= [System.Math]::Min(100, $progress) Write-Progress -Activity $LocalizedData.ProcessingPackage -PercentComplete $progress -Id $script:FindPackageId if($request.IsCanceled) { return } $Matches = $null if (($pkg -like "*Approved*") -and ($pkg -match $script:PackageRegex)) { Write-Debug ("Found a package '{0}'" -f $pkg) $pkgname = $Matches.name $pkgversion = $Matches.version if (-not (Test-Name -Name $name -PackageName $pkgname -NameContainsWildcard $nameContainWildCard)) { continue } if ($pkgname -and $pkgversion) { # filter on version if(-not $filterRequired -or (Test-Version ` -Version $pkgversion ` -RequiredVersion $requiredVersion ` -MinimumVersion $minimumVersion ` -MaximumVersion $maximumVersion )) { $swidObject = @{ FastPackageReference = $pkgname+"#" + $pkgversion; Name = $pkgname; Version = $pkgversion; versionScheme = "MultiPartNumeric"; Source = $script:PackageSource; } $sid = New-SoftwareIdentity @swidObject Write-Output -InputObject $sid } } } } Write-Progress -Activity $LocalizedData.Complete -PercentComplete 100 -Completed -Id $script:FindPackageId } # This function is called by OneGet while a user types Save-Package. function Download-Package { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $FastPackageReference, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $Location ) Write-Debug ($LocalizedData.ProviderDebugMessage -f ('Download-Package')) Write-Warning $LocalizedData.SavePackageNotSupported -f $script:ProviderName } # It is required to implement this function for the providers that support install-package. function Install-Package { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $fastPackageReference ) Write-Debug -Message ($LocalizedData.ProviderDebugMessage -f ('Install-Package')) Write-Debug -Message ($LocalizedData.FastPackageReference -f $fastPackageReference) $force = Get-ForceProperty # Check the source location if(-Not $fastPackageReference) { ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage ($LocalizedData.PathNotFound -f ($fastPackageReference)) ` -ErrorId "PathNotFound" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidArgument ` -ExceptionObject $fastPackageReference } $additionalArgs = Get-AdditionalArguments $additionalArgs = if($additionalArgs) {$additionalArgs.Split(' ')} $Matches = $null $isMatch = $fastPackageReference -match $script:FastReferenceRegex if (-not $isMatch) { write-error ($LocalizedData.FailToInstall -f $fastPackageReference) return } $name =$matches.name $version = $Matches.version if (-not ($name -and $version)) { write-error ($LocalizedData.FailToInstall -f $fastPackageReference) return } $shouldContinueQueryMessage = ($LocalizedData.InstallPackageQuery -f "Installing", $name) $shouldContinueCaption = $LocalizedData.InstallPackageCaption if(-not ($Force -or $request.ShouldContinue($shouldContinueQueryMessage, $shouldContinueCaption))) { Write-Warning ($LocalizedData.NotInstalled -f $fastPackageReference) return } if($force) { $installCmd=@("install", $name, "--version", $version, "-y", "-force") } else { $installCmd=@("install", $name, "--version", $version, "-y") } Write-debug ("Calling $installCmd $additionalArgs") $progress = 1 $job=Start-Job -ScriptBlock { & $args[0] $args[1] $args[2] } -ArgumentList @($script:ChocoExePath, $installCmd, $additionalArgs) Show-Progress -ProgressMessage $LocalizedData.InstallingPackage -PercentComplete $progress -ProgressId $script:InstallPackageId $packages= $job | Receive-Job -Wait Process-Package -Name $name ` -RequiredVersion $version ` -OperationMessage $LocalizedData.InstallingPackage ` -ProgressId $script:InstallPackageId ` -PercentComplete $progress ` -Packages $packages } # It is required to implement this function for the providers that support UnInstall-Package. function UnInstall-Package { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $FastPackageReference ) Write-Debug -Message ($LocalizedData.ProviderDebugMessage -f ('Uninstall-Package')) Write-Debug -Message ($LocalizedData.FastPackageReference -f $FastPackageReference) $force = Get-ForceProperty $Matches = $null $isMatch = $FastPackageReference -match $script:FastReferenceRegex if (-not $isMatch) { write-error ($LocalizedData.FailToInstall -f $fastPackageReference) return } $name =$matches.name $version = $Matches.version if (-not ($name -and $version)) { write-error ($LocalizedData.FailToInstall -f $fastPackageReference) return } # Choco will prompt to confirm whether it wants to uninstall dependencies # a user can pass in -y --remove-dependencies option to avoid hanging # only provides '--yes' does not suppress prompts $additionalArgs = Get-AdditionalArguments $args = $additionalArgs if($args) {$args = $additionalArgs.Split(' ')} if($request.Options.ContainsKey($script:AllVersions)) { $unInstallCmd=@("uninstall", $name, $additionalArgs, "--all-versions") } else { $unInstallCmd=@("uninstall", $name, "--version", $version) } Write-debug ("calling $script:ChocoExePath $unInstallCmd $additionalArgs") $progress = 1 $job=Start-Job -ScriptBlock { & $args[0] $args[1] $args[2] } -ArgumentList @($script:ChocoExePath, $unInstallCmd, $args) Show-Progress -ProgressMessage $LocalizedData.UnInstallingPackage -PercentComplete $progress -ProgressId $script:UnInstallPackageId $packages= $job | Receive-Job -Wait Write-debug ("Completed calling $script:ChocoExePath $unInstallCmd") Process-Package -Name $name ` -RequiredVersion $version ` -OperationMessage $LocalizedData.UnInstallingPackage ` -ProgressId $script:UnInstallPackageId ` -PercentComplete $progress -Packages $packages ` -NameContainsWildCard $true # pass in $true so that we do not exact name match because choco returns different sometimes. } # Returns the packages that are installed. function Get-InstalledPackage { [CmdletBinding()] param ( [Parameter()] [string] $Name, [Parameter()] [string] $RequiredVersion, [Parameter()] [string] $MinimumVersion, [Parameter()] [string] $MaximumVersion ) Write-Debug -Message ($LocalizedData.ProviderDebugMessage -f ('Get-InstalledPackage')) $ValidationResult = Validate-VersionParameters -Name $Name ` -MinimumVersion $MinimumVersion ` -MaximumVersion $MaximumVersion ` -RequiredVersion $RequiredVersion ` -AllVersions:$request.Options.ContainsKey($script:AllVersions) if(-not $ValidationResult) { # Return now as the version validation failed already return } $force = Get-ForceProperty if(-not (Install-ChocoBinaries -Force $force)) { return } $nameContainsWildCard = $false $additionalArgs = Get-AdditionalArguments $args = if($additionalArgs) {$additionalArgs.Split(' ')} Write-Progress -Activity $LocalizedData.FindingLocalPackage -PercentComplete 30 -Id $script:InstalledPackageId # If a user does not provide name or name contains wildcard, search all. # Choco does not support wildcard if searching local only if (-not $Name -or (Test-WildcardPattern -Name $Name)) { $nameContainsWildCard = $true Write-Debug "calling $script:ChocoExePath search --local-only --allversions $additionalArgs" $packages = & $script:ChocoExePath search --local-only --allversions $args } else { Write-Debug "calling $script:ChocoExePath search $Name --local-only --allversions $additionalArgs" $packages = & $script:ChocoExePath search $Name --local-only --allversions $args } Process-Package -Name $Name -ProgressId $script:InstalledPackageId ` -RequiredVersion $RequiredVersion ` -MinimumVersion $MinimumVersion ` -MaximumVersion $MaximumVersion ` -PercentComplete 20 ` -Packages $packages ` -NameContainsWildCard $nameContainsWildCard Write-Progress -Activity $LocalizedData.Complete -PercentComplete 100 -Completed -Id $script:InstalledPackageId } #endregion #region Helper functions # Display progress bar function Show-Progress { [CmdletBinding()] param ( [parameter()] [string] $ProgressMessage, [parameter()] [int] $PercentComplete, [parameter()] [int] $ProgressId ) $progress = $PercentComplete While(Get-Job -State 'Running') { Start-Sleep -Milliseconds 1000 Write-Progress -Activity $ProgressMessage -PercentComplete $progress -Id $ProgressId if($progress -ge 100) { $progress = 100 } else {$progress = 0.5 + $progress} } } # Processing the package function Process-Package { [CmdletBinding()] param ( [parameter()] [string] $Name, [Parameter()] [string] $RequiredVersion, [Parameter()] [string] $MinimumVersion, [Parameter()] [string] $MaximumVersion, [parameter()] [string] $OperationMessage, [parameter()] [int] $ProgressId, [parameter()] [int] $PercentComplete, [parameter()] [string[]] $Packages, [parameter()] [bool] $NameContainsWildCard = $false ) $progress = $PercentComplete $actionTaken = $false foreach ($pkg in $packages) { if($request.IsCanceled) { return } $progress += 5 $progress = [System.Math]::Min(100, $progress) Write-Progress -Activity $LocalizedData.ProcessingPackage -PercentComplete $progress -Id $ProgressId $Matches = $null if (($pkg -match $script:PackageRegex) -and ($pkg -notmatch $script:PackageReportRegex)) { $pkgname = $Matches.name $pkgversion = $Matches.version Write-Debug ("Choco message: '{0}'" -f $pkg) # check name match if(-not (Test-Name -Name $name -PackageName $pkgname -NameContainsWildcard $NameContainsWildCard)) { Write-Debug ("Skipping processing: '{0}'" -f $pkg) continue } if ($pkgname -and $pkgversion) { # filter on version if((Test-Version -Version $pkgversion.TrimStart('v') ` -RequiredVersion $requiredVersion ` -MinimumVersion $minimumVersion ` -MaximumVersion $maximumVersion )) { $swidObject = @{ FastPackageReference = $pkgname+"#" + $pkgversion; Name = $pkgname; Version = $pkgversion; versionScheme = "MultiPartNumeric"; Source = $script:PackageSource; } $sid = New-SoftwareIdentity @swidObject Write-Output -InputObject $sid if(-Not $actionTaken) {$actionTaken = $true} } } } } if ($OperationMessage) { if($actionTaken) { Write-Verbose ($LocalizedData.OperationSucceed -f $OperationMessage, $FastPackageReference) } else { Write-Error ($LocalizedData.OperationFailed -f $OperationMessage, $FastPackageReference) } } Write-Progress -Activity $LocalizedData.Complete -PercentComplete 100 -Completed -Id $ProgressId } # Get AdditionalArguments property from the input cmdline function Get-AdditionalArguments { [CmdletBinding()] [OutputType([string])] param ( ) $additionalArgs = $null $options = $request.Options if($options.ContainsKey($script:additionalArguments)) { $additionalArgs = $options[$script:additionalArguments] } return $additionalArgs } # Filter on name function Test-Name { [CmdletBinding()] [OutputType([bool])] param ( [string] $Name, [string] $PackageName, [bool] $NameContainsWildcard ) $nameRegex=$Name.TrimStart('*') $nameRegex=$nameRegex.TrimEnd('.') $nameRegex="^.*$nameRegex.*$" # filter on name if ($Name -and $PackageName -and ($PackageName -notmatch "$nameRegex")) { return $false } # exact name match if($Name -and $PackageName -and (-not $NameContainsWildcard) -and ($Name -ne $PackageName)) { return $false } return $true } # Find whether a user specifies -force function Get-ForceProperty { [CmdletBinding()] [OutputType([bool])] param ( ) $force = $false $options = $request.Options if($options.ContainsKey('Force')) { $force = (-not [System.String]::IsNullOrWhiteSpace($options['Force'])) } return $force } # Install choco function Install-ChocoBinaries { [CmdletBinding()] [OutputType([bool])] param ( [parameter()] [bool] $Force ) if($script:isNanoServer -or (IsCoreCLR)) { Write-Error ($LocalizedData.ChocoUnSupportedOnCoreCLR -f $script:ProviderName) return $false } if($script:ChocoExePath -and (Microsoft.PowerShell.Management\Test-Path -Path $script:ChocoExePath)) { Write-Debug ("Choco already installed in '{0}'" -f $script:ChocoExePath) return $true } # Setup $script:ChocoExePath if ((Get-ChocoPath) -and $script:ChocoExePath) { Write-Debug ("Choco already installed in '{0}'" -f $script:ChocoExePath) if(-not $script:firstTime) { $script:firstTime = $false return $true } $progress = 5 Write-Progress -Activity $LocalizedData.CheckingChoco -PercentComplete $progress -Id $script:InstallChocoId # For the first time in the current PowerShell Session, we check choco version to see if upgrade is needed $name = "Chocolatey" Write-Debug ("$script:ChocoExePath search $name") $packages = & $script:ChocoExePath search $name $progress += 5 Write-Progress -Activity $LocalizedData.CheckingChoco -PercentComplete $progress -Id $script:InstallChocoId foreach ($pkg in $packages) { if($request.IsCanceled) { return } $Matches = $null if (($pkg -match $script:PackageRegex) -and ($pkg -notmatch $script:PackageReportRegex)) { $pkgname = $Matches.name $pkgversion = $Matches.version Write-Debug ("Choco message: '{0}'" -f $pkg) # check name match if(-not (Test-Name -Name $name -PackageName $pkgname)) { Write-Debug ("Skipping processing: '{0}'" -f $pkg) continue } if ($pkgname -and $pkgversion) { $installedVersion = Get-InstalledChocoVersion $progress += 5 Write-Progress -Activity $LocalizedData.CheckingChoco -PercentComplete $progress -Id $script:InstallChocoId if((Compare-SemVer -Version1 $pkgversion.Trim('v') -Version2 $installedVersion) -eq 1) { # There is a newer version of Chocolatey available Write-Verbose ($LocalizedData.FoundNewerChocolatey -f $pkgversion, $installedVersion) # Should continue message for upgrading Choco.exe $shouldContinueQueryMessageUpgrade = ($LocalizedData.UpgradePackageQuery -f $pkgversion) $shouldContinueCaptionUpgrade = ($LocalizedData.InstallPackageQuery -f "Upgrading", $name) if($Force -or $request.ShouldContinue($shouldContinueQueryMessageUpgrade, $shouldContinueCaptionUpgrade)) { Write-Progress -Activity $LocalizedData.UpgradingChoco -PercentComplete $progress -Id $script:InstallChocoId Write-Debug ("Calling $script:ChocoExePath upgrade chocolatey") $job=Start-Job -ScriptBlock { & $args[0] upgrade chocolatey -y } -ArgumentList @($script:ChocoExePath) Show-Progress -ProgressMessage $LocalizedData.UpgradingChoco ` -PercentComplete $progress ` -ProgressId $script:InstallChocoId $packages= $job | Receive-Job -Wait Process-Package -Name $name ` -RequiredVersion $pkgversion ` -OperationMessage $LocalizedData.UpgradingChoco ` -ProgressId $script:InstallChocoId ` -PercentComplete $progress ` -Packages $packages } } else { Write-Debug ("Current version of chocolatey is up to date") } break } } } # foreach Write-Progress -Activity $LocalizedData.Complete -PercentComplete 100 -Completed -Id $script:InstallChocoId return $true } # Should continue message for installing Choco.exe $shouldContinueQueryMessage = $LocalizedData.InstallChocoExeShouldContinueQuery $shouldContinueCaption = $LocalizedData.InstallChocoExeShouldContinueCaption if(-not $Force) { $continue = $request.ShouldContinue($shouldContinueQueryMessage, $shouldContinueCaption) if(-not $continue) { Write-Error ($LocalizedData.UserDeclined -f "install") return $false } } # install choco based on https://chocolatey.org/install#before-you-install try{ Invoke-WebRequest 'https://chocolatey.org/install.ps1' -UseBasicParsing | Invoke-Expression > $null } catch { if($error[0]) { Write-Error $error[0] } } if (Get-ChocoPath) { return $true } else { Write-Error ($LocalizedData.FailToInstallChoco) } return $false } # Get the choco installed path function Get-ChocoPath { [CmdletBinding()] [OutputType([string])] param( ) # Using Get-Command cmdlet, get the location of Choco.exe if it is available under $env:PATH. $chocoCmd = Microsoft.PowerShell.Core\Get-Command -Name $script:ChocoExeName ` -ErrorAction SilentlyContinue ` -WarningAction SilentlyContinue | Microsoft.PowerShell.Core\Where-Object { $_.Path -and ((Microsoft.PowerShell.Management\Split-Path -Path $_.Path -Leaf) -eq $script:ChocoExeName) -and (-not $_.Path.StartsWith($env:windir, [System.StringComparison]::OrdinalIgnoreCase)) } | Microsoft.PowerShell.Utility\Select-Object -First 1 if($chocoCmd -and $chocoCmd.Path) { $script:ChocoExePath = $chocoCmd.Path $BootstrapChocoExe = $false Write-Verbose ($LocalizedData.ChocoFound -f $script:ChocoExePath) } else { return $null } return $chocoCmd.Path } function Get-InstalledChocoVersion { $name = "Chocolatey" $installedChocoVersion = $null Write-Debug ("Calling $script:ChocoExePath search chocolatey --local-only") $packages = & $script:ChocoExePath search chocolatey --local-only foreach ($pkg in $packages) { if($request.IsCanceled) { return } $Matches = $null if (($pkg -match $script:PackageRegex) -and ($pkg -notmatch $script:PackageReportRegex)) { $pkgname = $Matches.name $pkgversion = $Matches.version Write-Debug ("Choco message: '{0}'" -f $pkg) # check name match if(-not (Test-Name -Name $name -PackageName $pkgname)) { Write-Debug ("Skipping processing: '{0}'" -f $pkg) continue } if ($pkgname -and $pkgversion) { $installedChocoVersion = $pkgversion.Trim('v') break } } } # foreach return $installedChocoVersion } # Check whether $version meets the criteria defined in $RequiredVersion, $MinimumVersion and $MaximumVersion function Test-Version { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] $Version, [Parameter()] [string] $RequiredVersion, [Parameter()] [string] $MinimumVersion, [Parameter()] [string] $MaximumVersion ) if(-not ($RequiredVersion -or $MinimumVersion -or $MaximumVersion)) { return $true } if($RequiredVersion) { return ($Version -eq $RequiredVersion) } $isMatch = $false if($MinimumVersion) { $isMatch = $Version -ge $MinimumVersion } if($MaximumVersion) { if($MinimumVersion) { $isMatch = $isMatch -and ($Version -le $MaximumVersion) } else { $isMatch = $Version -le $MaximumVersion } } return $isMatch } # Test if the $Name contains any wildcard characters function Test-WildcardPattern { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] $Name ) return [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name) } # Validate versions function Validate-VersionParameters { Param( [Parameter()] [String[]] $Name, [Parameter()] [String] $MinimumVersion, [Parameter()] [String] $RequiredVersion, [Parameter()] [String] $MaximumVersion, [Parameter()] [Switch] $AllVersions ) if($AllVersions -and ($RequiredVersion -or $MinimumVersion -or $MaximumVersion)) { ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $LocalizedData.AllVersionsCannotBeUsedWithOtherVersionParameters ` -ErrorId 'AllVersionsCannotBeUsedWithOtherVersionParameters' ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidArgument } elseif($RequiredVersion -and ($MinimumVersion -or $MaximumVersion)) { ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $LocalizedData.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether ` -ErrorId "VersionRangeAndRequiredVersionCannotBeSpecifiedTogether" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidArgument } elseif($MinimumVersion -and $MaximumVersion -and ($MinimumVersion -gt $MaximumVersion)) { $Message = $LocalizedData.MinimumVersionIsGreaterThanMaximumVersion -f ($MinimumVersion, $MaximumVersion) ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $Message ` -ErrorId "MinimumVersionIsGreaterThanMaximumVersion" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidArgument } elseif($AllVersions -or $RequiredVersion -or $MinimumVersion -or $MaximumVersion) { if(-not $Name -or $Name.Count -ne 1 -or (Test-WildcardPattern -Name $Name[0])) { ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $LocalizedData.VersionParametersAreAllowedOnlyWithSingleName ` -ErrorId "VersionParametersAreAllowedOnlyWithSingleName" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidArgument } } return $true } # Utility to throw an errorrecord function ThrowError { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $CallerPSCmdlet, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionName, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionMessage, [System.Object] $ExceptionObject, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [parameter(Mandatory = $true)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $ErrorCategory ) $exception = New-Object $ExceptionName $ExceptionMessage; $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject $CallerPSCmdlet.ThrowTerminatingError($errorRecord) } #region Semversion variables $AllowFourPartsVersion = "(?<Version>\d+(\s*\.\s*\d+){0,3})"; $ThreePartsVersion = "(?<Version>\d+(\.\d+){2})"; # the pre-release regex is of the form -<pre-release version> where <pre-release> version is set of identifier # delimited by ".". Each identifer can be any characters in [A-z0-9a-z-] $ReleasePattern = "(?<Release>-[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; # The build regex is of the same form except with a + instead of - $BuildPattern = "(?<Build>\+[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; # For some reason Chocolatey version uses "-" instead of "+" for the build metadata. Here change it to "-" $ReleasePatternDash = "(?<Release>-[A-Z0-9a-z]+(\.[A-Z0-9a-z]+)*)?"; $BuildPatternDash = "(?<Build>\-[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; # Purposely this should be the regex $SemanticVersionPattern = "^" + $AllowFourPartsVersion + $ReleasePattern +$BuildPattern + "$" # But we use this one because Chocolatey uses <version>-<release>-<build> format $SemanticVersionPatternDash = "^" + $AllowFourPartsVersion + $ReleasePatternDash + $BuildPatternDash + "$" #endregion # Compare two sematic verions # -1 if $Version1 < $Version2 # 0 if $Version1 = $Version2 # 1 if $Version1 > $Version2 function Compare-SemVer { [CmdletBinding()] [OutputType([int])] param( [string] $Version1, [string] $Version2 ) $versionObject1 = Get-VersionPSObject $Version1 $versionObject2 = Get-VersionPSObject $Version2 if((-not $versionObject1) -and (-not $versionObject2)) { return 0 } if((-not $versionObject1) -and ($versionObject2)) { return -1 } if(($versionObject1) -and (-not $versionObject2)) { return 1 } $VersionResult = ([Version]$versionObject1.Version).CompareTo([Version]$versionObject2.Version) if($VersionResult -ne 0) { return $VersionResult } if($versionObject1.Release -and (-not $versionObject2.Release)) { return -1 } if(-not $versionObject1.Release -and $versionObject2.Release) { return 1 } $ReleaseResult = Compare-ReleaseMetadata -Version1Metadata $versionObject1.Release -Version2Metadata $versionObject2.Release return $ReleaseResult # Based on http://semver.org/, Build metadata SHOULD be ignored when determining version precedence } function Get-VersionPSObject { param( [Parameter(Mandatory=$true)] [string] $Version ) $isMatch=$Version.Trim() -match $SemanticVersionPatternDash if($isMatch) { if ($Matches.Version) {$v = $Matches.Version.Trim()} else {$v = $Matches.Version} if ($Matches.Release) {$r = $Matches.Release.Trim("-, +")} else {$r = $Matches.Release} if ($Matches.Build) {$b = $Matches.Build.Trim("-, +")} else {$b = $Matches.Build} return New-Object PSObject -Property @{ Version = $v Release = $r Build = $b } } else { ThrowError -ExceptionName "System.InvalidOperationException" ` -ExceptionMessage ($LocalizedData.InvalidVersionFormat -f $Version, $SemanticVersionPatternDash) ` -ErrorId "InvalidVersionFormat" ` -CallerPSCmdlet $PSCmdlet ` -ErrorCategory InvalidOperation } } function Compare-ReleaseMetadata { [CmdletBinding()] [OutputType([int])] param( [string] $Version1Metadata, [string] $Version2Metadata ) if((-not $Version1Metadata) -and (-not $Version2Metadata)) { return 0 } # For release part, 1.0.0 is newer/greater then 1.0.0-alpha. So return 1 here. if((-not $Version1Metadata) -and $Version2Metadata) { return 1 } if(($Version1Metadata) -and (-not $Version2Metadata)) { return -1 } $version1Parts=$Version1Metadata.Trim('-').Split('.') $version2Parts=$Version2Metadata.Trim('-').Split('.') $length = [System.Math]::Min($version1Parts.Length, $version2Parts.Length) for ($i = 0; ($i -lt $length); $i++) { $result = Compare-MetadataPart -Version1Part $version1Parts[$i] -Version2Part $version2Parts[$i] if ($result -ne 0) { return $result } } # so far we found two versions are the same. If length is the same, we think two version are indeed the same if($version1Parts.Length -eq $version1Parts.Length) { return 0 } # 1.0.0-alpha < 1.0.0-alpha.1 if($version1Parts.Length -lt $length) { return -1 } else { return 1 } } function Compare-MetadataPart { [CmdletBinding()] [OutputType([int])] param( [string] $Version1Part, [string] $Version2Part ) if((-not $Version1Part) -and (-not $Version2Part)) { return 0 } # For release part, 1.0.0 is newer/greater then 1.0.0-alpha. So return 1 here. if((-not $Version1Part) -and $Version2Part) { return 1 } if(($Version1Part) -and (-not $Version2Part)) { return -1 } $version1Num = 0 $version2Num = 0 $v1IsNumeric = [System.Int32]::TryParse($Version1Part, [ref] $version1Num); $v2IsNumeric = [System.Int32]::TryParse($Version2Part, [ref] $version2Num); $result = 0 # if both are numeric compare them as numbers if ($v1IsNumeric -and $v2IsNumeric) { $result = $version1Num.CompareTo($version2Num); } elseif ($v1IsNumeric -or $v2IsNumeric) { # numeric numbers come before alpha chars if ($v1IsNumeric) { return -1 } else { return 1 } } else { $result = [string]::Compare($Version1Part, $Version2Part) } return $result } #endregion #Export-ModuleMember -Function Compare-SemVer |