context/setup
I'm working on a rather large build automation tool that is made of 90% PowerShell code.
A lot of the funcitonality is grouped in .psm1 files that load each other by using
Import-Module "$PSScriptRoot/AnotherModule.psm1" -Force
this seemed to be a good idea for developing purposes / testing code etc, but I recently stumbled upon a problem with this approach.
finding/problem
whenever a loaded module force-imports another module that had previously been loaded in the parent/outer scope, it not longer is accessible in this parent/outer scope
now I'm running into a problem, where importing required modules at the beginning of script does not guarantee that the imported modules are actually available during script execution, as one of the imported modules may 'hide' another, previously loaded module :-/
example
let's say we've got three modules: ModA, ModB, ModC:
ModA:
$ErrorActionPreference = "Stop"
Import-Module "$PSScriptRoot\ModB.psm1" -Force
// module A entrails
ModB:
$ErrorActionPreference = "Stop"
// module B entrails
ModC:
$ErrorActionPreference = "Stop"
Import-Module "$PSScriptRoot\ModA.psm1" -Force
// module C entrails
following pester tests demonstrate this 'module hiding' behavior:
Describe 'powershell module system tests' {
# ModA -> Import ModB -Force
# ModC -> Import ModA -Force
$mod_path = Join-Path $PSScriptRoot "testmod"
$mods = @("ModA", "ModB", "ModC")
It "cleanup leftovers from previous tests" {
Remove-Module $mods -ErrorAction SilentlyContinue
}
It 'is able to import ModA, which internally loads module B' {
Import-Module "$mod_path\ModA.psm1" -Force
Get-Module ModA | Should -Not -BeNullOrEmpty
Get-Module ModB | Should -BeNullOrEmpty
}
It 'is able to import a second module already imported by the first one' {
Get-Module ModA | Should -Not -BeNullOrEmpty
Import-Module "$mod_path\ModB.psm1" -Force
Get-Module ModB | Should -Not -BeNullOrEmpty
}
It 'still knows the first module' {
Get-Module ModA | Should -Not -BeNullOrEmpty
}
It 'is able to import a thid module that also imports the first one' {
Get-Module ModC | Should -BeNullOrEmpty
Import-Module "$mod_path\ModC.psm1" -Force
Get-Module ModC | Should -Not -BeNullOrEmpty
}
It 'does not know A or B any longer, as it has been hidden by C' {
Get-Module ModA | Should -BeNullOrEmpty
Get-Module ModB | Should -BeNullOrEmpty
}
It 'still knows C' {
Get-Module ModC | Should -Not -BeNullOrEmpty
}
It 'cleanup leftovers from this test' {
Remove-Module $mods
}
}
tracing the bugs
in order to pinpoint the issue(s), I've tried to come up with another pester test that analyzes the codebase and verifies no 'already loaded' module is removed from the global module table when importing another one:
Describe 'verify tool module system hierachy' {
$tool_modules = Get-ChildItem $script:engine_path -Filter "*.psm1"
function UNLoadAllToolModules {
Remove-Module 'Tool-*'
}
function LoadAllToolModules {
$tool_modules | Foreach-Object { Import-Module $_.FullName }
}
It "cleanup leftovers from previous tests" {
UNLoadAllToolModules
}
It "loading of a module does not hide an already loaded module" {
$errMods = $()
$tool_modules | ForEach-Object {
LoadAllToolModules
$pre = Get-Module
$mod = $_
Import-Module $mod.FullName -Force
$post = Get-Module
$mods = Compare-Object $post $pre | Select-Object -Expand InputObject | Select-Object -Expand Name
$mods | Foreach-Object {
"import of '$mod' hides '$_'" | Write-Host
}
if ($mods) {
$errMods += $mod.Name
}
}
$errMods | Should -BeNullOrEmpty
}
}
question
is there a way to print / debug the global module table, as depicted here, so I can figure out which module is loaded multiple times / how the module tree gets modified after an import?
solution?
currently, the only way around this 'issue' (let beside that it's probably a bad design choice to -force import modules in other modules) is to either drop the -force or use -global whenever calling Import-Module -Force on a module contained in this build automation suite/tool.