Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcRuntimeAppFile.ps1

function Get-BcRuntimeAppFile {
    <#
    .SYNOPSIS
        Returns the compiled .app of a product for one platform version, compiling it once if needed.
 
    .DESCRIPTION
        A runtime package is produced from an app that was compiled AGAINST the target platform - the
        manifest is stamped to that version and the ONPREM / BC<major> preprocessor symbols are applied -
        so every (product, platform version) pair needs its own build.
 
        Results are cached per platform version inside the worker's private work root and reused. That
        matters more than it looks: within one container the dependency apps are compiled first and then
        installed again for every product that depends on them. Without the cache, 365 business API would
        be recompiled once per dependent, on every platform version.
 
        Everything happens inside -WorkRoot, on a copy of the project. The shared checkout is never
        touched, which is what allows several workers to run at once: the previous design rewrote
        app.json in place and restored it in a 'finally', so two concurrent versions would have fought
        over the same file.
 
    .PARAMETER Product
        The planned product entry (Name, Publisher, AppId, AppVersion, Projects).
 
    .PARAMETER WorkRoot
        The worker's private working folder.
 
    .PARAMETER PlatformVersion
        Target platform version.
 
    .PARAMETER Signing
        Splat for Invoke-BcAppSigning applied to the compiled app. Omitted = no signing.
 
    .PARAMETER SymbolFolder
        Package cache folders for the compiler (first-party symbols for this platform version).
 
    .OUTPUTS
        System.String - path to the compiled .app.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNull()] [object] $Product,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkRoot,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $PlatformVersion,
        [hashtable] $Signing,
        [string[]] $SymbolFolder = @()
    )

    $cacheFolder = Join-Path (Join-Path $WorkRoot 'apps') $PlatformVersion
    if (-not (Test-Path -LiteralPath $cacheFolder)) { New-Item -ItemType Directory -Force -Path $cacheFolder | Out-Null }
    $cached = Join-Path $cacheFolder "$($Product.AppId).app"
    if (Test-Path -LiteralPath $cached) { return $cached }

    $projects = @(Get-BcRuntimeProperty -InputObject $Product -Name 'Projects' -Default @())
    if ($projects.Count -eq 0) {
        throw "No project folder is configured for '$($Product.Name)', so it cannot be compiled for platform $PlatformVersion."
    }
    # One catalogue entry = one shipped app. Extra folders in a repo (tests, demo) are excluded by the
    # planner, so the first project is the app itself.
    $source = "$($projects[0])"
    if (-not (Test-Path -LiteralPath $source)) { throw "Project folder '$source' for '$($Product.Name)' does not exist." }

    # Private copy: the manifest is rewritten per platform version, and the shared checkout must stay
    # untouched so other workers are unaffected.
    $build = Join-Path (Join-Path $WorkRoot 'build') "$PlatformVersion-$($Product.AppId)"
    if (Test-Path -LiteralPath $build) { Remove-Item -LiteralPath $build -Recurse -Force }
    New-Item -ItemType Directory -Force -Path $build | Out-Null
    Copy-Item -LiteralPath $source -Destination $build -Recurse -Force
    $projectFolder = Join-Path $build (Split-Path -Leaf $source)

    $appJson = Join-Path $projectFolder 'app.json'
    if (-not (Test-Path -LiteralPath $appJson)) { throw "No app.json in '$projectFolder'." }

    # Stamp the RELEASED version, not the source's. The factory compiles from the product's master while
    # the version being shipped comes from the last CD build's artifact, and the runtime package is
    # requested from the service tier BY name and version - a mismatch means Get-NAVAppRuntimePackage
    # simply does not find the app that was just installed.
    Set-BcAppVersion -Path $projectFolder -Version "$($Product.AppVersion)" | Out-Null

    $major = ([version](ConvertTo-BcVersion $PlatformVersion)).Major
    Update-BcAppManifest -Path $appJson -BcVersion $PlatformVersion -PreprocessorSymbols @('ONPREM', "BC$major") | Out-Null

    $packageCache = @(Join-Path $projectFolder '.alpackages') + @($SymbolFolder)
    $compiled = Invoke-BcCompiler -ProjectFolder $projectFolder -Engine AlTool -OutputFolder $cacheFolder -PackageCachePath $packageCache
    if (-not $compiled.Success) { throw "AL compilation of '$($Product.Name)' for platform $PlatformVersion failed with exit code $($compiled.ExitCode)." }

    if ($Signing) { Invoke-BcAppSigning -Path $compiled.OutputFile @Signing }

    # Store under the app id: the compiler names the file from the manifest, and the dependency lookup
    # needs a name it can predict without re-reading it.
    Move-Item -LiteralPath $compiled.OutputFile -Destination $cached -Force
    return $cached
}