PipProvider.ps1
|
function Get-PipPackageProvider { [CmdletBinding()] param() [pscustomobject]@{ Name = 'Pip' Platforms = @('Windows', 'Linux', 'MacOS') RequiredModules = @('Shmuelie.Utilities') RequiredCommands = @('Shmuelie.Utilities\Get-PipPackages', 'Shmuelie.Utilities\Update-PipPackage', 'pip') OptionNames = @('User', 'TopLevelOnly') TestAvailable = $null GetTargets = { param([hashtable]$Options) $parameters = Get-PipProviderOptions -Options $Options $packages = @(Get-PipProviderPackages @parameters -PackageState Outdated) $names = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($package in $packages) { $name = Get-PipProviderPackageName -Name $package.name if (-not $names.Add($name)) { throw "pip returned duplicate package '$($package.name)'." } New-PackageUpdateTarget -Target $package.name -PreviousVersion $package.version -ProposedVersion $package.latest_version } } Update = { param($Target, [hashtable]$Options) $parameters = Get-PipProviderOptions -Options $Options $name = Get-PipProviderPackageName -Name $Target.Target $diagnostics = [System.Collections.Generic.List[string]]::new() # The public updater sends native output to verbose and has no User switch. $results = @(Shmuelie.Utilities\Update-PipPackage -PackageName $Target.Target -Confirm:$false -ErrorAction Stop -Verbose 4>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.VerboseRecord]) { $diagnostics.Add($_.Message) Write-Verbose $_.Message } else { $_ } }) $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { throw "pip update failed for '$($Target.Target)' (exit code $exitCode). $($diagnostics -join [Environment]::NewLine)" } if ($results.Count -ne 1 -or $results[0].PSTypeNames -notcontains 'PipUpdateResult' -or $results[0].Success -isnot [bool] -or (Get-PipProviderPackageName -Name $results[0].Name) -ne $name) { throw "Update-PipPackage returned an invalid result for '$($Target.Target)'; the outcome is unknown." } if (-not $results[0].Success) { throw "Update-PipPackage reported failure for '$($Target.Target)'. $($diagnostics -join [Environment]::NewLine)" } # Observe all installed packages in the selected scope, even if dependencies changed. $installed = @(Get-PipProviderPackages -User:$parameters.User -PackageState Any) $observed = @($installed | Where-Object { (Get-PipProviderPackageName -Name $_.name) -eq $name }) if ($observed.Count -ne 1 -or [string]::IsNullOrWhiteSpace($observed[0].version)) { throw "Cannot observe one installed version for '$($Target.Target)' after the update." } $status = if ($observed[0].version -ceq $Target.PreviousVersion) { 'Unchanged' } else { 'Updated' } New-PackageUpdateResult -Provider Pip -Target $Target.Target -PreviousVersion $Target.PreviousVersion -ResultingVersion $observed[0].version -Status $status } } } function Get-PipProviderOptions { [CmdletBinding()] param([Parameter(Mandatory)][hashtable]$Options) $parameters = @{ User = $false; TopLevelOnly = $true } foreach ($key in $Options.Keys) { if ($key -notin 'User', 'TopLevelOnly' -or ($Options[$key] -isnot [bool] -and $Options[$key] -isnot [System.Management.Automation.SwitchParameter])) { throw "Pip option '$key' must be User or TopLevelOnly with a Boolean value." } $parameters[$key] = [bool]$Options[$key] } $parameters } function Get-PipProviderPackageName { [CmdletBinding()] param([AllowNull()][object]$Name) # Distribution names only: never forward options, requirement specifiers, URLs, or shell syntax. if ($Name -isnot [string] -or $Name -cnotmatch '\A[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\z') { throw "pip returned an invalid package name '$Name'." } ($Name -replace '[-_.]+', '-').ToLowerInvariant() } function Get-PipProviderPackages { [CmdletBinding()] param( [switch]$User, [switch]$TopLevelOnly, [ValidateSet('Any', 'Outdated')][string]$PackageState ) $packages = @(Shmuelie.Utilities\Get-PipPackages -User:$User -TopLevelOnly:$TopLevelOnly -PackageState $PackageState -ErrorAction Stop) if ($LASTEXITCODE -ne 0) { throw "pip list failed (exit code $LASTEXITCODE)." } foreach ($package in $packages) { $null = Get-PipProviderPackageName -Name $package.name foreach ($property in 'version', 'latest_version') { if ($null -ne $package.$property -and $package.$property -isnot [string]) { throw "pip returned an invalid $property for '$($package.name)'." } } } $packages } |