Get-ALCompilerFromNuget.ps1

<#
.SYNOPSIS
    Download AL Compiler from NuGet package Microsoft.Dynamics.BusinessCentral.Development.Tools
.DESCRIPTION
    Downloads the newest version of the Microsoft.Dynamics.BusinessCentral.Development.Tools package
    from nuget.org with the same major version as specified in RuntimeVersion parameter.
    Extracts the compiler and returns the path to alc.exe.
 
.EXAMPLE
    PS C:\> Get-ALCompilerFromNuget -RuntimeVersion "25.0" -TargetPath "c:\compiler"
    Will download the newest version of Microsoft.Dynamics.BusinessCentral.Development.Tools with major version 25
    and extract it to c:\compiler
 
.EXAMPLE
    PS C:\> Get-ALCompilerFromNuget -RuntimeVersion "26.1.12345.0" -TargetPath "c:\compiler"
    Will download the newest version of Microsoft.Dynamics.BusinessCentral.Development.Tools with major version 26
    and extract it to c:\compiler
 
.Parameter RuntimeVersion
    The runtime version to match. The major version will be extracted and used to find
    the newest package with matching major version.
 
.Parameter TargetPath
    Target path where the compiler will be extracted.
 
.OUTPUTS
    String - Path to the folder containing alc.exe
#>

