Public/Clear-PSDependencyCanaryEnvironment.ps1
|
function Clear-PSDependencyCanaryEnvironment { <# .SYNOPSIS Removes a project's installed PowerShell dependencies before bootstrapping. .DESCRIPTION Removes every installed version of PSDepend, PSDependencyCanary, modules declared in requirements.psd1, and modules declared by RequiredModules in the project manifest. Pester 3.x is preserved for Windows PowerShell compatibility. Run this command in its own process or CI step. The executing PSDependencyCanary module remains loaded in memory until the process exits even after its installed files have been removed. The next process can then install only the project's committed dependency pins. .PARAMETER ProjectRoot Root directory of the PowerShell module project. Defaults to BHProjectPath when available and otherwise to the current directory. .PARAMETER ModulePath Module roots to clean. Defaults to the current process PSModulePath entries. This parameter is primarily useful for isolated validation. .PARAMETER AllowPreinstalledModules Warn instead of failing when a protected or locked dependency cannot be removed. .EXAMPLE Clear-PSDependencyCanaryEnvironment -ProjectRoot . -Confirm:$false Cleans installed project dependencies before the committed bootstrap. .EXAMPLE Clear-PSDependencyCanaryEnvironment -ProjectRoot . -WhatIf Shows which installed dependency directories would be removed. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [string]$ProjectRoot, [string[]]$ModulePath, [switch]$AllowPreinstalledModules ) if ([string]::IsNullOrWhiteSpace($ProjectRoot)) { $ProjectRoot = if ($env:BHProjectPath) { $env:BHProjectPath } else { (Get-Location).Path } } $ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path $requirementsPaths = @((Join-Path -Path $ProjectRoot -ChildPath 'requirements.psd1')) $moduleNames = @('PSDepend', 'PSDependencyCanary') $preservedPesterMajorVersion = 3 $sourceRoot = Join-Path -Path $ProjectRoot -ChildPath 'src' $projectModuleRoots = @( if (Test-Path -LiteralPath $sourceRoot -PathType Container) { Get-ChildItem -LiteralPath $sourceRoot -Directory } Get-ChildItem -LiteralPath $ProjectRoot -Directory | Where-Object Name -ne 'src' ) $manifestCandidates = @( $projectModuleRoots | ForEach-Object { $candidate = Join-Path -Path $_.FullName -ChildPath "$($_.Name).psd1" if (Test-Path -LiteralPath $candidate -PathType Leaf) { $candidate } } ) if ($manifestCandidates.Count -eq 1) { $manifestData = Import-PowerShellDataFile -LiteralPath $manifestCandidates[0] foreach ($requiredModule in @($manifestData.RequiredModules)) { if ($requiredModule -is [string]) { $moduleNames += $requiredModule } elseif ($requiredModule.ModuleName) { $moduleNames += [string]$requiredModule.ModuleName } } $templateRequirements = Join-Path -Path (Split-Path -Path $manifestCandidates[0] -Parent) ` -ChildPath 'template/requirements.psd1' if (Test-Path -LiteralPath $templateRequirements -PathType Leaf) { $requirementsPaths += $templateRequirements } } foreach ($requirementsPath in $requirementsPaths) { if (-not (Test-Path -LiteralPath $requirementsPath -PathType Leaf)) { continue } $requirements = Import-PowerShellDataFile -LiteralPath $requirementsPath $moduleNames += @($requirements.Keys | Where-Object { $_ -ne 'PSDependOptions' }) } $moduleNames = @($moduleNames | ForEach-Object { [string]$_ } | Sort-Object -Unique) foreach ($moduleName in $moduleNames) { if ([string]::IsNullOrWhiteSpace($moduleName) -or $moduleName -in @('.', '..') -or $moduleName -match '[/\\]') { throw "Refusing to clean invalid module name '$moduleName'." } } $cleanPester = $moduleNames -contains 'Pester' $moduleNamesWithoutPester = @($moduleNames | Where-Object { $_ -ne 'Pester' }) if (-not $PSBoundParameters.ContainsKey('ModulePath')) { $ModulePath = @($env:PSModulePath -split [System.IO.Path]::PathSeparator) } $moduleRoots = @( $ModulePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { try { [System.IO.Path]::GetFullPath( $_.TrimEnd( [System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar ) ) } catch { Write-Verbose "Ignoring malformed PSModulePath entry: $_" } } | Sort-Object -Unique ) $removedPaths = @() $preservedPaths = @() $failedPaths = @() foreach ($loadedModule in @(Get-Module)) { if (-not $loadedModule.ModuleBase) { continue } $loadedModuleBase = [System.IO.Path]::GetFullPath($loadedModule.ModuleBase) $isInCleanRoot = @( $moduleRoots | Where-Object { $loadedModuleBase.Equals($_, [System.StringComparison]::OrdinalIgnoreCase) -or $loadedModuleBase.StartsWith( $_ + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase ) } ).Count -gt 0 $isTargetedModule = $moduleNamesWithoutPester -contains $loadedModule.Name $isTargetedPester = $cleanPester -and $loadedModule.Name -eq 'Pester' -and $loadedModule.Version.Major -ne $preservedPesterMajorVersion $isExecutingModule = $loadedModule.Name -eq $MyInvocation.MyCommand.ModuleName if ($isInCleanRoot -and ($isTargetedModule -or $isTargetedPester) -and -not $isExecutingModule) { $loadedModule | Remove-Module -Force -ErrorAction SilentlyContinue } } foreach ($moduleRoot in $moduleRoots) { foreach ($moduleName in $moduleNamesWithoutPester) { $moduleDirectory = Join-Path -Path $moduleRoot -ChildPath $moduleName if (-not (Test-Path -LiteralPath $moduleDirectory -PathType Container)) { continue } if ($PSCmdlet.ShouldProcess($moduleDirectory, 'Remove installed project dependency')) { try { Remove-Item -LiteralPath $moduleDirectory -Recurse -Force $removedPaths += $moduleDirectory } catch { $failedPaths += $moduleDirectory if (-not $AllowPreinstalledModules) { throw } Write-Warning "Unable to remove module path: $moduleDirectory" } } } } if ($cleanPester) { foreach ($moduleRoot in $moduleRoots) { $pesterDirectory = Join-Path -Path $moduleRoot -ChildPath 'Pester' if (-not (Test-Path -LiteralPath $pesterDirectory -PathType Container)) { continue } $flatManifestPath = Join-Path -Path $pesterDirectory -ChildPath 'Pester.psd1' if (Test-Path -LiteralPath $flatManifestPath -PathType Leaf) { $flatPesterVersion = [version]( (Import-PowerShellDataFile -LiteralPath $flatManifestPath).ModuleVersion ) if ($flatPesterVersion.Major -eq $preservedPesterMajorVersion) { $preservedPaths += $pesterDirectory continue } if ($PSCmdlet.ShouldProcess($pesterDirectory, 'Remove installed project dependency')) { try { Remove-Item -LiteralPath $pesterDirectory -Recurse -Force $removedPaths += $pesterDirectory } catch { $failedPaths += $pesterDirectory if (-not $AllowPreinstalledModules) { throw } Write-Warning "Unable to remove module path: $pesterDirectory" } } continue } foreach ($pesterEntry in @(Get-ChildItem -LiteralPath $pesterDirectory -Force)) { $parsedVersion = $null $isPreservedVersion = $pesterEntry.PSIsContainer -and [version]::TryParse($pesterEntry.Name, [ref]$parsedVersion) -and $parsedVersion.Major -eq $preservedPesterMajorVersion if ($isPreservedVersion) { $preservedPaths += $pesterEntry.FullName continue } if ($PSCmdlet.ShouldProcess($pesterEntry.FullName, 'Remove installed project dependency')) { try { Remove-Item -LiteralPath $pesterEntry.FullName -Recurse -Force $removedPaths += $pesterEntry.FullName } catch { $failedPaths += $pesterEntry.FullName if (-not $AllowPreinstalledModules) { throw } Write-Warning "Unable to remove module path: $($pesterEntry.FullName)" } } } } } if (-not $WhatIfPreference) { $remainingPaths = @( foreach ($moduleRoot in $moduleRoots) { foreach ($moduleName in $moduleNamesWithoutPester) { $moduleDirectory = Join-Path -Path $moduleRoot -ChildPath $moduleName if (Test-Path -LiteralPath $moduleDirectory -PathType Container) { $moduleDirectory } } } if ($cleanPester) { foreach ($moduleRoot in $moduleRoots) { $pesterDirectory = Join-Path -Path $moduleRoot -ChildPath 'Pester' if (-not (Test-Path -LiteralPath $pesterDirectory -PathType Container)) { continue } $flatManifestPath = Join-Path -Path $pesterDirectory -ChildPath 'Pester.psd1' if (Test-Path -LiteralPath $flatManifestPath -PathType Leaf) { $flatPesterVersion = [version]( (Import-PowerShellDataFile -LiteralPath $flatManifestPath).ModuleVersion ) if ($flatPesterVersion.Major -ne $preservedPesterMajorVersion) { $pesterDirectory } continue } foreach ($pesterEntry in @(Get-ChildItem -LiteralPath $pesterDirectory -Force)) { $parsedVersion = $null $isPreservedVersion = $pesterEntry.PSIsContainer -and [version]::TryParse($pesterEntry.Name, [ref]$parsedVersion) -and $parsedVersion.Major -eq $preservedPesterMajorVersion if (-not $isPreservedVersion) { $pesterEntry.FullName } } } } ) if ($remainingPaths.Count -gt 0) { $message = "Unable to remove project dependencies: $($remainingPaths -join ', ')" if (-not $AllowPreinstalledModules) { throw $message } Write-Warning $message } } [pscustomobject][ordered]@{ ProjectRoot = $ProjectRoot ModuleNames = @($moduleNames) ModuleRoots = @($moduleRoots) RemovedPaths = @($removedPaths) PreservedPaths = @($preservedPaths | Sort-Object -Unique) FailedPaths = @($failedPaths | Sort-Object -Unique) PreservedPesterMajorVersion = $preservedPesterMajorVersion } } |