Modules/businessdev.ALbuild.Apps/Public/Install-BcAlTool.ps1
|
function Install-BcAlTool { <# .SYNOPSIS Ensures the cross-platform AL Tool CLI is installed (as a .NET global tool) and on PATH. .DESCRIPTION Installs 'Microsoft.Dynamics.BusinessCentral.Development.Tools' as a dotnet global tool so the 'al' command (which wraps the AL compiler, alc) is available to Invoke-BcCompiler's AlTool engine on a build agent. Idempotent: when the AL Tool is already on PATH it does nothing (unless -Force). Requires the .NET SDK ('dotnet'). After installing it adds the global-tools folder (~/.dotnet/tools) to PATH for the current session so the freshly installed 'al' is immediately resolvable. .PARAMETER PackageId The dotnet tool package id. Default 'Microsoft.Dynamics.BusinessCentral.Development.Tools'. .PARAMETER Version Optional specific version to pin; otherwise the latest is installed. .PARAMETER Force Reinstall/update even when the AL Tool is already available. .PARAMETER DotNetExecutable The .NET CLI executable. Default 'dotnet'. .EXAMPLE Install-BcAlTool .OUTPUTS PSCustomObject: Installed (bool), Path, Version. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [string] $PackageId = 'Microsoft.Dynamics.BusinessCentral.Development.Tools', [string] $Version, [switch] $Force, [string] $DotNetExecutable = 'dotnet' ) # dotnet installs --global tools under <home>\.dotnet\tools. That folder is not always on PATH on # a fresh agent right after install, and some agents relocate the CLI home off the user profile # (DOTNET_CLI_HOME), so the tool can land outside %USERPROFILE%\.dotnet\tools. Consider every # candidate home and, as a backstop, locate the executable directly on disk. $toolDirs = @( @($env:DOTNET_CLI_HOME, $env:USERPROFILE, $HOME) | Where-Object { $_ } | ForEach-Object { Join-Path (Join-Path $_ '.dotnet') 'tools' } | Select-Object -Unique ) $ensurePath = { foreach ($dir in $toolDirs) { if ((Test-Path -LiteralPath $dir) -and (($env:PATH -split [System.IO.Path]::PathSeparator) -notcontains $dir)) { $env:PATH = $dir + [System.IO.Path]::PathSeparator + $env:PATH } } } # Resolve the AL Tool command from PATH, falling back to a direct scan of the global-tools dirs. # The direct scan covers a freshly installed tool whose folder PATH lookup has not picked up yet, # or one installed under a non-default home. $resolveAlTool = { $cmd = @('al', 'altool', 'alc') | ForEach-Object { Get-Command -Name $_ -ErrorAction SilentlyContinue } | Select-Object -First 1 if ($cmd) { return $cmd } foreach ($dir in $toolDirs) { foreach ($exe in @('al.exe', 'altool.exe', 'alc.exe', 'al', 'altool', 'alc')) { $candidate = Join-Path $dir $exe if (Test-Path -LiteralPath $candidate) { return (Get-Command -Name $candidate -ErrorAction SilentlyContinue) } } } return $null } & $ensurePath $existing = & $resolveAlTool if ($existing -and -not $Force) { Write-ALbuildLog "AL Tool already available: $($existing.Source)." return [PSCustomObject]@{ Installed = $false; Path = $existing.Source; Version = $existing.Version } } $dotnet = Get-Command -Name $DotNetExecutable -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $dotnet) { throw "The .NET SDK ('$DotNetExecutable') is required to install the AL Tool. Install the .NET SDK, or pass -CompilerPath to Invoke-BcCompiler / install '$PackageId' manually." } if ($PSCmdlet.ShouldProcess($PackageId, 'Install AL Tool (dotnet global tool)')) { $verb = if ($Force) { 'update' } else { 'install' } # --ignore-failed-sources so a single unreachable/auth-failing NuGet feed configured on the # agent (common with Azure Artifacts) does not abort the restore of a package available from # another source (e.g. nuget.org). $installArgs = @('tool', $verb, '--global', $PackageId, '--ignore-failed-sources') if ($Version) { $installArgs += @('--version', $Version) } # SuccessExitCodes 0,1 keeps Invoke-ALbuildProcess from throwing on the benign exit 1 that # `dotnet tool install` returns for "already installed"; we still inspect the result below so a # genuine exit-1 failure is surfaced instead of being reported as a successful install. $result = Invoke-ALbuildProcess -FilePath $dotnet.Source -Arguments $installArgs -PassThru -SuccessExitCodes @(0, 1) $combined = "$($result.StdOut)`n$($result.StdErr)".Trim() if ($combined) { Write-ALbuildLog "dotnet $($installArgs -join ' '):`n$combined" } $benign = $combined -match 'already installed|is up to date' if ($result.ExitCode -ne 0 -and -not $benign) { throw "Failed to install the AL Tool ('$PackageId') [dotnet exit $($result.ExitCode)]: $combined" } Write-ALbuildLog -Level Success "AL Tool '$PackageId' install step completed (dotnet exit $($result.ExitCode))." } & $ensurePath $al = & $resolveAlTool if (-not $al) { # Surface where we looked and what dotnet thinks is installed, so a path/relocation problem on # the agent is diagnosable from the failed build log instead of guesswork. $listing = '(unavailable)' try { $listing = (Invoke-ALbuildProcess -FilePath $dotnet.Source -Arguments @('tool', 'list', '--global') -PassThru).StdOut } catch { $listing = "(could not list global tools: $($_.Exception.Message))" } throw ("The AL Tool was installed but its 'al' command was not found. Searched PATH and: $($toolDirs -join ', '). " + "DOTNET_CLI_HOME='$($env:DOTNET_CLI_HOME)', USERPROFILE='$($env:USERPROFILE)'.`nInstalled global tools:`n$($listing.Trim())") } return [PSCustomObject]@{ Installed = $true; Path = $al.Source; Version = $al.Version } } |