Modules/businessdev.ALbuild.Apps/Private/Resolve-BcAlToolVersion.ps1

function Resolve-BcAlToolVersion {
    <#
    .SYNOPSIS
        Selects the AL Tool (dotnet tool) version that can compile a given AL runtime.
 
    .DESCRIPTION
        The AL Tool package is versioned so that its MAJOR is the HIGHEST AL runtime it supports
        (17.x compiles runtime 17 = BC28 and everything older; 18.x compiles runtime 18 = BC29 and
        older). A compiler older than the app's declared runtime fails the compile outright with
        AL1043 ("The runtime version 'x' is not supported by the AL compiler"), so the compiler has to
        satisfy 'major >= required runtime' rather than be left at "whatever happens to be installed".
 
        Selection takes the CLOSEST compiler that satisfies that: the lowest major still >= the required
        runtime, newest build inside it. Requiring an exact major match would be wrong, because entire
        majors ship prerelease-only for good - nuget.org carries no stable 15.x at all, so every BC26
        build would land on a '-beta' compiler even though stable 16.x/17.x compile the very same app.
 
        A prerelease is therefore only chosen when NO stable compiler on the feed supports the required
        runtime - i.e. for a BC major that has not shipped yet (NextMajor). That makes NextMajor builds
        work today and returns to a stable compiler the moment such a version exists, without a
        pipeline change.
 
        Versions are read from the NuGet flat-container index (a plain JSON GET, no NuGet client).
 
    .PARAMETER PackageId
        The dotnet tool package id. Default 'Microsoft.Dynamics.BusinessCentral.Development.Tools'.
 
    .PARAMETER RequiredMajor
        The AL runtime major that must be supported (AL Tool major >= this). 0 = no constraint, which
        selects the newest version allowed by -Prerelease.
 
    .PARAMETER Prerelease
        Auto (default) - the closest stable compiler supporting the required runtime; a prerelease only
        when no stable one on the feed supports it. Always - the newest build of the required runtime's
        own major, prerelease included. Never - stable only; returns nothing when no stable compiler
        supports the required runtime.
 
    .PARAMETER IndexUrl
        Override the version index URL (defaults to the nuget.org flat-container index for -PackageId).
 
    .PARAMETER TimeoutSec
        HTTP timeout for the index query.
 
    .EXAMPLE
        Resolve-BcAlToolVersion -RequiredMajor 18
        # -> 18.0.40.43394-beta while BC29 is prerelease; a stable compiler the moment one ships.
 
    .EXAMPLE
        Resolve-BcAlToolVersion -RequiredMajor 15
        # -> the newest stable 16.x: runtime 15 (BC26) has no stable compiler of its own on the feed.
 
    .OUTPUTS
        PSCustomObject: Version, IsPrerelease, RequiredMajor, Available - or $null when nothing matches.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string] $PackageId = 'Microsoft.Dynamics.BusinessCentral.Development.Tools',
        [int] $RequiredMajor = 0,
        [ValidateSet('Auto', 'Always', 'Never')] [string] $Prerelease = 'Auto',
        [string] $IndexUrl,
        [int] $TimeoutSec = 60
    )

    if (-not $IndexUrl) {
        # The flat-container ("package base address") index lists every published version, prerelease
        # included; the id must be lower-cased for that endpoint.
        $IndexUrl = "https://api.nuget.org/v3-flatcontainer/$($PackageId.ToLowerInvariant())/index.json"
    }

    # Windows PowerShell 5.1 still negotiates TLS 1.0 by default on some build agents, which nuget.org
    # refuses; opt into TLS 1.2 for this call (additive, so nothing already enabled is turned off).
    try {
        if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
        }
    }
    catch { Write-ALbuildLog -Level Warning "Could not enable TLS 1.2 for the AL Tool version query: $($_.Exception.Message)" }

    $raw = @()
    try {
        $response = Invoke-RestMethod -Uri $IndexUrl -Method Get -TimeoutSec $TimeoutSec
        if ($response -and ($response.PSObject.Properties.Name -contains 'versions')) { $raw = @($response.versions) }
    }
    catch {
        throw "Could not query the available AL Tool versions from '$IndexUrl': $($_.Exception.Message)"
    }
    if ($raw.Count -eq 0) { throw "The AL Tool version index '$IndexUrl' returned no versions." }

    # Split '<numeric>-<label>' (e.g. '18.0.39.10160-beta'); a version whose numeric part does not parse
    # is skipped rather than failing the whole selection.
    $parsed = foreach ($entry in $raw) {
        if (-not $entry) { continue }
        $text = [string] $entry
        $parts = $text -split '-', 2
        $numericText = $parts[0]
        $label = if ($parts.Count -gt 1) { $parts[1] } else { '' }
        $numeric = $null
        if (-not [version]::TryParse($numericText, [ref] $numeric)) { continue }
        [PSCustomObject]@{
            Version      = $text
            Numeric      = $numeric
            IsPrerelease = [bool] $label
        }
    }
    $parsed = @($parsed)
    if ($parsed.Count -eq 0) { throw "None of the $($raw.Count) AL Tool version(s) from '$IndexUrl' could be parsed." }

    # A compiler can build its OWN runtime and every older one - AL1043 only fires when the declared
    # runtime is HIGHER than the compiler (verified against AL Tool 18: runtime 15.0 compiles, 19.0
    # raises AL1043). So the constraint is '>= RequiredMajor', not '== RequiredMajor'. That matters
    # because whole majors ship prerelease-only forever: nuget.org has no stable 15.x at all, so an
    # exact match would push every BC26 build onto a '-beta' compiler although stable 16.x/17.x
    # compile the same app.
    $candidates = @($parsed)
    if ($RequiredMajor -gt 0) { $candidates = @($parsed | Where-Object { $_.Numeric.Major -ge $RequiredMajor }) }

    # Pick the CLOSEST compiler that can do the job: the lowest major still satisfying the requirement,
    # and the newest build inside it. Jumping straight to the newest major would swap the analyzer and
    # diagnostic set underneath an existing build for no benefit.
    $closest = {
        param($pool)
        $pool = @($pool)
        if ($pool.Count -eq 0) { return $null }
        if ($RequiredMajor -le 0) {
            # No requirement: keep the historical "newest wins" behaviour.
            return @($pool | Sort-Object -Property @{ Expression = 'Numeric'; Descending = $true })[0]
        }
        @($pool | Sort-Object -Property `
                @{ Expression = { $_.Numeric.Major }; Descending = $false }, `
            @{ Expression = 'Numeric'; Descending = $true })[0]
    }

    $stable = @($candidates | Where-Object { -not $_.IsPrerelease })

    $selected = switch ($Prerelease) {
        'Never' { & $closest $stable }
        # 'Always' stays "bleeding edge for the required runtime": the requirement's own major, newest
        # build, prerelease included - not the newest major on the whole feed.
        'Always' {
            $ownMajor = if ($RequiredMajor -gt 0) { @($candidates | Where-Object { $_.Numeric.Major -eq $RequiredMajor }) } else { @() }
            if (@($ownMajor).Count -gt 0) { @($ownMajor | Sort-Object -Property @{ Expression = 'Numeric'; Descending = $true })[0] }
            else { & $closest $candidates }
        }
        default {
            $pick = & $closest $stable
            if (-not $pick) { $pick = & $closest $candidates }
            $pick
        }
    }

    if (-not $selected) { return $null }

    if ($selected.IsPrerelease -and $Prerelease -eq 'Always') {
        Write-ALbuildLog -Level Warning ("AL Tool $($selected.Version) is a PRERELEASE - selected because " +
            "-Prerelease Always was requested$(if (@($stable).Count -gt 0) { " (a stable compiler for runtime $RequiredMajor is available)" }).")
    }
    elseif ($selected.IsPrerelease) {
        Write-ALbuildLog -Level Warning ("AL Tool $($selected.Version) is a PRERELEASE" +
            $(if ($RequiredMajor -gt 0) { " - no stable AL compiler supporting AL runtime $RequiredMajor or newer exists on the feed yet (expected while that BC major is in preview)" } else { '' }) + '.')
    }
    elseif ($RequiredMajor -gt 0 -and $selected.Numeric.Major -ne $RequiredMajor) {
        Write-ALbuildLog ("AL runtime $RequiredMajor has no stable compiler of its own; using the stable AL Tool " +
            "$($selected.Version) instead, which supports runtime $RequiredMajor as well.")
    }

    return [PSCustomObject]@{
        Version       = $selected.Version
        IsPrerelease  = $selected.IsPrerelease
        RequiredMajor = $RequiredMajor
        Available     = $candidates.Count
    }
}