Public/Invoke-ModuleInstall.ps1
|
function Invoke-ModuleInstall { <# .SYNOPSIS Installs a module from PSGallery if absent or below the required minimum version, then imports it. .DESCRIPTION Centralises the install-if-missing pattern used by all infrastructure setup scripts. Extracting it here makes the logic testable and removes the need for each consumer repo to duplicate it. Note: this function cannot bootstrap itself. Each consumer script still needs a short inline guard to install Infrastructure.Common before this function is available - but that is a one-time cost per script, and all other module installs flow through this function. That inline guard is also responsible for ensuring the NuGet package provider is present, since by the time this function runs NuGet is already available. .PARAMETER ModuleName The name of the module to install and import. .PARAMETER MinimumVersion The minimum acceptable version. If the installed version is below this, the module is reinstalled. When omitted, any installed version is accepted and only a missing module triggers an install. .EXAMPLE Invoke-ModuleInstall -ModuleName 'Infrastructure.Secrets' ` -MinimumVersion '1.1.0' .EXAMPLE Invoke-ModuleInstall -ModuleName 'Posh-SSH' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $ModuleName, [Parameter()] [Version] $MinimumVersion ) $installed = Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object -First 1 # When MinimumVersion is omitted, install only if the module is absent. # When provided, also reinstall if the version is too old. $needsInstall = -not $installed -or ($MinimumVersion -and $installed.Version -lt $MinimumVersion) if ($needsInstall) { $versionLabel = if ($MinimumVersion) { " >= $MinimumVersion" } else { '' } Write-Host "Installing $ModuleName$versionLabel from PSGallery ..." ` -ForegroundColor Cyan $installParams = @{ Name = $ModuleName Scope = 'CurrentUser' Force = $true # Required when the module exports commands that are already present # from a previously loaded version of the same module. AllowClobber = $true } if ($MinimumVersion) { $installParams.MinimumVersion = $MinimumVersion } Install-Module @installParams } # The version Import-Module would pick (highest on disk). Re-queried # after the install block so we see the freshly installed version. # Property accessed via an intermediate variable so Set-StrictMode # does not blow up when the module is genuinely absent (e.g. in unit # tests where Install-Module is mocked and installs nothing). $highestAvailable = Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object -First 1 $targetVersion = if ($highestAvailable) { $highestAvailable.Version } else { $null } # Skip the unload+reload cycle when exactly the target version is # already the only one loaded. The unload only exists to break the # two-versions-live trap (older + newer both in the session at once, # making command resolution order-dependent); when that trap is not # in play, reloading is wasted work. $loaded = @(Get-Module -Name $ModuleName) $alreadyCorrect = $loaded.Count -eq 1 -and $loaded[0].Version -eq $targetVersion if (-not $alreadyCorrect) { if ($loaded) { $loaded | Remove-Module -Force } Import-Module $ModuleName -Force -ErrorAction Stop } } |