Modules/businessdev.ALbuild.Feeds/Public/Resolve-BcExternalDependency.ps1
|
function Resolve-BcExternalDependency { <# .SYNOPSIS Stages dependency apps that are not resolved from a NuGet feed (Azure DevOps Universal feed packages and committed local .app files) into a folder, and returns their identities. .DESCRIPTION Complements the NuGet dependency resolver for sources it cannot query by app id: * UniversalPackages - named packages downloaded from an Azure DevOps Universal feed via Get-BcUniversalPackage (e.g. your own apps published to a 'Products' feed). * LocalPackage - repository folders of committed .app files, for ISVs without any public feed (e.g. CKL). Relative folders are resolved against -BaseFolder. All staged .app files land in -OutputFolder; the returned identities (read with Expand-BcAppFile) let the caller pin them as a satisfied baseline for NuGet resolution and install them into the container alongside the feed-resolved apps. Missing local folders are skipped with a warning; symbol-only files are kept (the caller decides installability). .PARAMETER UniversalPackages Universal feed entries, each an object/hashtable: { feed, name, version?, organization?, project? }. 'organization' falls back to -Organization; 'version' defaults to '*'. .PARAMETER LocalPackage Folders of committed .app files (relative paths resolved against -BaseFolder). .PARAMETER OutputFolder Folder the packages are staged into (created if missing). .PARAMETER Organization Default Azure DevOps organization (URL or name) for Universal entries without their own. .PARAMETER Project Default project for project-scoped Universal feeds. .PARAMETER AccessToken PAT for the Universal download (exported as AZURE_DEVOPS_EXT_PAT by Get-BcUniversalPackage). .PARAMETER BaseFolder Root used to resolve relative -LocalPackage folders. Default: current location. .OUTPUTS PSCustomObject per staged app: Id, Name, Publisher, Version, File. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Resolves the full set of external dependency packages; the plural is intentional.')] [CmdletBinding()] [OutputType([PSCustomObject])] param( [object[]] $UniversalPackages = @(), [string[]] $LocalPackage = @(), [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputFolder, [string] $Organization, [string] $Project, [string] $AccessToken, [string] $BaseFolder = (Get-Location).Path ) if (-not (Test-Path -LiteralPath $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } $field = { param($source, [string] $name) if ($null -eq $source) { return $null } $prop = $source.PSObject.Properties | Where-Object { $_.Name -ieq $name } | Select-Object -First 1 if ($prop) { return $prop.Value } return $null } foreach ($entry in @($UniversalPackages)) { $feed = & $field $entry 'feed' $name = & $field $entry 'name' if (-not $feed -or -not $name) { Write-ALbuildLog -Level Warning "Skipping a universalPackages entry without 'feed'/'name'." continue } $org = & $field $entry 'organization'; if (-not $org) { $org = $Organization } if (-not $org) { throw "Universal package '$name' has no 'organization' and no default was provided." } $proj = & $field $entry 'project'; if (-not $proj) { $proj = $Project } $version = & $field $entry 'version'; if (-not $version) { $version = '*' } $packageArgs = @{ Organization = $org; Feed = $feed; Name = $name; Version = $version; OutputFolder = $OutputFolder } if ($proj) { $packageArgs['Project'] = $proj } if ($AccessToken) { $packageArgs['AccessToken'] = $AccessToken } # Non-fatal: if a universal package is missing or access is denied, fall through to the local # package(s) instead of failing the resolve. Local packages are the availability fallback. try { Get-BcUniversalPackage @packageArgs | Out-Null } catch { Write-ALbuildLog -Level Warning "Universal package '$name' (feed '$feed') unavailable: $($_.Exception.Message). Falling back to local/other sources." } } foreach ($folder in @($LocalPackage)) { if ([string]::IsNullOrWhiteSpace($folder)) { continue } # [IO.Path]::Combine / [IO.Directory]::Exists (not Join-Path / Test-Path) so a drive-qualified # path on a foreign OS - e.g. a Windows 'C:\...' folder evaluated on Linux - is combined and # reported missing instead of throwing a terminating DriveNotFoundException (both Join-Path and # Test-Path resolve the drive qualifier) under $ErrorActionPreference = 'Stop'. $resolved = if ([System.IO.Path]::IsPathRooted($folder)) { $folder } else { [System.IO.Path]::Combine($BaseFolder, $folder) } if (-not [System.IO.Directory]::Exists($resolved)) { Write-ALbuildLog "Local dependency folder '$resolved' not found; skipping." continue } $apps = @(Get-ChildItem -LiteralPath $resolved -Filter '*.app' -File -Recurse -ErrorAction SilentlyContinue) foreach ($app in $apps) { Copy-Item -LiteralPath $app.FullName -Destination $OutputFolder -Force } Write-ALbuildLog "Staged $($apps.Count) local dependency package(s) from '$resolved'." } # Read every staged package, then keep only the newest version per app id - several sources may # serve the same app (e.g. universal + local). Files for superseded versions are removed from the # staging folder so only the chosen one is pinned and installed. $byId = @{} $loose = [System.Collections.Generic.List[object]]::new() # packages without a readable id foreach ($file in (Get-ChildItem -LiteralPath $OutputFolder -Filter '*.app' -File -ErrorAction SilentlyContinue)) { try { $info = Expand-BcAppFile -Path $file.FullName } catch { Write-ALbuildLog -Level Warning "Could not read staged package '$($file.Name)': $($_.Exception.Message)"; continue } finally { if ($info -and $info.ExtractedPath -and (Test-Path -LiteralPath $info.ExtractedPath)) { Remove-Item -LiteralPath $info.ExtractedPath -Recurse -Force -ErrorAction SilentlyContinue } } $entry = [PSCustomObject]@{ Id = $info.Id; Name = $info.Name; Publisher = $info.Publisher; Version = $info.Version; File = $file.FullName } if (-not $info.Id) { $loose.Add($entry); continue } $id = "$($info.Id)".ToLowerInvariant() if (-not $byId.ContainsKey($id)) { $byId[$id] = $entry; continue } $kept = $byId[$id] if ((ConvertTo-BcVersion $entry.Version) -gt (ConvertTo-BcVersion $kept.Version)) { Write-ALbuildLog "Superseding '$($kept.Name)' $($kept.Version) with newer $($entry.Version) from another source." Remove-Item -LiteralPath $kept.File -Force -ErrorAction SilentlyContinue $byId[$id] = $entry } else { if ($entry.File -ne $kept.File) { Remove-Item -LiteralPath $entry.File -Force -ErrorAction SilentlyContinue } } } return @(@($byId.Values) + @($loose)) } |