Modules/businessdev.ALbuild.Apps/Public/Invoke-BcCompiler.ps1

function Invoke-BcCompiler {
    <#
    .SYNOPSIS
        Compiles an AL project into an .app package.
 
    .DESCRIPTION
        Compiles an AL project using either the cross-platform AL compiler (default) or a
        container's compiler:
          -Engine AlTool (default) invokes the AL compiler executable (alc) on the host. Symbols
                           are taken from the package cache (.alpackages), which Resolve-BcDependencies
                           populates; the AL compiler does not download dependencies itself.
          -Engine Container runs the compiler inside a Business Central container via the exec seam.
        The output file name follows the BC convention "<publisher>_<name>_<version>.app", derived
        from app.json.
 
    .PARAMETER ProjectFolder
        The AL project folder (contains app.json).
 
    .PARAMETER OutputFolder
        Where to write the compiled .app. Default: <ProjectFolder>/output.
 
    .PARAMETER Engine
        AlTool (default) or Container.
 
    .PARAMETER CompilerPath
        (AlTool) The AL compiler executable. Default 'alc'.
 
    .PARAMETER ContainerName
        (Container) The container to compile in.
 
    .PARAMETER ContainerCompilerPath
        (Container) The AL compiler path inside the container. Default 'alc.exe'.
 
    .PARAMETER PackageCachePath
        Symbol package folder(s). Default: <ProjectFolder>/.alpackages.
 
    .PARAMETER Analyzer
        Analyzer assembly paths (CodeCop/AppSourceCop/etc.).
 
    .PARAMETER RuleSet
        Ruleset file.
 
    .PARAMETER AssemblyProbingPath
        Additional .NET assembly probing paths.
 
    .PARAMETER LogLevel
        Compiler log level.
 
    .PARAMETER DockerExecutable
        (Container) The Docker executable to use (default 'docker').
 
    .EXAMPLE
        Invoke-BcCompiler -ProjectFolder ./app
 
    .OUTPUTS
        PSCustomObject with Success, OutputFile, ExitCode, Output.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ProjectFolder,
        [string] $OutputFolder,
        [ValidateSet('AlTool', 'Container')] [string] $Engine = 'AlTool',
        [string] $CompilerPath = 'alc',
        [string] $ContainerName,
        [string] $ContainerCompilerPath = 'alc.exe',
        [string[]] $PackageCachePath = @(),
        [string[]] $Analyzer = @(),
        [string] $RuleSet,
        [string[]] $AssemblyProbingPath = @(),
        [ValidateSet('', 'Error', 'Warning', 'Verbose', 'Normal')] [string] $LogLevel = '',
        [string] $DockerExecutable = 'docker'
    )

    $appJsonPath = Join-Path $ProjectFolder 'app.json'
    if (-not (Test-Path -LiteralPath $appJsonPath)) {
        throw "No app.json found in project folder '$ProjectFolder'."
    }
    $appJson = Get-Content -LiteralPath $appJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json

    if (-not $OutputFolder) { $OutputFolder = Join-Path $ProjectFolder 'output' }
    if (-not (Test-Path -LiteralPath $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null }

    if (-not $PackageCachePath -or $PackageCachePath.Count -eq 0) {
        $PackageCachePath = @(Join-Path $ProjectFolder '.alpackages')
    }

    $outputFileName = "$($appJson.publisher)_$($appJson.name)_$($appJson.version).app"
    $outputFile = Join-Path $OutputFolder $outputFileName

    $compilerArgs = Get-BcCompilerArguments -ProjectFolder $ProjectFolder -OutputFile $outputFile `
        -PackageCachePath $PackageCachePath -Analyzer $Analyzer -RuleSet $RuleSet `
        -AssemblyProbingPath $AssemblyProbingPath -LogLevel $LogLevel

    Write-ALbuildLog "Compiling '$($appJson.name)' $($appJson.version) ($Engine engine)..."

    if ($Engine -eq 'AlTool') {
        # Locate the AL Tool CLI: the requested -CompilerPath ('alc'), then the 'al'/'altool' command
        # from the cross-platform AL Tool dotnet global tool
        # (dotnet tool install --global Microsoft.Dynamics.BusinessCentral.Development.Tools). The 'al'
        # tool wraps alc.exe behind a 'compile' subcommand; a raw alc takes the arguments directly.
        $compiler = Get-Command -Name $CompilerPath -ErrorAction SilentlyContinue | Select-Object -First 1
        # Fall back to the AL Tool only when relying on the default name; an explicit -CompilerPath that
        # is not found is an error (don't silently substitute a different compiler).
        if (-not $compiler -and -not $PSBoundParameters.ContainsKey('CompilerPath')) {
            $compiler = @('al', 'altool') | ForEach-Object { Get-Command -Name $_ -ErrorAction SilentlyContinue } | Select-Object -First 1
        }
        if (-not $compiler) {
            throw "The AL Tool was not found (tried '$CompilerPath' and the 'al' command). Install it with: dotnet tool install --global Microsoft.Dynamics.BusinessCentral.Development.Tools (or pass -CompilerPath)."
        }

        $compilerBase = [System.IO.Path]::GetFileNameWithoutExtension($compiler.Source)
        $invokeArgs = if ($compilerBase -in @('al', 'altool')) { @('compile') + $compilerArgs } else { $compilerArgs }
        $result = Invoke-ALbuildProcess -FilePath $compiler.Source -Arguments $invokeArgs -PassThru
    }
    else {
        if (-not $ContainerName) { throw "-ContainerName is required when -Engine is Container." }
        $output = Invoke-BcContainerCommand -ContainerName $ContainerName -DockerExecutable $DockerExecutable -Variables @{
            Compiler = $ContainerCompilerPath
            CompilerArgs = $compilerArgs
        } -ScriptBlock {
            & $Compiler @CompilerArgs
            [PSCustomObject]@{ ExitCode = $LASTEXITCODE } | ConvertTo-Json
        }
        $exit = 0
        try { $exit = ($output | ConvertFrom-Json).ExitCode }
        catch { Write-Verbose "Could not parse container compiler exit code: $($_.Exception.Message)" }
        $result = [PSCustomObject]@{ Success = ($exit -eq 0); ExitCode = $exit; StdOut = $output; StdErr = '' }
    }

    $success = $result.Success -and (Test-Path -LiteralPath $outputFile)
    if ($result.StdOut) { Write-ALbuildLog $result.StdOut.TrimEnd() }

    if (-not $success) {
        Write-ALbuildLog -Level Error "Compilation of '$($appJson.name)' failed (exit $($result.ExitCode))."
    }
    else {
        Write-ALbuildLog -Level Success "Compiled '$outputFile'."
    }

    return [PSCustomObject]@{
        Success    = $success
        OutputFile = if ($success) { $outputFile } else { $null }
        ExitCode   = $result.ExitCode
        Output     = $result.StdOut
    }
}