Modules/businessdev.ALbuild.OnPrem/Private/Import-BcManagementShell.ps1
|
function Import-BcManagementShell { <# .SYNOPSIS Loads the Business Central administration shell (Publish-NAVApp / Sync-NAVApp / Install-NAVApp / Get-NAVAppInfo / ...) into the current session. .DESCRIPTION The on-prem cmdlets are provided by the BC server's management assemblies, which are NOT on PSModulePath and are therefore not auto-loaded. Without this, an on-prem publish fails with "The term 'Publish-NAVApp' is not recognized". Mirrors the AL.build V1 ReleaseOnPrem behaviour: if the shell is already present it is reused; otherwise the management assemblies are located under the BC install tree and imported (modern 'Microsoft.BusinessCentral.*' first, then the legacy 'Microsoft.Dynamics.Nav.*' names for older versions). Progress is logged in detail (every search root, every assembly imported and from where) so a failure on a customer agent is diagnosable from the pipeline log alone. .PARAMETER SentinelCommand The command whose presence proves the shell is loaded. Default 'Publish-NAVApp' (the cmdlet the on-prem publish actually needs). .PARAMETER SearchRoot Glob root(s) under which to search for the management assemblies. .OUTPUTS None. Throws a clear, actionable error listing the searched locations if the shell cannot be loaded. #> [CmdletBinding()] param( [string] $SentinelCommand = 'Publish-NAVApp', [string[]] $SearchRoot = @( 'C:\Program Files\Microsoft Dynamics*\*\Service\', 'C:\Program Files (x86)\Microsoft Dynamics*\*\Service\' ) ) if (Get-Command -Name $SentinelCommand -ErrorAction SilentlyContinue) { Write-ALbuildLog "BC management shell already loaded ('$SentinelCommand' available); reusing it." return } Write-ALbuildLog "BC management shell not loaded ('$SentinelCommand' missing); locating the management assemblies..." # Modern (BC15+) assemblies first, then the legacy NAV names for older on-prem versions. Each set # provides the server (…Management) + app (…Apps.Management) cmdlets we call. $moduleSets = @( @('Microsoft.BusinessCentral.Management.dll', 'Microsoft.BusinessCentral.Apps.Management.dll'), @('Microsoft.Dynamics.Nav.Management.dll', 'Microsoft.Dynamics.Nav.Apps.Management.dll') ) # A per-assembly import failure is EXPECTED and harmless: on a modern BC server under Windows # PowerShell 5.1 the modern Microsoft.BusinessCentral.* assemblies target .NET 8 and fail to load # ("System.Runtime, Version=8.0.0.0 ... not found"), then the legacy Microsoft.Dynamics.Nav.* set # loads fine. So the search is quiet (Verbose): attempts and failures are recorded, not warned, and # surfaced only in the throw if NOTHING yields the sentinel cmdlet. Only that final state is an error. $searched = [System.Collections.Generic.List[string]]::new() $loadErrors = [System.Collections.Generic.List[string]]::new() $importedAny = $false foreach ($set in $moduleSets) { foreach ($dll in $set) { foreach ($root in $SearchRoot) { # String-concatenate (roots end in a separator) rather than Join-Path: Join-Path # resolves the 'C:' qualifier and throws "drive 'C' does not exist" on non-Windows # agents (the module is loaded/tested on the Linux CI leg, though it only runs for real # on a Windows BC server). $searched.Add($root + $dll) $found = @() # NOTE: do NOT add -File here. With a wildcard '...\*\Service\' path + -Recurse, -File makes # Get-ChildItem return nothing (PowerShell quirk), so the assemblies are never found - which # is exactly why V1/the reference installer omit it. Filter out directories in the loop instead. try { $found = @(Get-ChildItem -Path $root -Filter $dll -Recurse -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer }) } catch { $found = @() } # a Windows-only root throws 'drive not found' off Windows; treat as no match foreach ($f in $found) { Write-ALbuildLog -Level Verbose " trying '$($f.Name)' from '$($f.Directory.FullName)'." try { Import-Module -Name $f.FullName -Force -Global -DisableNameChecking -ErrorAction Stop $importedAny = $true } catch { # Expected for the modern set on PS 5.1; recorded (not warned) in case the whole load fails. $loadErrors.Add("$($f.FullName): $($_.Exception.Message)") Write-ALbuildLog -Level Verbose " '$($f.Name)' did not load: $($_.Exception.Message)" } } } } # Stop at the first assembly set that yields the cmdlet we need. if (Get-Command -Name $SentinelCommand -ErrorAction SilentlyContinue) { break } } if (-not (Get-Command -Name $SentinelCommand -ErrorAction SilentlyContinue)) { $hint = if ($importedAny) { "Assemblies were found but none exposed '$SentinelCommand' - the installed BC version may use different module names, or every candidate failed to load." } else { 'No BC management assemblies were found under the search roots - the BC server (or its management tools) does not appear to be installed on this agent.' } $detail = "Could not load the Business Central management shell. $hint" $detail += [Environment]::NewLine + "Searched: $(($searched | Select-Object -Unique) -join '; ')" if ($loadErrors.Count -gt 0) { $detail += [Environment]::NewLine + "Load errors: " + ($loadErrors -join ' | ') } throw $detail } Write-ALbuildLog -Level Success "BC management shell loaded ('$SentinelCommand' now available)." } |