Modules/businessdev.ALbuild.RuntimePackages/Private/Invoke-BcRuntimeVersionBuildFromSource.ps1

function Invoke-BcRuntimeVersionBuildFromSource {
    <#
    .SYNOPSIS
        Builds every product of one platform version WITHOUT a container, straight from AL source.
 
    .DESCRIPTION
        The container route spends 78-94 % of a platform version on provisioning, and it spends it for
        two things: publishing the dependency chain so the service tier can resolve it, and asking that
        tier for the runtime package. New-BcRuntimePackageFromSource removes both - the AL compiler that
        ships inside the artifact emits the package itself. Measured on this catalogue: 11 s per package.
 
        The dependencies do not disappear, they change shape. A dependent still needs its chain, but only
        as SYMBOLS - which is exactly what the chain folder already holds, because Get-BcRuntimeAppFile
        compiles each chain app for this platform version and drops the .app there. So the loop below is
        the container loop with the publish, the package request and the teardown taken out; everything
        the caller sees - the result object, the per-product rows, the checkpoint - is unchanged, and that
        is deliberate: the report stages, the JUnit and the by-cause report all read those rows.
 
        What CANNOT be reproduced without a server: a legacy 'extensionsPermissionSet.xml' is migrated
        into PermissionSet objects by the service tier at publish time, and the compiler does not do that.
        Measured on Sanction Screen (modern permission sets, no legacy file) the two routes differ by one
        part in 154 - the compiler-generated .g.xlf. On an app that still carries the legacy file the
        difference is larger, and three products still do: License, API and ERiC.
 
    .PARAMETER Item
        One work item: PlatformVersion, ArtifactUrl, Country, Products[].
 
    .PARAMETER WorkRoot
        The worker's private working folder.
 
    .PARAMETER OutputFolder
        Root of the '<appId>/<platformVersion>/' output layout.
 
    .PARAMETER Signing
        Splat for Invoke-BcAppSigning, applied to the produced package.
 
    .PARAMETER Started
        When the caller started this platform version, so the reported duration covers the whole thing.
 
    .OUTPUTS
        PSCustomObject: PlatformVersion, Country, Status, Seconds, Products[] - the same shape the
        container route returns.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNull()] [object] $Item,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkRoot,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputFolder,
        [hashtable] $Signing,
        [datetime] $Started = (Get-Date)
    )

    $pv = "$($Item.PlatformVersion)"
    $country = "$(Get-BcRuntimeProperty -InputObject $Item -Name 'Country' -Default '')"
    $results = [System.Collections.Generic.List[object]]::new()
    $failedProducts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $status = 'Succeeded'

    try {
        # Not caught: without the first-party symbols nothing here can compile, so it is a failure of the
        # platform version - which the outer catch reports on every product, with the real message.
        $artifact = Get-BcArtifact -ArtifactUrl $Item.ArtifactUrl
        $symbolFolders = @(Get-BcArtifactSymbolFolder -Artifact $artifact)
        Write-ALbuildLog "[$pv] Symbol folders: $($symbolFolders.Count) (no container: the compiler in the artifact emits the package)."

        # The chain, as symbols. A folder of its own, not the shared app cache, so a product never finds
        # ITSELF in its own package cache.
        $chainSymbolFolder = Join-Path (Join-Path $WorkRoot 'chainsym') "$pv$(if ($country) { "-$($country.ToLowerInvariant())" })"
        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)")
                $status = 'SucceededWithIssues'
                $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
            }

            try {
                # The chain is compiled, not installed. Deepest first, so each entry compiles against the
                # ones before it.
                foreach ($dependency in $chain) {
                    try {
                        $dependencyApp = Get-BcRuntimeAppFile -Product $dependency -WorkRoot $WorkRoot -PlatformVersion $pv `
                            -Country $country -Signing $Signing -SymbolFolder ($symbolFolders + @($chainSymbolFolder))
                    }
                    catch {
                        # A chain app that fails here fails for everyone; mark it by name so the check
                        # above skips the products that need it, including its own turn.
                        [void]$failedProducts.Add("$($dependency.Name)")
                        throw
                    }
                    Copy-Item -LiteralPath $dependencyApp -Destination (Join-Path $chainSymbolFolder (Split-Path -Leaf $dependencyApp)) -Force
                }

                # The product's own project, prepared exactly as the container route prepares it: private
                # copy, manifest stamped to this platform version, released version written in.
                $projectFolder = Get-BcRuntimeProjectFolder -Product $product -WorkRoot $WorkRoot -PlatformVersion $pv -Country $country

                $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-BcRuntimePackageFromSource -ProjectFolder $projectFolder -OutputFolder $targetFolder `
                    -SymbolFolder ($symbolFolders + @($chainSymbolFolder)) -PlatformPath $artifact.PlatformPath -Kind Runtime
                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)"
            }
        }
    }
    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
                })
        }
    }

    return [PSCustomObject]@{
        PlatformVersion = $pv
        Country         = $country
        Status          = $status
        Seconds         = [Math]::Round(((Get-Date) - $Started).TotalSeconds, 1)
        Products        = $results.ToArray()
    }
}