Modules/businessdev.ALbuild.RuntimePackages/Private/Invoke-BcRuntimeVersionBuild.ps1
|
function Invoke-BcRuntimeVersionBuild { <# .SYNOPSIS Produces the runtime packages of every pending app for ONE platform version, in one container. .DESCRIPTION The body a factory worker runs. One container is created for the platform version and every pending app is processed inside it, in dependency order: create container for each app (dependencies first): install the app's dependency chain publish + install the app produce the runtime package tear the app AND its chain down again remove container WHY THE TEARDOWN IS NOT OPTIONAL The dependencies are themselves catalogue products that need runtime packages of their own, so they are built here too - the container is not a fixed stage with a fixed set of apps on it, it is reused by apps with different requirements. Leaving an app installed would mean each app's runtime package is produced against whatever happened to be installed before it, which depends on the order the planner emitted and on which apps failed earlier. That is not a property anyone wants a shipped artifact to have. Tearing down after each app makes the package a function of the app and the platform version only. The teardown runs in 'finally': an app that fails must not leave its partial state behind for the next one, or one failure quietly turns into a series of them. FAILURE ISOLATION Every app is isolated. If one fails, its transitive dependents in the same container are marked as failed too (they cannot be installed without it) and everything else continues. A failed platform version never fails the run - Microsoft occasionally publishes an artifact that cannot produce a working container, and one bad version must not stall a catalogue. .PARAMETER Item One work item from Get-BcRuntimeWorkSet: PlatformVersion, ArtifactUrl, Products[]. .PARAMETER WorkRoot Private working folder for this version. Nothing outside it is written, which is what lets several workers run at once - the shared checkout is never mutated. .PARAMETER OutputFolder Root of the '<appId>/<platformVersion>/' output layout. .PARAMETER Credential Container admin credential. .PARAMETER MemoryLimit Container memory limit. .PARAMETER LegacyLicenseFile The .flf license, used for platform majors up to 19. Business Central switched licence formats at BC20: a .bclicense is rejected by an older service tier, and vice versa. The factory spans majors inside one run, so it needs both and picks per platform version. .PARAMETER LicenseFile BC licence for the container (runtime package generation is licence-checked on the server). .PARAMETER Signing Splat for Invoke-BcAppSigning. Omitted = no signing. .PARAMETER TeardownMode Full (default) removes the app and its dependency chain after each app. AppOnly keeps the chain installed - faster, but the packages are then produced against a shared installation state. .PARAMETER UseImageCache Start from a cached version-specific image instead of installing the artifact on every start. .PARAMETER DockerExecutable Docker executable. .OUTPUTS PSCustomObject: PlatformVersion, Status, Seconds, Products[] (Name, Status, Seconds, Error, File). #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNull()] [object] $Item, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkRoot, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputFolder, [pscredential] $Credential, [string] $MemoryLimit = '8G', [string] $LicenseFile, [string] $LegacyLicenseFile, [hashtable] $Signing, [ValidateSet('Full', 'AppOnly')] [string] $TeardownMode = 'Full', [switch] $UseImageCache, [string] $DockerExecutable = 'docker' ) $started = Get-Date $pv = "$($Item.PlatformVersion)" $results = [System.Collections.Generic.List[object]]::new() # An app cannot be installed without its dependencies, so once a product fails every product that # depends on it fails too - reported honestly rather than attempted and failing with a confusing # "dependency not found". $failedProducts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $containerName = "bdev-rt-$([guid]::NewGuid().ToString('N').Substring(0, 8))" $status = 'Succeeded' try { $pvMajor = try { ([version](ConvertTo-BcVersion $pv)).Major } catch { 0 } Write-ALbuildLog "[$pv] Creating container '$containerName' ..." $createArgs = @{ Name = $containerName ArtifactUrl = $Item.ArtifactUrl DockerExecutable = $DockerExecutable } if ($Credential) { $createArgs['Credential'] = $Credential } if ($MemoryLimit) { $createArgs['MemoryLimit'] = $MemoryLimit } # Up to BC19 the .flf applies, from BC20 the .bclicense. Without a licence the container # falls back to the demo licence bundled with the artifact, which for old majors is no # longer valid - BC17 in run 27319 died with 'The license file is corrupt. Error Code: -200' # before the service tier ever came up. $licenseForVersion = if ($pvMajor -le 19 -and $LegacyLicenseFile) { $LegacyLicenseFile } else { $LicenseFile } if ($licenseForVersion) { $createArgs['LicenseFile'] = $licenseForVersion } if ($UseImageCache) { $createArgs['UseImageCache'] = $true } New-BcContainer @createArgs | Out-Null # First-party symbols for THIS platform version, resolved once and reused by every app. The apps # are recompiled against the target platform, so without them the compile fails with AL1022 # ('Microsoft Application ... could not be found'). Get-BcArtifact / Get-BcArtifactSymbolFolder # are lock-protected, keyed by country and published atomically, so several workers can resolve # the same artifact concurrently without seeing a half-staged or foreign-country one. # # DELIBERATELY NOT CAUGHT. Without these symbols nothing in this container can compile, so a # failure here is a failure of the platform version - which the outer catch already reports, on # every product, with the real message. It used to be caught and downgraded to a warning: in run # 27372 the staging for 27.5.46862.0 lost a race, and what reached the log was AL1022 for # 'Microsoft Base Application' followed by four products 'blocked by a failed dependency' - every # line of it pointing away from the actual cause. $artifact = Get-BcArtifact -ArtifactUrl $Item.ArtifactUrl $symbolFolders = @(Get-BcArtifactSymbolFolder -Artifact $artifact) Write-ALbuildLog "[$pv] Symbol folders: $($symbolFolders.Count)" # Symbols for the catalogue's OWN apps. A dependent is recompiled against this platform # version, so the compiler needs its dependencies as packages - and the only build of them # that matches this platform is the one made moments ago in this container. Without it the # compile dies with AL1022 naming the dependency (run 27319: Print Agent and Proxy # Application failed on every platform version, looking for Extension License 2.1.0.0). # # A folder of its own, not the shared app cache, so a product never finds ITSELF in its own # package cache - only the chain is written here. $chainSymbolFolder = Join-Path (Join-Path $WorkRoot 'chainsym') $pv if (Test-Path -LiteralPath $chainSymbolFolder) { Remove-Item -LiteralPath $chainSymbolFolder -Recurse -Force } New-Item -ItemType Directory -Force -Path $chainSymbolFolder | Out-Null foreach ($product in @($Item.Products)) { $productStarted = Get-Date $chain = @(Get-BcRuntimeProperty -InputObject $product -Name 'DependencyChain' -Default @()) $blockedBy = @($chain | Where-Object { $failedProducts.Contains("$($_.Name)") } | ForEach-Object { "$($_.Name)" }) if ($blockedBy.Count -gt 0) { [void]$failedProducts.Add("$($product.Name)") $results.Add([PSCustomObject]@{ Name = "$($product.Name)" Status = 'Failed' Seconds = 0 Error = "Skipped because its dependency failed on this platform version: $($blockedBy -join ', ')." File = $null }) Write-ALbuildLog -Level Warning "[$pv] $($product.Name): dependency failed ($($blockedBy -join ', ')); not attempted." continue } $installed = [System.Collections.Generic.List[object]]::new() try { Write-ALbuildLog "[$pv] $($product.Name): installing dependency chain ($(if ($chain.Count) { ($chain.Name) -join ' -> ' } else { 'none' })) ..." # The chain arrives deepest-first, so each entry compiles against the ones before it. foreach ($dependency in $chain) { $dependencyApp = Get-BcRuntimeAppFile -Product $dependency -WorkRoot $WorkRoot -PlatformVersion $pv -Signing $Signing -SymbolFolder ($symbolFolders + @($chainSymbolFolder)) Publish-BcContainerApp -Name $containerName -AppFile $dependencyApp -Sync -Install -SkipVerification -DockerExecutable $DockerExecutable $installed.Add($dependency) # Copied, not referenced: Get-BcRuntimeAppFile caches per (platform version, app id) # in a folder shared with every other product, and pointing the compiler at that # would hand a product its own package. Copy-Item -LiteralPath $dependencyApp -Destination (Join-Path $chainSymbolFolder (Split-Path -Leaf $dependencyApp)) -Force } $appFile = Get-BcRuntimeAppFile -Product $product -WorkRoot $WorkRoot -PlatformVersion $pv -Signing $Signing -SymbolFolder ($symbolFolders + @($chainSymbolFolder)) Publish-BcContainerApp -Name $containerName -AppFile $appFile -Sync -Install -SkipVerification -DockerExecutable $DockerExecutable $installed.Add($product) $targetFolder = Join-Path $OutputFolder ($product.TargetPath -replace '/', [System.IO.Path]::DirectorySeparatorChar) if (-not (Test-Path -LiteralPath $targetFolder)) { New-Item -ItemType Directory -Force -Path $targetFolder | Out-Null } $runtimeApp = New-BcRuntimePackage -Name $containerName -AppName "$($product.Name)" ` -AppPublisher "$($product.Publisher)" -AppVersion "$($product.AppVersion)" ` -OutputFolder $targetFolder -DockerExecutable $DockerExecutable if ($Signing) { Invoke-BcAppSigning -Path $runtimeApp @Signing } $targetFile = Join-Path $targetFolder "$($product.FileName)" if ("$runtimeApp" -ne "$targetFile") { Move-Item -LiteralPath $runtimeApp -Destination $targetFile -Force } $results.Add([PSCustomObject]@{ Name = "$($product.Name)" Status = 'Succeeded' Seconds = [Math]::Round(((Get-Date) - $productStarted).TotalSeconds, 1) Error = $null File = $targetFile }) Write-ALbuildLog -Level Success "[$pv] $($product.Name): runtime package created." } catch { [void]$failedProducts.Add("$($product.Name)") $status = 'SucceededWithIssues' $results.Add([PSCustomObject]@{ Name = "$($product.Name)" Status = 'Failed' Seconds = [Math]::Round(((Get-Date) - $productStarted).TotalSeconds, 1) Error = $_.Exception.Message File = $null }) Write-ALbuildLog -Level Warning "[$pv] $($product.Name) failed: $($_.Exception.Message)" } finally { if ($TeardownMode -eq 'Full') { # Reverse order: a dependency cannot be removed while something still depends on it. for ($i = $installed.Count - 1; $i -ge 0; $i--) { $entry = $installed[$i] try { Uninstall-BcContainerApp -Name $containerName -AppName "$($entry.Name)" -AppVersion "$($entry.AppVersion)" -Force -DockerExecutable $DockerExecutable Unpublish-BcContainerApp -Name $containerName -AppName "$($entry.Name)" -AppVersion "$($entry.AppVersion)" -DockerExecutable $DockerExecutable } catch { # Best effort: the container is discarded at the end of the version anyway, and a # teardown error must not mask the build error that may have caused it. Write-ALbuildLog -Level Warning "[$pv] Could not remove '$($entry.Name)': $($_.Exception.Message)" } } } } } } catch { $status = 'Failed' Write-ALbuildLog -Level Warning "[$pv] Platform version failed: $($_.Exception.Message)" foreach ($product in @($Item.Products)) { if (@($results | Where-Object { $_.Name -eq "$($product.Name)" }).Count -gt 0) { continue } $results.Add([PSCustomObject]@{ Name = "$($product.Name)" Status = 'Failed' Seconds = 0 Error = "Platform version failed before this app was reached: $($_.Exception.Message)" File = $null }) } } finally { Remove-BcContainer -Name $containerName -DockerExecutable $DockerExecutable -Confirm:$false -ErrorAction SilentlyContinue } return [PSCustomObject]@{ PlatformVersion = $pv Status = $status Seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 1) Products = $results.ToArray() } } |