diff --git a/.github/actions/verify-sqlclient/action.yml b/.github/actions/verify-sqlclient/action.yml new file mode 100644 index 00000000..681ea1fa --- /dev/null +++ b/.github/actions/verify-sqlclient/action.yml @@ -0,0 +1,48 @@ +name: Verify SqlClient runtime asset +description: Verifies that Microsoft.Data.SqlClient loads from the platform-specific runtime asset. + +inputs: + module-path: + description: Path to the dbatools.library module manifest. + required: false + default: ./artifacts/dbatools.library/dbatools.library.psd1 + core-lib-path: + description: Path to the built core/lib directory. + required: false + default: ./artifacts/dbatools.library/core/lib + +runs: + using: composite + steps: + - name: Verify SqlClient runtime asset selection + shell: pwsh + run: | + Import-Module "${{ inputs.module-path }}" -Force + + $sqlClient = [System.AppDomain]::CurrentDomain.GetAssemblies() | + Where-Object { $_.GetName().Name -eq 'Microsoft.Data.SqlClient' } | + Select-Object -First 1 + + if (-not $sqlClient) { + throw "Microsoft.Data.SqlClient was not loaded" + } + + $location = $sqlClient.Location.Replace('/', '\') + $expected = if ($IsWindows) { + "runtimes\win\lib\net8.0\Microsoft.Data.SqlClient.dll" + } else { + "runtimes\unix\lib\net8.0\Microsoft.Data.SqlClient.dll" + } + + if ($location -notlike "*$expected") { + throw "Microsoft.Data.SqlClient loaded from unexpected path: $($sqlClient.Location)" + } + + if ($IsWindows) { + $sniPath = Join-Path "${{ inputs.core-lib-path }}" "runtimes/win-x64/native/Microsoft.Data.SqlClient.SNI.dll" + if (-not (Test-Path $sniPath)) { + throw "Missing Windows x64 SqlClient native SNI DLL at $sniPath" + } + } + + Write-Host "SqlClient runtime asset verified: $($sqlClient.Location)" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1fc0552d..6e1fea1b 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -201,6 +201,9 @@ jobs: New-Item -ItemType Directory -Path /tmp/DbatoolsExport -Force | Out-Null } + - name: Verify SqlClient runtime asset selection + uses: ./.github/actions/verify-sqlclient + - name: Install SqlPackagetests shell: pwsh run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d4b3a0d..e375ed83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,6 +117,9 @@ jobs: Write-Host "Loaded dbatools.library version: $($module.Version)" Write-Host "Module path: $($module.Path)" + - name: Verify SqlClient runtime asset selection + uses: ./.github/actions/verify-sqlclient + - name: Test AvoidConflicts with SqlServer module (Windows PowerShell 5.1) shell: powershell run: | diff --git a/build/build.ps1 b/build/build.ps1 index deaf134a..0a937275 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -134,8 +134,6 @@ if (Test-Path $coreRuntimesPath) { } } -Copy-Item (Join-Path $libPath "core\lib\runtimes\unix\lib\net8.0\Microsoft.Data.SqlClient.dll") -Destination (Join-Path $libPath "core/lib/") -Force - if ($CoreOnly) { Write-Host "CoreOnly specified - returning after core build" return @@ -296,6 +294,8 @@ $validationErrors = @() # Check for critical runtime dependencies $criticalFiles = @( @{Path = "core\lib\runtimes\win-x64\native\Microsoft.Data.SqlClient.SNI.dll"; Description = ".NET Core SNI DLL (Windows x64)"}, + @{Path = "core\lib\runtimes\win\lib\net8.0\Microsoft.Data.SqlClient.dll"; Description = ".NET Core SqlClient (Windows runtime asset)"}, + @{Path = "core\lib\runtimes\unix\lib\net8.0\Microsoft.Data.SqlClient.dll"; Description = ".NET Core SqlClient (Unix runtime asset)"}, @{Path = "core\lib\Microsoft.Data.SqlClient.dll"; Description = ".NET Core SqlClient"}, @{Path = "desktop\lib\Microsoft.Data.SqlClient.dll"; Description = ".NET Framework SqlClient"}, @{Path = "core\lib\dbatools.dll"; Description = ".NET Core dbatools assembly"}, @@ -312,6 +312,19 @@ foreach ($file in $criticalFiles) { } } +$coreSqlClientPath = Join-Path $libPath "core\lib\Microsoft.Data.SqlClient.dll" +$coreUnixSqlClientPath = Join-Path $libPath "core\lib\runtimes\unix\lib\net8.0\Microsoft.Data.SqlClient.dll" +if ((Test-Path $coreSqlClientPath) -and (Test-Path $coreUnixSqlClientPath)) { + $coreSqlClientHash = (Get-FileHash -Path $coreSqlClientPath -Algorithm SHA256).Hash + $coreUnixSqlClientHash = (Get-FileHash -Path $coreUnixSqlClientPath -Algorithm SHA256).Hash + if ($coreSqlClientHash -eq $coreUnixSqlClientHash) { + Write-Host "[ERROR] core\lib\Microsoft.Data.SqlClient.dll matches the Unix runtime asset" -ForegroundColor Red + $validationErrors += "Core SqlClient was overwritten with the Unix runtime asset" + } else { + Write-Host "[OK] Core SqlClient root assembly was not overwritten with the Unix runtime asset" -ForegroundColor Green + } +} + # Check if runtimes folder structure exists if (Test-Path (Join-Path $libPath "core\lib\runtimes")) { Write-Host "[OK] Runtimes folder structure preserved" -ForegroundColor Green @@ -332,4 +345,4 @@ if ($env:GITHUB_ACTIONS -and $LASTEXITCODE -eq 0) { exit 0 } else { exit $LASTEXITCODE -} \ No newline at end of file +} diff --git a/dbatools.library.psm1 b/dbatools.library.psm1 index 99cdacd7..2fb561c9 100644 --- a/dbatools.library.psm1 +++ b/dbatools.library.psm1 @@ -90,26 +90,31 @@ if ($PSVersionTable.PSEdition -ne "Core") { # PowerShell Core: Use AssemblyLoadContext.Resolving event for version redirection # This handles version mismatches when SqlServer module loads different versions of assemblies # IMPORTANT: Must be implemented in C# because the resolver runs on .NET threads without PowerShell runspaces - $dir = [System.IO.Path]::Combine($script:libraryroot, "lib") - $dir = ("$dir" + [System.IO.Path]::DirectorySeparatorChar).Replace('\', '\\') + $dir = [System.IO.Path]::Combine($script:libraryroot, "lib") + [System.IO.Path]::DirectorySeparatorChar if (-not ("CoreRedirector" -as [type])) { $coreSource = @" using System; + using System.Collections.Generic; + using System.Diagnostics; using System.IO; using System.Reflection; + using System.Runtime.InteropServices; using System.Runtime.Loader; public class CoreRedirector { private static string _libPath; private static bool _registered = false; + private static readonly string _platformRid = ComputePlatformRid(); + private static readonly string _architectureRid = ComputeArchitectureRid(); public static void Register(string libPath) { if (_registered) return; _libPath = libPath; AssemblyLoadContext.Default.Resolving += OnResolving; + AssemblyLoadContext.Default.ResolvingUnmanagedDll += OnResolvingUnmanagedDll; _registered = true; } @@ -134,27 +139,164 @@ if ($PSVersionTable.PSEdition -ne "Core") { } } - // Try to load from our lib folder if the file exists - string dllPath = _libPath + name + ".dll"; - if (File.Exists(dllPath)) + foreach (string dllPath in GetManagedAssemblyPaths(name)) { - try + if (File.Exists(dllPath)) { - return AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath); + try + { + return AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath); + } + catch (Exception ex) + { + Trace.TraceWarning("Failed to load managed assembly from '{0}': {1}", dllPath, ex.Message); + // Failed to load, try the next candidate + } } - catch + } + + return null; + } + + // This resolver is global to the load context, but it only returns module-owned native assets from _libPath. + private static IntPtr OnResolvingUnmanagedDll(Assembly assembly, string libraryName) + { + if (String.IsNullOrEmpty(_architectureRid)) + { + return IntPtr.Zero; + } + + foreach (string fileName in GetNativeLibraryNames(libraryName)) + { + string nativePath = Path.Combine(_libPath, "runtimes", _architectureRid, "native", fileName); + if (File.Exists(nativePath)) { - // Failed to load, return null to let default resolution continue + try + { + return NativeLibrary.Load(nativePath); + } + catch (Exception ex) + { + Trace.TraceWarning("Failed to load native library from '{0}': {1}", nativePath, ex.Message); + // Failed to load, try the next candidate + } } } - return null; + return IntPtr.Zero; + } + + private static string[] GetManagedAssemblyPaths(string name) + { + string fileName = name + ".dll"; + + var paths = new List(); + + if (!String.IsNullOrEmpty(_platformRid)) + { + paths.Add(Path.Combine(_libPath, "runtimes", _platformRid, "lib", "net8.0", fileName)); + } + + if (!String.IsNullOrEmpty(_architectureRid)) + { + paths.Add(Path.Combine(_libPath, "runtimes", _architectureRid, "lib", "net8.0", fileName)); + } + + paths.Add(Path.Combine(_libPath, fileName)); + + return paths.ToArray(); + } + + private static string[] GetNativeLibraryNames(string libraryName) + { + var names = new List(); + names.Add(libraryName); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (!libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + names.Add(libraryName + ".dll"); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (!libraryName.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase)) + { + names.Add(libraryName + ".dylib"); + names.Add("lib" + libraryName + ".dylib"); + } + } + else + { + if (!libraryName.EndsWith(".so", StringComparison.OrdinalIgnoreCase)) + { + names.Add(libraryName + ".so"); + names.Add("lib" + libraryName + ".so"); + } + } + + return names.ToArray(); + } + + private static string ComputePlatformRid() + { + // Managed runtime assets used by the module, especially SqlClient, ship + // win/unix folders. OS-specific native assets are handled by ComputeArchitectureRid. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "win"; + } + + return "unix"; + } + + private static string ComputeArchitectureRid() + { + string osPart; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + osPart = "win"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + osPart = "linux"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + osPart = "osx"; + } + else + { + return null; + } + + string architecture; + switch (RuntimeInformation.ProcessArchitecture) + { + case Architecture.X86: + architecture = "x86"; + break; + case Architecture.X64: + architecture = "x64"; + break; + case Architecture.Arm: + architecture = "arm"; + break; + case Architecture.Arm64: + architecture = "arm64"; + break; + default: + return null; + } + + return osPart + "-" + architecture; } } "@ try { - $null = Add-Type -TypeDefinition $coreSource -ReferencedAssemblies 'System.Runtime.Loader' + $null = Add-Type -TypeDefinition $coreSource -ReferencedAssemblies 'System.Runtime.Loader','System.Runtime.InteropServices','System.Collections','System.Diagnostics.TraceSource' } catch { Write-Verbose "Could not compile CoreRedirector: $_" } @@ -167,8 +309,58 @@ if ($PSVersionTable.PSEdition -ne "Core") { } } -# REMOVED win-sqlclient logic - SqlClient is now directly in lib -$sqlclient = [System.IO.Path]::Combine($script:libraryroot, "lib", "Microsoft.Data.SqlClient.dll") +function Get-DbatoolsSqlClientPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$LibraryRoot + ) + + $libPath = [System.IO.Path]::Combine($LibraryRoot, "lib") + if ($PSVersionTable.PSEdition -eq "Core") { + if ($IsWindows) { + $runtimeSqlClient = [System.IO.Path]::Combine($libPath, "runtimes", "win", "lib", "net8.0", "Microsoft.Data.SqlClient.dll") + } else { + $runtimeSqlClient = [System.IO.Path]::Combine($libPath, "runtimes", "unix", "lib", "net8.0", "Microsoft.Data.SqlClient.dll") + } + + if (Test-Path $runtimeSqlClient) { + return $runtimeSqlClient + } + } + + [System.IO.Path]::Combine($libPath, "Microsoft.Data.SqlClient.dll") +} + +function Add-DbatoolsNativeSearchPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$LibraryRoot + ) + + if ($PSVersionTable.PSEdition -ne "Core" -or -not $IsWindows) { + return + } + + $architecture = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant() + $nativeRid = "win-$architecture" + $nativePath = [System.IO.Path]::Combine($LibraryRoot, "lib", "runtimes", $nativeRid, "native") + if (-not (Test-Path $nativePath)) { + return + } + + $pathSeparator = [System.IO.Path]::PathSeparator + $pathParts = $env:PATH -split [Regex]::Escape([string]$pathSeparator) + if ($pathParts -contains $nativePath) { + return + } + + $env:PATH = $nativePath + $pathSeparator + $env:PATH +} + +Add-DbatoolsNativeSearchPath -LibraryRoot $script:libraryroot +$sqlclient = Get-DbatoolsSqlClientPath -LibraryRoot $script:libraryroot # Get loaded assemblies once for reuse (used for AvoidConflicts checks and later assembly loading) $script:loadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() @@ -328,4 +520,4 @@ foreach ($name in $names) { if ($PSVersionTable.PSEdition -ne "Core" -and $redirector) { # Store the redirector in script scope so it stays alive and can be accessed if needed $script:assemblyRedirector = $redirector -} \ No newline at end of file +}