Modules/businessdev.ALbuild.Core/Public/Get-BcProjectBuildOrder.ps1
|
function Get-BcProjectBuildOrder { <# .SYNOPSIS Discovers the AL projects under a repository root and orders them by inter-project dependency. .DESCRIPTION Finds every app.json under -Path (recursively, excluding symbol/package/output folders), reads each project's id/name/publisher/version and its declared dependency ids, drops any project whose folder leaf name is listed in -ExcludeProjects, then returns the projects topologically sorted so that a project always comes after the in-repo projects it depends on (e.g. 'app' before 'test'/'migration'). Dependencies on apps that are not part of the repository are ignored for ordering - they come from feeds or are already in the container. Throws on a dependency cycle. This is the single source of "which projects, in what order" for the multi-root build: the ResolveDependencies, CompileApp and PublishApp tasks all walk this list. .PARAMETER Path Repository root to search, a single app folder, or a single app.json. Default: current location. .PARAMETER ExcludeProjects Folder leaf names to exclude from the build (case-insensitive), e.g. @('migration'). .EXAMPLE Get-BcProjectBuildOrder -Path . -ExcludeProjects 'migration' .OUTPUTS PSCustomObject per project, in build order: Folder, Name, Id, Publisher, Version, AppJsonPath, RepoDependencyIds. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Position = 0)] [string] $Path = (Get-Location).Path, [string[]] $ExcludeProjects = @() ) if (-not (Test-Path -LiteralPath $Path)) { throw "Path '$Path' does not exist." } $item = Get-Item -LiteralPath $Path if ($item.PSIsContainer) { $appJsonFiles = @(Get-ChildItem -LiteralPath $item.FullName -Filter 'app.json' -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '[\\/](\.alpackages|\.altemplates|\.snapshots|\.output)[\\/]' }) } elseif ($item.Name -eq 'app.json') { $appJsonFiles = @($item) } else { throw "Path '$Path' is neither a folder nor an app.json file." } $hasProp = { param($obj, $name) $null -ne $obj -and ($obj.PSObject.Properties.Name -contains $name) } # Read each project's manifest into a descriptor. $projects = foreach ($file in $appJsonFiles) { try { $json = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Write-ALbuildLog -Level Warning "Ignoring invalid app.json '$($file.FullName)': $($_.Exception.Message)." continue } $depIds = @() if (& $hasProp $json 'dependencies') { foreach ($dep in @($json.dependencies)) { $id = if (& $hasProp $dep 'id') { "$($dep.id)" } elseif (& $hasProp $dep 'appId') { "$($dep.appId)" } else { '' } if ($id) { $depIds += $id.ToLowerInvariant() } } } [PSCustomObject]@{ Folder = $file.Directory.FullName Leaf = $file.Directory.Name Name = if (& $hasProp $json 'name') { "$($json.name)" } else { $file.Directory.Name } Id = if (& $hasProp $json 'id') { "$($json.id)".ToLowerInvariant() } else { '' } Publisher = if (& $hasProp $json 'publisher') { "$($json.publisher)" } else { '' } Version = if (& $hasProp $json 'version') { "$($json.version)" } else { '' } AppJsonPath = $file.FullName DependencyIds = $depIds RepoDependencyIds = @() } } $projects = @($projects) # Apply exclusions by folder leaf name (case-insensitive; -contains is case-insensitive for strings). if ($ExcludeProjects -and $ExcludeProjects.Count -gt 0) { $projects = @($projects | Where-Object { $ExcludeProjects -notcontains $_.Leaf }) } if ($projects.Count -eq 0) { return @() } # Restrict each project's dependencies to the ones that are themselves projects in this repo. $idToProject = @{} foreach ($p in $projects) { if ($p.Id) { $idToProject[$p.Id] = $p } } foreach ($p in $projects) { $p.RepoDependencyIds = @($p.DependencyIds | Where-Object { $idToProject.ContainsKey($_) -and $_ -ne $p.Id }) } # Topological sort: emit a project once all of its in-repo dependencies have been emitted. $doneIds = [System.Collections.Generic.HashSet[string]]::new() $ordered = [System.Collections.Generic.List[object]]::new() $remaining = [System.Collections.Generic.List[object]]::new() foreach ($p in $projects) { [void]$remaining.Add($p) } while ($remaining.Count -gt 0) { $progressed = $false foreach ($p in @($remaining)) { $unmet = @($p.RepoDependencyIds | Where-Object { -not $doneIds.Contains($_) }) if ($unmet.Count -eq 0) { [void]$ordered.Add($p) if ($p.Id) { [void]$doneIds.Add($p.Id) } [void]$remaining.Remove($p) $progressed = $true } } if (-not $progressed) { throw "Cyclic inter-project dependency among: $(@($remaining | ForEach-Object { $_.Leaf }) -join ', ')." } } return @($ordered | Select-Object Folder, Name, Id, Publisher, Version, AppJsonPath, RepoDependencyIds) } |