Modules/businessdev.ALbuild.Apps/Public/Update-BcAppManifest.ps1

function Update-BcAppManifest {
    <#
    .SYNOPSIS
        Prepares an AL app manifest (app.json) for a target BC version: version features + preprocessor symbols.
 
    .DESCRIPTION
        Restores the pipeline's V1 "update manifest" behaviour for a compile against a specific BC version
        (a normal build against Latest, or a runtime package built for an older platform). Two adjustments,
        both keyed to -BcVersion:
 
          * Version features (port of Apply-BcVersionFeatures): set 'application' and 'platform' to
            '<major>.0.0.0' and 'runtime' to the target's AL runtime version. The runtime is
            '(major - 11).0' for BC >= 12 (17->6.0, 22->11.0, 28->17.0, ...), with the BC 18.1+ -> 7.1
            special case; this keeps working for future majors (V1 stopped at 26/27). Skipped with
            -SkipVersionFeatures (then only the preprocessor symbols are written).
 
          * Preprocessor symbols: 'BC<min>'..'BC<target>', where 'min' is the app's ORIGINAL 'application'
            major (read BEFORE the version-feature rewrite), plus any -PreprocessorSymbols. This lets
            version-conditional AL ('#if BC24 ... #endif') compile. Any existing 'preprocessorSymbols' array
            is replaced.
 
        Operates on a single app.json or every app.json under a folder (skipping .alpackages / output).
        Rewrites the file in place (build workspace); use before Invoke-BcCompiler.
 
    .PARAMETER Path
        An app.json file, or a folder searched recursively for app.json.
 
    .PARAMETER BcVersion
        The target BC version - full ('28.3.52162.52455') or major ('28'); its major drives everything.
 
    .PARAMETER PreprocessorSymbols
        Extra preprocessor symbols to add alongside the BC<n> range.
 
    .PARAMETER SkipVersionFeatures
        Only inject preprocessor symbols; leave application / runtime / platform unchanged.
 
    .EXAMPLE
        Update-BcAppManifest -Path .\app -BcVersion '28.3.52162.52455'
 
    .OUTPUTS
        PSCustomObject per app.json: AppJsonPath, MinMajor, TargetMajor, Runtime, Symbols.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $Path,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $BcVersion,
        [string[]] $PreprocessorSymbols = @(),
        [switch] $SkipVersionFeatures
    )

    if (-not (Test-Path -LiteralPath $Path)) { throw "Path '$Path' does not exist." }

    $target = ConvertTo-BcVersion $BcVersion
    $targetMajor = $target.Major
    # AL runtime for the target major: (major - 11).0 for BC >= 12 (17->6.0 ... 28->17.0); 18.1+ -> 7.1.
    $runtime = if ($targetMajor -eq 18 -and $target.Minor -ge 1) { '7.1' } else { "$($targetMajor - 11).0" }

    $item = Get-Item -LiteralPath $Path
    if ($item.PSIsContainer) {
        $files = @(Get-ChildItem -LiteralPath $item.FullName -Filter 'app.json' -File -Recurse -ErrorAction SilentlyContinue |
                Where-Object { $_.FullName -notmatch '[\\/](\.alpackages|\.altemplates|\.snapshots|\.output|output)[\\/]' })
    }
    elseif ($item.Name -eq 'app.json') {
        $files = @($item)
    }
    else {
        throw "Path '$Path' is neither a folder nor an app.json file."
    }
    if ($files.Count -eq 0) { throw "No app.json found under '$Path'." }

    $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
    foreach ($file in $files) {
        $raw = [System.IO.File]::ReadAllText($file.FullName)
        $json = $raw | ConvertFrom-Json
        if (-not ($json.PSObject.Properties.Name -contains 'application')) {
            throw "'$($file.FullName)' has no top-level 'application' property."
        }
        $minMajor = (ConvertTo-BcVersion ([string] $json.application)).Major

        # BC<min>..BC<target> (either direction) + any extra symbols.
        $symbols = [System.Collections.Generic.List[string]]::new()
        $lo = [Math]::Min($minMajor, $targetMajor)
        $hi = [Math]::Max($minMajor, $targetMajor)
        for ($i = $lo; $i -le $hi; $i++) { $symbols.Add("BC$i") }
        foreach ($s in $PreprocessorSymbols) { if (-not [string]::IsNullOrWhiteSpace($s)) { $symbols.Add($s.Trim()) } }

        if (-not $SkipVersionFeatures) {
            $json | Add-Member -Name 'application' -Value "$targetMajor.0.0.0" -MemberType NoteProperty -Force
            $json | Add-Member -Name 'platform' -Value "$targetMajor.0.0.0" -MemberType NoteProperty -Force
            $json | Add-Member -Name 'runtime' -Value $runtime -MemberType NoteProperty -Force
        }
        $json | Add-Member -Name 'preprocessorSymbols' -Value ([string[]] $symbols) -MemberType NoteProperty -Force

        if ($PSCmdlet.ShouldProcess($file.FullName, "Update manifest for BC $targetMajor")) {
            $out = $json | ConvertTo-Json -Depth 100
            # Windows PowerShell 5.1 escapes < > & ' to \uXXXX; restore them (valid JSON either way).
            $out = $out -replace '\\u0026', '&' -replace '\\u003c', '<' -replace '\\u003e', '>' -replace '\\u0027', "'"
            [System.IO.File]::WriteAllText($file.FullName, $out, $utf8NoBom)
            $vf = if ($SkipVersionFeatures) { '(symbols only)' } else { "application $targetMajor.0.0.0, runtime $runtime" }
            Write-ALbuildLog -Level Success "Manifest '$($file.Directory.Name)': $vf, symbols [$($symbols -join ', ')]."
        }

        [PSCustomObject]@{
            AppJsonPath = $file.FullName
            MinMajor    = $minMajor
            TargetMajor = $targetMajor
            Runtime     = $runtime
            Symbols     = @($symbols)
        }
    }
}