Modules/businessdev.ALbuild.Feeds/Public/Resolve-BcDependencies.ps1

function Resolve-BcDependencies {
    <#
    .SYNOPSIS
        Resolves an AL project's dependencies across feeds and writes them to .alpackages.
 
    .DESCRIPTION
        Reads the project's app.json, resolves a mutually-compatible, transitively-complete set of
        dependency packages from the given (or registered) feeds - respecting the target build's
        platform/application versions and any apps already installed in the build (pinned, e.g.
        Microsoft first-party apps) - then downloads them in dependency order and writes a
        dependencies.lock.json for reproducible builds.
 
    .PARAMETER ProjectFolder
        The AL project folder (contains app.json).
 
    .PARAMETER Feeds
        Feed provider objects. When omitted, uses the feeds registered with Register-BcFeed plus any
        feeds declared in the project's albuild.json ('feeds' array) - so a pipeline can simply call
        Resolve-BcDependencies without repeating Register-BcFeed.
 
    .PARAMETER WorkspaceRoot
        Workspace root holding the shared albuild.json (merged under the project-folder config) when
        auto-loading feeds from configuration. Ignored when -Feeds is supplied.
 
    .PARAMETER InstalledApps
        Apps already present in the target build (objects with AppId/Id and Version), pinned as a
        baseline. Typically Get-BcContainerAppInfo output.
 
    .PARAMETER TargetApplication
        Target application version. Defaults to app.json 'application'.
 
    .PARAMETER TargetPlatform
        Target platform version. Defaults to app.json 'platform'.
 
    .PARAMETER Select
        Latest (default; the newest version compatible with the target build) or LowestCompatible
        (the oldest version that still satisfies every constraint, for maximally reproducible builds).
 
    .PARAMETER OutputFolder
        Where to place resolved .app files. Default: <ProjectFolder>/.alpackages.
 
    .PARAMETER LockFilePath
        Where to write the lock file. Default: <ProjectFolder>/dependencies.lock.json.
 
    .PARAMETER SkipDownload
        Resolve and write the lock file without downloading packages.
 
    .EXAMPLE
        Resolve-BcDependencies -ProjectFolder ./app -InstalledApps (Get-BcContainerAppInfo -Name bld)
 
    .OUTPUTS
        PSCustomObject with Resolved, OutputFolder, LockFile, Packages.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '',
        Justification = 'Resolves the full set of project dependencies; the plural noun is the intended, descriptive name.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ProjectFolder,
        [object[]] $Feeds,
        [string] $WorkspaceRoot,
        [object[]] $InstalledApps = @(),
        [string] $TargetApplication = '',
        [string] $TargetPlatform = '',
        [ValidateSet('LowestCompatible', 'Latest')] [string] $Select = 'Latest',
        [string] $OutputFolder,
        [string] $LockFilePath,
        [switch] $SkipDownload
    )

    $appJsonPath = Join-Path $ProjectFolder 'app.json'
    if (-not (Test-Path -LiteralPath $appJsonPath)) { throw "No app.json found in '$ProjectFolder'." }
    $appJson = Get-Content -LiteralPath $appJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json

    if (-not $Feeds -or $Feeds.Count -eq 0) {
        # Auto-load feeds declared in the project's albuild.json (idempotent by name), then use the
        # full registry. Explicit -Feeds always wins; this only runs when none were supplied.
        [void](Register-BcFeed -FromProjectConfig $ProjectFolder -WorkspaceRoot $WorkspaceRoot)
        $Feeds = @(Get-BcFeed)
    }
    if (-not $Feeds -or $Feeds.Count -eq 0) { throw "No feeds available. Declare a 'feeds' array in albuild.json, register one with Register-BcFeed, or pass -Feeds." }

    $hasProp = { param($obj, $name) $obj.PSObject.Properties.Name -contains $name }

    if (-not $TargetApplication -and (& $hasProp $appJson 'application')) { $TargetApplication = "$($appJson.application)" }
    if (-not $TargetPlatform -and (& $hasProp $appJson 'platform')) { $TargetPlatform = "$($appJson.platform)" }

    # Pinned baseline from the target build.
    $pinned = @{}
    foreach ($app in $InstalledApps) {
        $id = if (& $hasProp $app 'AppId') { "$($app.AppId)" } elseif (& $hasProp $app 'Id') { "$($app.Id)" } else { '' }
        if ($id) { $pinned[$id] = "$($app.Version)" }
    }

    # Root dependencies from app.json.
    $roots = @()
    if (& $hasProp $appJson 'dependencies') {
        foreach ($dep in @($appJson.dependencies)) {
            $id = if (& $hasProp $dep 'id') { "$($dep.id)" } elseif (& $hasProp $dep 'appId') { "$($dep.appId)" } else { '' }
            if (-not $id) { continue }
            $min = if (& $hasProp $dep 'version') { "$($dep.version)" } elseif (& $hasProp $dep 'minVersion') { "$($dep.minVersion)" } else { '0.0.0.0' }
            $name = if (& $hasProp $dep 'name') { "$($dep.name)" } else { '' }
            $publisher = if (& $hasProp $dep 'publisher') { "$($dep.publisher)" } else { '' }
            $roots += [PSCustomObject]@{ Id = $id; Name = $name; Publisher = $publisher; MinVersion = $min }
        }
    }

    $appName = if (& $hasProp $appJson 'name') { "$($appJson.name)" } else { Split-Path -Path $ProjectFolder -Leaf }
    $feedNames = @($Feeds | ForEach-Object { $_.Name }) -join ', '
    Write-ALbuildLog -Level Information "Resolving dependencies for '$appName' (target application '$TargetApplication', platform '$TargetPlatform', strategy $Select)."
    Write-ALbuildLog -Level Information "$($roots.Count) direct dependency(ies) across $(@($Feeds).Count) feed(s): $feedNames."

    $graph = Resolve-BcDependencyGraph -RootDependencies $roots -Pinned $pinned -Providers $Feeds `
        -TargetApplication $TargetApplication -TargetPlatform $TargetPlatform -Select $Select

    $resolvedCount = @($graph.Resolved).Count
    Write-ALbuildLog -Level Information "Resolved a compatible set of $resolvedCount package(s)$(if ($SkipDownload) { '; download skipped.' } else { '; downloading...' })"

    if (-not $OutputFolder) { $OutputFolder = Join-Path $ProjectFolder '.alpackages' }
    if (-not $LockFilePath) { $LockFilePath = Join-Path $ProjectFolder 'dependencies.lock.json' }
    if (-not $SkipDownload -and -not (Test-Path -LiteralPath $OutputFolder)) {
        New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
    }

    $packages = @()
    $position = 0
    foreach ($item in $graph.Resolved) {
        $position++
        $fileName = $null
        # Indirect (metapackage) candidates download their runtime sub-package build, not the app
        # package itself; EffectiveDownload* resolves to the right coordinates for either shape.
        $downloadId = $item.EffectiveDownloadId()
        $downloadVersion = $item.EffectiveDownloadVersion()
        $label = if ($item.Name) { $item.Name } else { $item.Id }
        if (-not $SkipDownload -and $item.Provider) {
            Write-ALbuildLog -Level Information " [$position/$resolvedCount] Downloading $label $downloadVersion (feed '$($item.ProviderName)')..."
            $downloaded = $item.Provider.DownloadPackage($downloadId, $downloadVersion, $OutputFolder)
            $fileName = Split-Path -Path $downloaded -Leaf
        }
        $packages += [PSCustomObject]@{
            name            = $item.Name
            id              = $item.Id
            version         = $item.Version
            kind            = $item.Kind
            packageId       = $item.PackageId
            downloadPackage = $downloadId
            downloadVersion = $downloadVersion
            feed            = $item.ProviderName
            file            = $fileName
        }
    }

    $lock = [PSCustomObject]@{
        generated = (Get-Date -Format 'o')
        target    = [PSCustomObject]@{ application = $TargetApplication; platform = $TargetPlatform }
        packages  = $packages
    }
    $lock | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $LockFilePath -Encoding UTF8
    Write-ALbuildLog -Level Success "Resolved $($packages.Count) dependency package(s); lock file written to '$LockFilePath'."

    return [PSCustomObject]@{
        Resolved     = $graph.Resolved
        OutputFolder = $OutputFolder
        LockFile     = $LockFilePath
        Packages     = $packages
    }
}