PxGet.psm1
# Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $moduleRoot as the relative root from which to find # things. A published module has its function appended to this file, while a # module in development has its functions in the Functions directory. $moduleRoot = $PSScriptRoot # Store each of your module's functions in its own file in the Functions # directory. On the build server, your module's functions will be appended to # this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the # functions that are in their own files. $functionsPath = Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function Invoke-PxGet { <# .SYNOPSIS Invokes PxGet. .DESCRIPTION A tool similar to nuget but for PowerShell modules. A config file in the root of a repository that specifies what modules should be installed into the PSModules directory of the repository. If a path is provided for the module it will be installed at the specified path instead of the PSModules directory. .EXAMPLE Invoke-PxGet 'install' Demonstrates how to call this function to install required PSModules. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('install')] [string] $Command ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $origModulePath = $env:PSModulePath $privateModulesPath = Join-Path -Path $(Get-Location) -ChildPath 'PSModules' if( -not (Test-Path -Path $privateModulesPath) ) { New-Item -Path $privateModulesPath -ItemType 'Directory' | Out-Null } $deepPrefs = @{} if( (Test-Path -Path 'env:PXGET_DISABLE_DEEP_DEBUG') ) { $deepPrefs['Debug'] = $false } if( (Test-Path -Path 'env:PXGET_DISABLE_DEEP_VERBOSE') ) { $deepPrefs['Verbose'] = $false } $activity = 'pxget install' try { # pxget should ship with its own private copies of PackageManagement and PowerShellGet. Setting PSModulePath # to pxget module's Modules directory ensures no other package modules get loaded. $pxGetModulesRoot = Join-Path -Path $moduleRoot -ChildPath 'Modules' $env:PSModulePath = $pxGetModulesRoot Write-Debug "PSModulePath $($env:PSModulePath)" Write-Debug "moduleRoot $($pxGetModulesRoot)" Get-Module -ListAvailable | Format-Table -AutoSize | Out-String | Write-Debug Import-Module -Name 'PackageManagement' @deepPrefs Import-Module -Name 'PowerShellGet' @deepPrefs Get-Module | Format-Table -AutoSize | Out-String | Write-Debug $env:PSModulePath = @($privateModulesPath, $pxGetModulesRoot) -join [IO.Path]::PathSeparator Write-Debug "PSModulePath $($env:PSModulePath)" Get-Module -ListAvailable | Format-Table -AutoSize | Out-String | Write-Debug $modulesNotFound = @() $pxgetJsonPath = Join-Path -Path (Get-Location) -ChildPath 'pxget.json' if( -not (Test-Path -Path $pxgetJsonPath) ) { Write-Error 'There is no pxget.json file in the current directory.' return } $pxModules = Get-Content -Path $pxgetJsonPath | ConvertFrom-Json if( -not $pxModules ) { Write-Warning 'The pxget.json file is empty!' return } $moduleNames = $pxModules.PSModules | Select-Object -ExpandProperty 'Name' if( -not $moduleNames ) { Write-Warning "There are no modules listed in ""$($pxgetJsonPath | Resolve-Path -Relative)""." return } $numInstalls = $moduleNames | Measure-Object | Select-Object -ExpandProperty 'Count' $numInstalls = $numInstalls * 2 + 1 Write-Debug " numSteps $($numInstalls)" $curStep = 0 $status = 'Finding latest module versions.' $uniqueModuleNames = $moduleNames | Select-Object -Unique $op = "Find-Module -Name '$($uniqueModuleNames -join "', '")'" $percentComplete = ($curStep++/$numInstalls * 100) Write-Progress -Activity $activity -Status $status -CurrentOperation $op -PercentComplete $percentComplete $modules = Find-Module -Name $uniqueModuleNames -ErrorAction Ignore @deepPrefs if( -not $modules ) { $msg = "$($pxgetJsonPath | Resolve-Path -Relative): Modules ""$($uniqueModuleNames -join '", "')"" not " + 'found.' Write-Error $msg return } # Find-Module is expensive. Limit calls as much as possible. $findModuleCache = @{} # We only care if the module is in PSModules right now. Later we'll allow dev dependencies, which can be # installed globally. $env:PSModulePath = $privateModulesPath foreach( $pxModule in $pxModules.PSModules ) { $allowPrerelease = $pxModule.Version -match '-' $progressState = @{ Activity = $activity; Status = "Saving $($pxModule.Name) $($pxModule.Version)"; } Write-Debug " curStep $($curStep)" $percentComplete = ($curStep++/$numInstalls * 100) $moduleToInstall = $modules | Select-Module -Name $pxModule.Name -Version $pxModule.Version -AllowPrerelease:$allowPrerelease | Select-Object -First 1 if( -not $moduleToInstall ) { $allowPrereleaseOp = '' if( $allowPrerelease ) { $allowPrereleaseOp = ' -AllowPrerelease' } $op = "Find-Module -Name '$($pxModule.Name)' -AllVersions$($allowPrereleaseOp)" if( -not $findModuleCache.ContainsKey($op) ) { Write-Progress @progressState -CurrentOperation $op -PercentComplete $percentComplete $findModuleCache[$op] = Find-Module -Name $pxModule.Name ` -AllVersions ` -AllowPrerelease:$allowPrerelease ` -ErrorAction Ignore ` @deepPrefs } $moduleToInstall = $findModuleCache[$op] | Select-Module -Name $pxModule.Name -Version $pxModule.Version -AllowPrerelease:$allowPrerelease | Select-Object -First 1 } if( -not $moduleToInstall ) { Write-Debug " curStep $($curStep)" $curStep += 1 $modulesNotFound += $pxModule.Name continue } $progressState['Status'] = "Saving $($moduleToInstall.Name) $($moduleToInstall.Version)" Write-Progress @progressState -PercentComplete $percentComplete Start-Sleep -Seconds 2 $installedModule = Get-Module -Name $pxModule.Name -List | Where-Object 'Version' -eq $moduleToInstall.Version # The latest version that matches the version in the pxget.json file is already installed if( $installedModule ) { Write-Debug " curStep $($curStep)" $curStep += 1 $installedModule continue } $installPath = $privateModulesPath if( ($pxModule.PSObject.Properties.Name -Contains 'Path') -and (-not [string]::IsNullOrWhiteSpace($pxModule.Path)) ) { $installPath = $pxModule.Path } if( -not (Test-Path -Path $installPath) ) { New-Item -Path $installPath -ItemType 'Directory' | Out-Null } $op = "Save-Module -Name '$($moduleToInstall.Name)' -Version '$($moduleToInstall.Version)' -Path " + "'$($installPath | Resolve-Path -Relative)' -Repository '$($moduleToInstall.Repository)'" Write-Debug " curStep $($curStep)" $percentComplete = ($curStep++/$numInstalls * 100) Write-Progress @progressState -CurrentOperation $op -PercentComplete $percentComplete $curProgressPref = $ProgressPreference $Global:ProgressPreference = [Management.Automation.ActionPreference]::SilentlyContinue try { # Not installed. Install it. We pipe it so the repository of the module is also used. $moduleToInstall | Save-Module -Path $installPath @deepPrefs } finally { $Global:ProgressPreference = $curProgressPref } $savedToPath = Join-Path -Path $installPath -ChildPath $moduleToInstall.Name $savedToPath = Join-Path -Path $savedToPath -ChildPath ($moduleToInstall.Version -replace '-.*$', '') Get-Module -Name $savedToPath -ListAvailable @deepPrefs } if( $modulesNotFound ) { Write-Error "$($pxgetJsonPath | Resolve-Path -Relative): Module(s) ""$($modulesNotFound -join '", "')"" not found." return } } finally { $env:PSModulePath = $origModulePath Write-Progress -Activity $activity -Completed } } Set-Alias -Name 'pxget' -Value 'Invoke-PxGet' function Select-Module { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true)] [PSCustomObject] $Module, [Parameter(Mandatory)] [String] $Name, [Parameter(Mandatory)] [String] $Version, [switch] $AllowPrerelease ) process { if( $Module.Name -ne $Name -or $Module.Version -notlike $Version ) { return } if( $AllowPrerelease ) { return $Module } [Version]$moduleVersion = $null if( [Version]::TryParse($Module.Version, [ref]$moduleVersion) ) { return $Module } } } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } |