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 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, [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 { Write-ALbuildLog "[$pv] Creating container '$containerName' ..." $createArgs = @{ Name = $containerName ArtifactUrl = $Item.ArtifactUrl DockerExecutable = $DockerExecutable } if ($Credential) { $createArgs['Credential'] = $Credential } if ($MemoryLimit) { $createArgs['MemoryLimit'] = $MemoryLimit } if ($LicenseFile) { $createArgs['LicenseFile'] = $LicenseFile } 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 and published atomically, so several workers can resolve the same artifact # concurrently without seeing a half-extracted one. $symbolFolders = @() try { $artifact = Get-BcArtifact -ArtifactUrl $Item.ArtifactUrl $symbolFolders = @(Get-BcArtifactSymbolFolder -Artifact $artifact) Write-ALbuildLog "[$pv] Symbol folders: $($symbolFolders.Count)" } catch { # Not fatal here: report it once, then let each app fail with the compiler's own message # rather than aborting the whole platform version on a symbol-resolution hiccup. Write-ALbuildLog -Level Warning "[$pv] Could not resolve first-party symbols: $($_.Exception.Message)" } 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' })) ..." foreach ($dependency in $chain) { $dependencyApp = Get-BcRuntimeAppFile -Product $dependency -WorkRoot $WorkRoot -PlatformVersion $pv -Signing $Signing -SymbolFolder $symbolFolders Publish-BcContainerApp -Name $containerName -AppFile $dependencyApp -Sync -Install -SkipVerification -DockerExecutable $DockerExecutable $installed.Add($dependency) } $appFile = Get-BcRuntimeAppFile -Product $product -WorkRoot $WorkRoot -PlatformVersion $pv -Signing $Signing -SymbolFolder $symbolFolders 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() } } |