function Get-ALCompilerFromNuget {
    param(
        # Runtime version - major version will be used to find matching package
        [Parameter(Mandatory = $true)]
        [String] $RuntimeVersion,
        # Target path for extracted compiler
        [Parameter(Mandatory = $true)]
        [String] $TargetPath
    )

    function Expand-7zipArchive {
        Param (
            [Parameter(Mandatory = $true)]
            [string] $Path,
            [string] $DestinationPath
        )
    
        $7zipPath = "$env:ProgramFiles\7-Zip\7z.exe"
    
        $use7zip = $false
        if ($bcContainerHelperConfig.use7zipIfAvailable -and (Test-Path -Path $7zipPath -PathType Leaf)) {
            try {
                $use7zip = [decimal]::Parse([System.Diagnostics.FileVersionInfo]::GetVersionInfo($7zipPath).FileVersion, [System.Globalization.CultureInfo]::InvariantCulture) -ge 19
            }
            catch {
                $use7zip = $false
            }
        }
    
        if ($use7zip) {
            Write-Host "using 7zip"
            Set-Alias -Name 7z -Value $7zipPath
            $command = '7z x "{0}" -o"{1}" -aoa -r' -f $Path, $DestinationPath
            Invoke-Expression -Command $command | Out-Null
        }
        else {
            Write-Host "using Expand-Archive"
            Expand-Archive -Path $Path -DestinationPath "$DestinationPath" -Force
        }
    }

    $packageName = "Microsoft.Dynamics.BusinessCentral.Development.Tools"
    $nugetApiUrl = "https://api.nuget.org/v3/index.json"

    # Parse major version from RuntimeVersion
    try {
        $versionObj = [System.Version]::Parse($RuntimeVersion)
        $majorVersion = $versionObj.Major
    }
    catch {
        # If parsing fails, try to extract just the first number
        if ($RuntimeVersion -match '^(\d+)') {
            $majorVersion = [int]$matches[1]
        }
        else {
            throw "Cannot parse RuntimeVersion '$RuntimeVersion'. Please provide a valid version string."
        }
    }

    # Minimum supported major version is 15
    if ($majorVersion -lt 15) {
        Write-Host "Major version $majorVersion is less than 15, using version 15 instead"
        $majorVersion = 15
    }

    Write-Host "Looking for $packageName with major version $majorVersion on nuget.org"

    if (Test-Path $TargetPath) {
        Write-Host "Removing $TargetPath"
        Remove-Item $TargetPath -Recurse -Force
    }

    # Get NuGet API endpoints
    Write-Host "Fetching NuGet API endpoints..."
    $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
    try {
        $nugetIndex = Invoke-RestMethod -UseBasicParsing -Method GET -Uri $nugetApiUrl
    }
    finally {
        $global:ProgressPreference = $prev
    }

    $searchUrl = $nugetIndex.resources | Where-Object { $_.'@type' -eq 'SearchQueryService' } | Select-Object -ExpandProperty '@id' -First 1
    $packageBaseAddressUrl = $nugetIndex.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -ExpandProperty '@id' -First 1

    if (-not $searchUrl -or -not $packageBaseAddressUrl) {
        throw "Could not find required NuGet API endpoints"
    }

    # Search for the package
    Write-Host "Searching for package $packageName..."
    $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
    try {
        $searchResult = Invoke-RestMethod -UseBasicParsing -Method GET -Uri "$searchUrl`?q=$packageName&prerelease=false&take=50"
    }
    finally {
        $global:ProgressPreference = $prev
    }

    $package = $searchResult.data | Where-Object { $_.id -eq $packageName } | Select-Object -First 1
    if (-not $package) {
        throw "Package $packageName not found on nuget.org"
    }

    # Get all versions of the package
    Write-Host "Fetching available versions..."
    $versionsUrl = "$($packageBaseAddressUrl.TrimEnd('/'))/$($packageName.ToLowerInvariant())/index.json"
    $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
    try {
        $versionsResponse = Invoke-RestMethod -UseBasicParsing -Method GET -Uri $versionsUrl
    }
    finally {
        $global:ProgressPreference = $prev
    }

    # Filter versions by major version and find the latest
    $matchingVersions = $versionsResponse.versions | Where-Object {
        try {
            $v = [System.Version]::Parse($_)
            $v.Major -eq $majorVersion
        }
        catch {
            $false
        }
    } | ForEach-Object {
        [PSCustomObject]@{
            VersionString = $_
            Version       = [System.Version]::Parse($_)
        }
    } | Sort-Object -Property Version -Descending

    if (-not $matchingVersions -or $matchingVersions.Count -eq 0) {
        Write-Host "No versions of $packageName found with major version $majorVersion, falling back to latest version"
        # Get the latest version available
        $matchingVersions = $versionsResponse.versions | ForEach-Object {
            try {
                [PSCustomObject]@{
                    VersionString = $_
                    Version       = [System.Version]::Parse($_)
                }
            }
            catch {
                $null
            }
        } | Where-Object { $_ -ne $null } | Sort-Object -Property Version -Descending
        
        if (-not $matchingVersions -or $matchingVersions.Count -eq 0) {
            throw "No versions of $packageName found on nuget.org"
        }
    }

    $latestVersion = $matchingVersions[0].VersionString
    Write-Host "Found latest version with major version $majorVersion`: $latestVersion"

    # Use standard NuGet cache folder
    $nugetCacheFolder = Join-Path $env:USERPROFILE ".nuget\packages"
    $packageCacheFolder = Join-Path $nugetCacheFolder "$($packageName.ToLowerInvariant())\$($latestVersion.ToLowerInvariant())"
    
    if (Test-Path $packageCacheFolder) {
        Write-Host "Using cached package from $packageCacheFolder"
    }
    else {
        # Download the package to cache
        $downloadUrl = "$($packageBaseAddressUrl.TrimEnd('/'))/$($packageName.ToLowerInvariant())/$($latestVersion.ToLowerInvariant())/$($packageName.ToLowerInvariant()).$($latestVersion.ToLowerInvariant()).nupkg"
        
        Write-Host "Downloading package from $downloadUrl..."
        New-Item -Path $packageCacheFolder -ItemType Directory -Force | Out-Null
        
        $nupkgPath = Join-Path $packageCacheFolder "$($packageName.ToLowerInvariant()).$($latestVersion.ToLowerInvariant()).nupkg"
        
        $prev = $global:ProgressPreference; $global:ProgressPreference = "SilentlyContinue"
        try {
            Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $nupkgPath
        }
        finally {
            $global:ProgressPreference = $prev
        }
        
        # Extract the nupkg in cache folder
        Write-Host "Extracting NuGet package to cache..."
        Expand-7zipArchive -Path $nupkgPath -DestinationPath $packageCacheFolder
    }

    # Copy from cache to target path
    Write-Host "Copying package contents to $TargetPath..."
    New-Item -Path $TargetPath -ItemType Directory -Force | Out-Null
    Copy-Item -Path "$packageCacheFolder\*" -Destination $TargetPath -Recurse -Force

    # Find alc.exe path (exclude win32 and lib subfolders)
    $ALCPossiblePaths = (Get-ChildItem -Path $TargetPath -Filter alc.exe -Recurse | Where-Object { $_.FullName -notlike '*win32*' -and $_.FullName -notlike '*\lib\*' }).FullName
    if ($ALCPossiblePaths) {
        $ALCPath = (Split-Path ($ALCPossiblePaths | Select-Object -First 1))
    }
    else {
        $ALCPossiblePaths = (Get-ChildItem -Path $TargetPath -Filter alc.exe -Recurse | Where-Object { $_.FullName -like '*win32*' -and $_.FullName -notlike '*\lib\*' }).FullName
        if ($ALCPossiblePaths) {
            $ALCPath = (Split-Path ($ALCPossiblePaths | Select-Object -First 1))
        }
        else {
            throw "Could not find alc.exe in the extracted package"
        }
    }

    Write-Host "ALC.exe path: $($ALCPath)"

    return $ALCPath
}