Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcCompilerServiceTier.ps1
|
function Get-BcCompilerServiceTier { <# .SYNOPSIS Locates the Business Central service tier that carries Microsoft's AL compiler. .DESCRIPTION Every BC artifact ships the complete AL compiler (Microsoft.Dynamics.Nav.CodeAnalysis.dll) in its platform part, next to the runtime engine. Driving that compiler in-process is what lets a runtime package be built WITHOUT a container - see New-BcRuntimePackageFromSource. Resolution order, most specific first: 1. -Path an explicit service tier folder. 2. -PlatformPath the platform part of a Get-BcArtifact result (what the factory already has). 3. the artifact caches on this machine, optionally filtered by -Version. Two properties of the result decide whether the compiler can actually be used here: TargetFramework read from the service's runtimeconfig.json. The BC major dictates it - BC 29 targets net10, BC 28 and 26 target net8. Hostable whether THIS PowerShell can load it. A net10 assembly cannot be loaded by a net8 process, so pwsh 7.4 cannot drive BC 29's compiler however much the artifact is present. Reported rather than discovered as a cryptic load error. .PARAMETER Path An explicit service tier folder. .PARAMETER PlatformPath The PlatformPath of a Get-BcArtifact result. .PARAMETER Version Version or prefix to match (e.g. '28' or '28.4') when searching the caches. .OUTPUTS PSCustomObject with Directory, Version, TargetFramework, Hostable, Source. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [string] $Path, [string] $PlatformPath, [string] $Version ) $compilerAssembly = 'Microsoft.Dynamics.Nav.CodeAnalysis.dll' # A service tier carries the compiler AND the engine. The Admin, Management and WebPublish # subfolders of an artifact carry the same assemblies and would otherwise pass - a machine then # reports twice as many "tiers" as it has, and loading from Admin fails later with nothing pointing # at the cause. Excluded by name, because the file check alone does not tell them apart. function Test-Tier([string] $candidate) { if ([string]::IsNullOrWhiteSpace($candidate)) { return $false } if (-not (Test-Path -LiteralPath $candidate)) { return $false } if ((Split-Path -Leaf $candidate) -in @('Admin', 'Management', 'WebPublish')) { return $false } foreach ($required in $compilerAssembly, 'Microsoft.Dynamics.Nav.Ncl.dll', 'Microsoft.Dynamics.Nav.Types.dll') { if (-not (Test-Path -LiteralPath (Join-Path $candidate $required))) { return $false } } return $true } # The install folder inside an artifact is NOT one fixed path: sandbox artifacts use # 'ServiceTier\PFiles64\...', on-premises artifacts use 'ServiceTier\program files\...'. Hardcoding # one of them silently finds nothing for the other - and the runtime factory builds against the # on-premises artifacts. function Find-TierIn([string] $platformRoot) { $result = [System.Collections.Generic.List[string]]::new() $serviceTier = Join-Path $platformRoot 'ServiceTier' if (-not (Test-Path -LiteralPath $serviceTier)) { return , $result } foreach ($programFiles in (Get-ChildItem -LiteralPath $serviceTier -Directory -ErrorAction SilentlyContinue)) { $base = Join-Path $programFiles.FullName 'Microsoft Dynamics NAV' if (-not (Test-Path -LiteralPath $base)) { continue } foreach ($major in (Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue)) { $service = Join-Path $major.FullName 'Service' if (Test-Tier $service) { $result.Add($service) } } } return , $result } function New-TierInfo([string] $directory, [string] $source) { $bcVersion = $null $ncl = Join-Path $directory 'Microsoft.Dynamics.Nav.Ncl.dll' try { $bcVersion = [System.Reflection.AssemblyName]::GetAssemblyName($ncl).Version } catch { } # The service's own runtimeconfig names the framework it was built for. Guessing from the BC # major would be a rule that quietly goes wrong the next time Microsoft moves. $tfm = $null $runtimeConfig = Join-Path $directory 'Microsoft.Dynamics.Nav.Server.runtimeconfig.json' if (Test-Path -LiteralPath $runtimeConfig) { try { $tfm = (Get-Content -LiteralPath $runtimeConfig -Raw | ConvertFrom-Json).runtimeOptions.tfm } catch { } } # No runtimeconfig at all means the tier predates .NET (Core) - BC 17 and older are .NET # Framework, and a Framework build simply has no such file. Reporting an empty framework there # was worse than useless: the log read '(, no container)' and 'Hostable' came out true because # nothing was known, not because anything had been established. # # Measured on BC 17.17.38111.0: the compiler assembly targets .NET Framework 4.8 and still # loads AND completes a full emit under a .NET 10 pwsh, so Framework stays hostable. That is a # measurement, not a rule about every Framework assembly. if (-not $tfm -and (Test-Path -LiteralPath (Join-Path $directory 'Microsoft.Dynamics.Nav.Server.exe.config'))) { $tfm = '.NETFramework' } $hostMajor = [System.Environment]::Version.Major $tierMajor = if ($tfm -and $tfm -match '^net(\d+)\.') { [int]$Matches[1] } else { $null } [PSCustomObject]@{ Directory = $directory Version = $bcVersion TargetFramework = $tfm # A newer assembly cannot load in an older runtime; the other direction is fine. Hostable = ($null -eq $tierMajor) -or ($tierMajor -le $hostMajor) HostFramework = "net$hostMajor.0" Source = $source } } if ($PSBoundParameters.ContainsKey('Path') -and $Path) { if (-not (Test-Tier $Path)) { throw "'$Path' is not a Business Central service tier: it must contain $compilerAssembly next to Microsoft.Dynamics.Nav.Ncl.dll and Microsoft.Dynamics.Nav.Types.dll." } return New-TierInfo $Path 'explicit' } $roots = [System.Collections.Generic.List[string]]::new() if ($PlatformPath) { $roots.Add($PlatformPath) } if ($roots.Count -eq 0) { if ($env:ALBUILD_BC_SERVICE_TIER -and (Test-Tier $env:ALBUILD_BC_SERVICE_TIER)) { return New-TierInfo $env:ALBUILD_BC_SERVICE_TIER 'ALBUILD_BC_SERVICE_TIER' } # The artifact caches only exist on Windows - a BC artifact is a Windows install tree. Scanning # for them elsewhere is not merely pointless, it THROWS: Join-Path with a drive-qualified parent # ('C:') raises DriveNotFoundException on Linux, which took the module's Linux test job down at # discovery time (build 27411). RuntimeInformation rather than $IsWindows, because $IsWindows # does not exist in Windows PowerShell 5.1 and reading it under StrictMode throws there. $onWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( [System.Runtime.InteropServices.OSPlatform]::Windows) $caches = @() if ($onWindows) { $systemDrive = if ($env:SystemDrive) { $env:SystemDrive } else { 'C:' } $caches = @( 'C:\bcartifacts.cache', ($systemDrive + '\alb\artifacts'), (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'ALbuild\artifacts')) } # The same caches ALbuild already fills. A machine that has ever built a BC app has one. foreach ($cache in $caches) { if (-not (Test-Path -LiteralPath $cache)) { continue } # <cache>\<type>\<version>\platform - bounded, not a recursive walk of a cache that is # tens of gigabytes. foreach ($platform in (Get-ChildItem -LiteralPath $cache -Directory -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue } | ForEach-Object { Join-Path $_.FullName 'platform' } | Where-Object { Test-Path -LiteralPath $_ })) { $roots.Add($platform) } } } $found = [System.Collections.Generic.List[object]]::new() foreach ($root in $roots) { # An explicitly passed platform path may already BE the service folder. if (Test-Tier $root) { $found.Add((New-TierInfo $root 'platform')); continue } foreach ($service in (Find-TierIn $root)) { $found.Add((New-TierInfo $service 'artifact')) } } if ($Version) { $found = [System.Collections.Generic.List[object]]@($found | Where-Object { $_.Version -and ("$($_.Version)" -eq $Version -or "$($_.Version)".StartsWith("$Version.")) }) } if ($found.Count -eq 0) { return $null } # Hostable first, then newest: a tier this process cannot load is never the better answer. return ($found | Sort-Object -Property @{Expression = { -not $_.Hostable } }, @{Expression = { $_.Version }; Descending = $true } | Select-Object -First 1) } |