Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcRuntimeProductOrder.ps1
|
function Get-BcRuntimeProductOrder { <# .SYNOPSIS Orders catalogue products so every product comes after the products it depends on. .DESCRIPTION Inside one container the products are installed in sequence, and a product cannot be installed before the products it depends on. Several 365 business products depend on other products in the same catalogue (Extension License, 365 business API), so the order is a correctness requirement, not a preference. The sort is a depth-first topological sort that is STABLE: products with no ordering constraint between them keep their input order. That keeps the plan - and therefore the log, the per-app results and the run-to-run diff - reproducible instead of reshuffling on every run. A dependency cycle cannot be ordered. Rather than throwing (which would take down a whole catalogue run over one bad manifest) the products in the cycle are appended in input order and the caller is warned: the run then fails for those products only, at install time, with the real error from the service tier. Names in DependsOn that are not in the catalogue are ignored on purpose - a product may depend on a Microsoft app or on a third-party dependency resolved from a feed, neither of which this function has an opinion about. .PARAMETER Product Catalogue entries with Name and optional DependsOn (names of other entries). .OUTPUTS System.Object[] - the same objects, ordered. #> [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object[]] $Product ) $byName = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($p in $Product) { $byName["$($p.Name)"] = $p } $ordered = [System.Collections.Generic.List[object]]::new() $state = @{} # name -> 'visiting' | 'done' $cycles = [System.Collections.Generic.List[string]]::new() $visit = { param([string] $Name) if ($state[$Name] -eq 'done') { return } if ($state[$Name] -eq 'visiting') { if (-not $cycles.Contains($Name)) { $cycles.Add($Name) } return } $state[$Name] = 'visiting' $entry = $byName[$Name] $dependsOn = @(Get-BcRuntimeProperty -InputObject $entry -Name 'DependsOn' -Default @()) foreach ($dep in $dependsOn) { $depName = "$dep" if ([string]::IsNullOrWhiteSpace($depName)) { continue } if (-not $byName.Contains($depName)) { continue } # external dependency - not ours to order & $visit $depName } $state[$Name] = 'done' $ordered.Add($entry) } foreach ($p in $Product) { & $visit "$($p.Name)" } if ($cycles.Count -gt 0) { Write-ALbuildLog -Level Warning ("Dependency cycle between catalogue products: $($cycles -join ', '). " + 'They are ordered as given; an install that genuinely needs the missing dependency will fail for those products only.') } return $ordered.ToArray() } |