Modules/businessdev.ALbuild.RuntimePackages/Public/Build-BcProductRuntimePackages.ps1
|
function Build-BcProductRuntimePackages { <# .SYNOPSIS Builds every outstanding runtime package of ONE product, from AL source, without a container. .DESCRIPTION The per-product entry point for the two ways runtime packages are produced: as the last stages of a product's release run, and as one stage of the weekend sweep that fills in the platform versions Microsoft has released since. It runs SERIALLY and in the caller's own runspace, deliberately. The container route needed a worker pool because 78-94 % of a platform version was provisioning; emitting from source costs ~9 s (measured: 5.8-14.5 s across BC 17-29), so there is nothing left to hide behind parallelism. What that buys is worth more than the seconds: the Azure context stays usable (it is not runspace-safe), the log is in order without tailing anything, and a failure is attributable without replaying a worker file. Dependencies arrive as SYMBOLS, taken from the NuGet feed and then blob storage - never recompiled here. A runtime package is the correct shape for that and it is measured, not assumed: a probe app that declares AND uses Extension License compiled to the same bytes whether its symbol came from the dependency's runtime package or its normal .app. It is also correct by construction, which a normal .app is not - see Save-BcRuntimeDependencyPackage. A platform version whose chain cannot be assembled is SKIPPED, not failed, and says exactly which dependency was missing and where it was looked for. That is the honest outcome: on a BC version Microsoft released yesterday the dependency simply has no package yet, and the sweep - where the dependency's own stage runs first - fills it in. .PARAMETER Product The product: Name, Publisher, AppId, AppVersion, Projects[], and DependencyChain[] (deepest first, each with Name, Publisher, AppId, AppVersion). .PARAMETER PlatformArtifact The platform versions to build, each { PlatformVersion; ArtifactUrl }. .PARAMETER WorkRoot Working folder for stamped project copies and chain symbols. .PARAMETER OutputFolder Root of the '<appId>/<platformVersion>/' output layout the blob and the feed both mirror. .PARAMETER Country Localisation of the artifacts. Part of the output identity, not decoration. .PARAMETER Feed Runtime feed from Register-BcFeed (-Kind runtime): where chain symbols are looked for first, and what -SkipPublished asks about what already exists. .PARAMETER BlobContext Azure storage context for the legacy chain-symbol fallback. .PARAMETER BlobContainerName Blob container holding '<appId>/<platformVersion>/<file>'. .PARAMETER Signing Splat for Invoke-BcAppSigning applied to each produced package. Omitted = unsigned. .PARAMETER SkipPublished Ask the feed which platform versions this app version already has, and skip those. This is the idempotency the sweep needs: it re-plans from what exists rather than from a queue, so a missed run heals on the next one. .PARAMETER OnVersionComplete Called with the result of each finished platform version, before the next one starts. This is where the caller uploads to blob storage and pushes to the feed - so an interrupted run keeps everything it had already produced. .PARAMETER JUnitPath Where to write the JUnit results (one test case per platform version). .PARAMETER SummaryPath Where to write the Markdown summary. .PARAMETER IssuePath Where to write the by-cause JUnit report. .PARAMETER NoAzureDevOpsLogging Suppress '##vso[...]' commands. Set by tests, which otherwise annotate the job running them. .OUTPUTS PSCustomObject with Produced, Failed, Skipped and Results[] - Results in the same shape the factory returned, so the report sections, the JUnit and the by-cause XML all read unchanged. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Builds many packages in one invocation; the plural is the point.')] [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNull()] [object] $Product, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $PlatformArtifact, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkRoot, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputFolder, [string] $Country = '', [object] $Feed, [object] $BlobContext, [string] $BlobContainerName, [hashtable] $Signing, [switch] $SkipPublished, [scriptblock] $OnVersionComplete, [string] $JUnitPath, [string] $SummaryPath, [string] $IssuePath, [switch] $NoAzureDevOpsLogging ) Assert-ALbuildLicensed -Feature 'RuntimePackages' $name = "$($Product.Name)" $appVersion = "$($Product.AppVersion)" $chain = @(Get-BcRuntimeProperty -InputObject $Product -Name 'DependencyChain' -Default @()) $targets = @($PlatformArtifact) foreach ($folder in @($WorkRoot, $OutputFolder)) { if (-not (Test-Path -LiteralPath $folder)) { New-Item -ItemType Directory -Force -Path $folder | Out-Null } } Write-Host "##[section]$name $appVersion - runtime packages from source" Write-ALbuildLog "Product: $name $appVersion (app id $($Product.AppId))." Write-ALbuildLog "Platform versions requested: $($targets.Count)$(if ($Country) { " for country '$Country'" })." Write-ALbuildLog "Dependency chain: $(if ($chain.Count) { ($chain | ForEach-Object { "$($_.Name) $($_.AppVersion)" }) -join ' -> ' } else { 'none' })." Write-ALbuildLog "Output: $OutputFolder" # ---------------------------------------------------------------- what already exists $published = @() if ($SkipPublished -and $Feed) { $ownId = Get-BcRuntimeFeedPackageId -Publisher "$($Product.Publisher)" -Name $name -AppVersion $appVersion try { $published = @(Get-BcPackageVersion -Feed $Feed -PackageId $ownId | ForEach-Object { @($_.Versions) } | Where-Object { $_ }) Write-ALbuildLog "Feed '$ownId' already carries $($published.Count) platform version(s); those are skipped." } catch { # Deliberately not fatal, and deliberately loud: building a package that already exists is # wasteful but harmless, while refusing to build because the feed was unreachable is not. Write-ALbuildLog -Level Warning ("Could not ask the feed what already exists ($($_.Exception.Message)); " + 'every requested platform version will be built.') } } elseif ($SkipPublished) { Write-ALbuildLog -Level Warning '-SkipPublished was given without a feed; nothing can be skipped.' } $results = [System.Collections.Generic.List[object]]::new() $chainRoot = Join-Path $WorkRoot 'chainsym' if (-not $PSCmdlet.ShouldProcess("$name ($($targets.Count) platform version(s))", 'Build runtime packages')) { return [PSCustomObject]@{ Produced = 0; Failed = 0; Skipped = 0; Results = @() } } Write-Host '##[section]Build progress' foreach ($target in $targets) { $pv = "$($target.PlatformVersion)" $started = Get-Date $row = [PSCustomObject]@{ Name = $name; Status = 'Succeeded'; Seconds = 0; Error = $null; File = $null } if ($published -contains $pv) { $row.Status = 'Skipped' $row.Error = 'Already published for this platform version.' Write-Host (" {0,-34} skipped already published" -f "BC $pv") $results.Add((New-BcRuntimeVersionResult -PlatformVersion $pv -Country $Country -Row $row -Started $started)) continue } try { # The artifact carries the compiler that must emit this package: a BC 26 runtime package is # emitted by BC 26's compiler, so this is not interchangeable. $artifact = Get-BcArtifact -ArtifactUrl "$($target.ArtifactUrl)" $symbolFolders = @(Get-BcArtifactSymbolFolder -Artifact $artifact) # The chain gets a folder per platform version - never the shared one, or a product would # find symbols built for a different platform and compile against them. $chainFolder = Join-Path $chainRoot "$pv$(if ($Country) { "-$($Country.ToLowerInvariant())" })" $missing = @() foreach ($dependency in $chain) { $got = Save-BcRuntimeDependencyPackage -Publisher "$($dependency.Publisher)" -Name "$($dependency.Name)" ` -AppId "$($dependency.AppId)" -AppVersion "$($dependency.AppVersion)" -PlatformVersion $pv ` -OutputFolder $chainFolder -Feed $Feed -BlobContext $BlobContext -BlobContainerName $BlobContainerName if (-not $got.Found) { $missing += $got.Reason } } if ($missing.Count -gt 0) { # SKIPPED, not failed. On a platform version Microsoft released yesterday the dependency # genuinely has no package yet; the sweep builds the chain in order and fills this in. $row.Status = 'Skipped' $row.Error = "The dependency chain is not available for this platform version. $($missing -join ' ')" Write-Host (" {0,-34} skipped dependency missing" -f "BC $pv") foreach ($m in $missing) { Write-Host " $m" } if (-not $NoAzureDevOpsLogging) { Write-Host "##vso[task.logissue type=warning]BC $($pv): $name skipped - $($missing[0])" } $results.Add((New-BcRuntimeVersionResult -PlatformVersion $pv -Country $Country -Row $row -Started $started)) continue } $projectFolder = Get-BcRuntimeProjectFolder -Product $Product -WorkRoot $WorkRoot -PlatformVersion $pv -Country $Country # Emitted and signed in the WORK root, and moved into the output folder only once both have # succeeded. The output folder is published as a build artifact, and a package that failed # to sign is not a deliverable - it is an unsigned file with a plausible name. # # This is not hypothetical. Banking release 27680 failed all 138 versions on a signing # error AFTER each package had already been written into the output folder; the artifact # therefore shipped 138 unsigned runtime packages, and the Marketplace release picked them # up as if they were the app. $emitFolder = Join-Path (Join-Path $WorkRoot 'emit') "$pv$(if ($Country) { "-$($Country.ToLowerInvariant())" })" if (Test-Path -LiteralPath $emitFolder) { Remove-Item -LiteralPath $emitFolder -Recurse -Force } New-Item -ItemType Directory -Force -Path $emitFolder | Out-Null $package = New-BcRuntimePackageFromSource -ProjectFolder $projectFolder -OutputFolder $emitFolder ` -SymbolFolder ($symbolFolders + @($chainFolder)) -PlatformPath $artifact.PlatformPath -Kind Runtime if ($Signing) { Invoke-BcAppSigning -Path $package @Signing } $targetFolder = Join-Path $OutputFolder "$($Product.AppId)" | Join-Path -ChildPath $pv if (-not (Test-Path -LiteralPath $targetFolder)) { New-Item -ItemType Directory -Force -Path $targetFolder | Out-Null } # BC's own file name, which the blob layout and the downloads page both expect. $wanted = Join-Path $targetFolder (Get-BcRuntimePackageFileName -Publisher "$($Product.Publisher)" -Name $name -Version $appVersion) Move-Item -LiteralPath $package -Destination $wanted -Force $row.File = $wanted $row.Seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 1) Write-Host (" {0,-34} built {1,6} s {2} KB" -f "BC $pv", $row.Seconds, [Math]::Round((Get-Item $wanted).Length / 1KB, 0)) } catch { $row.Status = 'Failed' $row.Error = ("$($_.Exception.Message)" -replace '\s+', ' ').Trim() $row.Seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 1) Write-Host (" {0,-34} FAILED {1,6} s" -f "BC $pv", $row.Seconds) Write-Host " $($row.Error)" if (-not $NoAzureDevOpsLogging) { Write-Host "##vso[task.logissue type=error]BC $($pv): $name failed - $($row.Error)" } } $result = New-BcRuntimeVersionResult -PlatformVersion $pv -Country $Country -Row $row -Started $started $results.Add($result) # Published per version, not at the end: a run that dies keeps every package it had already # uploaded. The old design uploaded after the loop and threw 10.5 agent-hours away when a run # was cancelled. if ($OnVersionComplete -and $row.Status -eq 'Succeeded') { try { & $OnVersionComplete $result } catch { $row.Status = 'Failed' $row.Error = "The package was built but publishing it failed: $(("$($_.Exception.Message)" -replace '\s+', ' ').Trim())" Write-Host " $($row.Error)" if (-not $NoAzureDevOpsLogging) { Write-Host "##vso[task.logissue type=error]BC $($pv): $name - $($row.Error)" } } } } $rows = @($results | ForEach-Object { $_.Products }) $produced = @($rows | Where-Object { $_.Status -eq 'Succeeded' }).Count $failed = @($rows | Where-Object { $_.Status -eq 'Failed' }).Count $skipped = @($rows | Where-Object { $_.Status -eq 'Skipped' }).Count foreach ($line in @(Get-BcRuntimeLogSection -Result $results.ToArray())) { Write-Host $line } if ($JUnitPath) { Set-Content -LiteralPath $JUnitPath -Value (ConvertTo-BcRuntimeJUnit -Result $results.ToArray()) -Encoding UTF8 } if ($SummaryPath) { Set-Content -LiteralPath $SummaryPath -Value (ConvertTo-BcRuntimeSummaryMarkdown -Result $results.ToArray()) -Encoding UTF8 } if ($IssuePath) { Set-Content -LiteralPath $IssuePath -Value (ConvertTo-BcRuntimeIssueJUnit -Result $results.ToArray()) -Encoding UTF8 } $level = if ($failed -gt 0) { 'Warning' } else { 'Success' } Write-ALbuildLog -Level $level ("$($name): $produced package(s) built, $failed failed, $skipped skipped " + "across $($results.Count) platform version(s).") # No '##vso[task.complete ...]' here: a cmdlet must not decide the result of the task running it. # The caller owns that and does it from these counts. return [PSCustomObject]@{ Produced = $produced; Failed = $failed; Skipped = $skipped; Results = $results.ToArray() } } |