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 -MinMajor when given, otherwise the app's '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, never read and never extended. The committed array belongs to the repository and may hold internal symbols (DEBUG, ONPREM) that must not reach a published app, so it is not treated as input - not even its BC entries. That also means the function cannot recover the original floor from it after it has stamped once: where a manifest may be stamped repeatedly, the caller remembers the baseline and passes -MinMajor. 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. The manifest's existing 'preprocessorSymbols' array is REPLACED, never extended - a repository may commit internal symbols such as DEBUG or ONPREM, and those must never travel into a published app. Everything the build needs beyond the BC range is passed here, deliberately and visibly. .PARAMETER MinMajor The floor of the BC<n> range, overriding the value derived from 'application'. Needed because this function OVERWRITES 'application'. A second stamp of the same file would otherwise read the previous target back as the floor and collapse the range - BC17..BC29 becomes BC29 - which turns every '#if BC24' false and silently compiles the '#else' branch. The floor is not recovered from the manifest's own symbols either (see -PreprocessorSymbols); the caller remembers it. The DevOps CompileApp task keeps it in a per-project pipeline variable and passes it here. .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 = @(), [ValidateRange(1, 99)] [int] $MinMajor, [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." } # Named '$floor', not '$minMajor': PowerShell variable names are case-INSENSITIVE, so '$minMajor' # IS the '$MinMajor' parameter and this line would overwrite the caller's value before it could be # read. The tests caught exactly that. $floor = (ConvertTo-BcVersion ([string] $json.application)).Major # 'application' is ALSO what this function overwrites a few lines down, so reading it back on a # SECOND stamp of the same file yields the previous TARGET as the floor and the cumulative list # collapses (BC17..BC29 -> BC29). Every '#if BC24' is then FALSE and the '#else' branch compiles, # silently, because nothing about the manifest looks wrong afterwards. # # The floor is therefore NOT recovered from the manifest's own preprocessorSymbols. That array is # committed by the repository and may carry INTERNAL symbols - DEBUG, ONPREM - which must never # reach a published app, so it is never trusted and never extended: it is REPLACED wholesale, and # only -PreprocessorSymbols adds anything beyond the BC range. # # Where a manifest may be stamped more than once, the CALLER remembers the baseline (the DevOps task # keeps it in a pipeline variable per project) and passes it as -MinMajor. if ($PSBoundParameters.ContainsKey('MinMajor')) { $floor = $MinMajor } # BC<min>..BC<target> (either direction) + any extra symbols. $symbols = [System.Collections.Generic.List[string]]::new() $lo = [Math]::Min($floor, $targetMajor) $hi = [Math]::Max($floor, $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 = $floor TargetMajor = $targetMajor Runtime = $runtime Symbols = @($symbols) } } } |