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 } } |