Modules/businessdev.ALbuild.Apps/Private/Get-BcAlToolAssemblyFolder.ps1
|
function Get-BcAlToolAssemblyFolder { <# .SYNOPSIS Returns the folder of the assemblies an AL Tool executable actually runs. .DESCRIPTION Internal helper. The AL Tool package ships one build per target framework - AL Tool 18 carries tools/net8.0/any AND tools/net10.0/any, each with its own copy of CodeCop, UICop, AppSourceCop and PerTenantExtensionCop. 'dotnet tool install' picks ONE of them for the 'al' launcher, and which one depends on the SDK doing the install (a net8 SDK takes net8.0, a net10 SDK net10.0). Analyzers have to come from that same folder. A net10 analyzer loaded into a net8 compiler fails on 'System.Collections.Immutable, Version=10.0.0.0' for every single rule, the compiler reports that as a warning (AL1003) and compiles on without any analyzer - which is what every BC 29 build did while a recursive search of the package store handed out tools/net10.0 simply because it sorts before tools/net8.0. The launcher is a .NET apphost: it carries the relative path of the assembly it starts ('.store/<package>/<version>/.../tools/<tfm>/any/altool.dll') as a UTF-8 string, on Windows and on Linux alike. That path is read here rather than inferred from the installed SDKs or runtimes, because it is the one statement that cannot disagree with what runs. A compiler that is not a launcher (a plain alc.exe next to its own analyzers, as in the AL Language extension) is answered with its own folder when the analyzers are there. .PARAMETER CompilerPath Path of the AL Tool executable ('al.exe', 'al', 'alc.exe', ...). .OUTPUTS System.String: the folder, or $null when it cannot be determined. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $CompilerPath ) if (-not (Test-Path -LiteralPath $CompilerPath -PathType Leaf)) { return $null } $compilerFolder = Split-Path -Parent $CompilerPath # The embedded path is printable ASCII ('.store/<package id>/<version>/...'), so Latin-1 maps every # byte 1:1 and the match is confined to printable ASCII - the binary bytes around it cannot bleed # into the path. Launchers are a few hundred KB; reading one whole is cheaper than being clever. $bytes = [System.IO.File]::ReadAllBytes($CompilerPath) $text = [System.Text.Encoding]::GetEncoding(28591).GetString($bytes) $match = [regex]::Match($text, '(?<p>[\x20-\x7E-["<>|]]*?tools[\\/]net[0-9.]+[\\/]any[\\/][\x20-\x7E-["<>|\\/]]+\.dll)') if ($match.Success) { $relative = $match.Groups['p'].Value -replace '[\\/]', [System.IO.Path]::DirectorySeparatorChar $target = if ([System.IO.Path]::IsPathRooted($relative)) { $relative } else { Join-Path $compilerFolder $relative } if (Test-Path -LiteralPath $target -PathType Leaf) { return (Split-Path -Parent $target) } } if (Test-Path -LiteralPath (Join-Path $compilerFolder 'Microsoft.Dynamics.Nav.CodeCop.dll')) { return $compilerFolder } return $null } |