Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/actions/verify-sqlclient/action.yml
Original file line number Diff line number Diff line change
@@ -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)"
3 changes: 3 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
19 changes: 16 additions & 3 deletions build/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"},
Expand All @@ -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
Expand All @@ -332,4 +345,4 @@ if ($env:GITHUB_ACTIONS -and $LASTEXITCODE -eq 0) {
exit 0
} else {
exit $LASTEXITCODE
}
}
220 changes: 206 additions & 14 deletions dbatools.library.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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<string>();

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<string>();
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: $_"
}
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
}
Loading