From 668e4d8268fced9092d3eb714d43c1d10b855b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 10 Jun 2026 12:53:54 +0200 Subject: [PATCH 1/3] Collect MSBuild node-communication diagnostics in CodeQL pipeline Set MSBUILDDEBUGCOMM=1 and MSBUILDDEBUGPATH to artifacts/msbuild-debug for the Windows Build step, then publish the contents of that folder along with the build binlogs as pipeline artifacts so the MSBuild team can investigate the reported issue (cc Rainer Sigwald). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines-codeql.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/azure-pipelines-codeql.yml b/azure-pipelines-codeql.yml index e19d3ac96a..461c1e2ab4 100644 --- a/azure-pipelines-codeql.yml +++ b/azure-pipelines-codeql.yml @@ -66,11 +66,37 @@ jobs: - task: CodeQL3000Init@0 displayName: CodeQL Initialize + # Pre-create the folder so MSBuild has a stable, well-known location to dump + # MSBUILDDEBUGCOMM diagnostics, and so the publish step always has something + # to upload (even if MSBuild ends up not writing any files). + - powershell: | + New-Item -ItemType Directory -Force -Path '$(Build.SourcesDirectory)\artifacts\msbuild-debug' | Out-Null + displayName: 'Prepare MSBuild debug folder' + - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine /p:Test=false displayName: Windows Build + # MSBUILDDEBUGCOMM/MSBUILDDEBUGPATH are set to collect MSBuild node-communication + # diagnostics for investigation by the MSBuild team (cc Rainer Sigwald). + env: + MSBUILDDEBUGCOMM: 1 + MSBUILDDEBUGPATH: $(Build.SourcesDirectory)\artifacts\msbuild-debug - task: CodeQL3000Finalize@0 displayName: CodeQL Finalize + + - task: PublishBuildArtifacts@1 + displayName: 'Publish MSBuild debug logs' + inputs: + PathtoPublish: '$(Build.SourcesDirectory)\artifacts\msbuild-debug' + ArtifactName: MSBuild_Debug_Logs_Attempt$(System.JobAttempt) + condition: always() + + - task: PublishBuildArtifacts@1 + displayName: 'Publish build binlogs' + inputs: + PathtoPublish: '$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)' + ArtifactName: Build_Binlogs_$(_BuildConfig)_Attempt$(System.JobAttempt) + condition: always() From 4164b8395de9d7d7f207e7f7d84a0488207edd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 11 Jun 2026 19:37:02 +0200 Subject: [PATCH 2/3] Try arcade#17002 workaround in eng/Versions.props instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt put the SDK-root drive-casing workaround in Directory.Build.targets, but the Arcade orchestrator/toolset project — which is where the cross-runtime task-host handshake first fires (InstallDotNetCore etc.) — does NOT import the repo's Directory.Build.targets. It DOES import eng/Versions.props via the Arcade SDK chain, so moving the InitialTargets + UsingTask + Target there makes the target actually run before the first .NET task host launch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 156 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 6 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 1b53d2cfd3..4a3acb57bc 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,4 +1,4 @@ - + 4.3.0 @@ -7,10 +7,154 @@ preview - 11.0.0-beta.26309.4 - 18.9.0-preview.26309.4 + 11.0.0-beta.26310.1 + 18.9.0-preview.26310.1 - 4.3.0-preview.26309.6 - 2.3.0-preview.26309.6 + 4.3.0-preview.26310.6 + 2.3.0-preview.26310.6 - + + + + + + + + + + = 2 && canonical[1] == ':') + { + char canonicalDrive = canonical[0]; + if (canonicalDrive != SdkRoot[0]) + { + Result = canonicalDrive + SdkRoot.Substring(1); + Log.LogMessage( + MessageImportance.High, + "Normalized NetCoreSdkRoot drive casing '{0}' -> '{1}' to match Environment.ProcessPath (#14026 workaround).", + SdkRoot[0], + canonicalDrive); + } + } + } + finally + { + FreeLibrary(module); + } + } + catch (Exception ex) + { + Log.LogMessage(MessageImportance.Low, "NormalizeSdkRootDriveCasing skipped: {0}", ex.Message); + } + + return true; + } +} +]]> + + + + + + + + + \ No newline at end of file From 3001ebb0e900b49b58fac7524cfd992c5fa54731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 23 Jun 2026 15:10:39 +0200 Subject: [PATCH 3/3] Reset CodeQL pipeline and switch to dotnet-host-probe SDK drive-casing workaround Reset azure-pipelines-codeql.yml to its state at 5857015 (drop the MSBuild node-communication diagnostics) and replace the LoadLibraryEx-based #14026 NetCoreSdkRoot drive-casing workaround in eng/Versions.props with the dotnet-host-probe implementation from #9371. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines-codeql.yml | 33 ----- eng/Versions.props | 280 +++++++++++++++++++++++++++---------- 2 files changed, 209 insertions(+), 104 deletions(-) diff --git a/azure-pipelines-codeql.yml b/azure-pipelines-codeql.yml index 461c1e2ab4..493634d64e 100644 --- a/azure-pipelines-codeql.yml +++ b/azure-pipelines-codeql.yml @@ -48,13 +48,6 @@ jobs: steps: - # NOTE: Do not add an explicit `UseDotNet@2` with `useGlobalJson: true` here. - # global.json pins a preview SDK (11.0.x-preview.*) and the `UseDotNet@2` task - # does not search preview channels by default, so it fails with - # "sdk version matching: 11.0.x could not be found". `eng\common\cibuild.cmd` - # (invoked below) restores the correct SDK from global.json via `dotnet-install.ps1`, - # which handles preview versions correctly. - - task: PowerShell@2 displayName: 'Install Windows SDK' inputs: @@ -66,37 +59,11 @@ jobs: - task: CodeQL3000Init@0 displayName: CodeQL Initialize - # Pre-create the folder so MSBuild has a stable, well-known location to dump - # MSBUILDDEBUGCOMM diagnostics, and so the publish step always has something - # to upload (even if MSBuild ends up not writing any files). - - powershell: | - New-Item -ItemType Directory -Force -Path '$(Build.SourcesDirectory)\artifacts\msbuild-debug' | Out-Null - displayName: 'Prepare MSBuild debug folder' - - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine /p:Test=false displayName: Windows Build - # MSBUILDDEBUGCOMM/MSBUILDDEBUGPATH are set to collect MSBuild node-communication - # diagnostics for investigation by the MSBuild team (cc Rainer Sigwald). - env: - MSBUILDDEBUGCOMM: 1 - MSBUILDDEBUGPATH: $(Build.SourcesDirectory)\artifacts\msbuild-debug - task: CodeQL3000Finalize@0 displayName: CodeQL Finalize - - - task: PublishBuildArtifacts@1 - displayName: 'Publish MSBuild debug logs' - inputs: - PathtoPublish: '$(Build.SourcesDirectory)\artifacts\msbuild-debug' - ArtifactName: MSBuild_Debug_Logs_Attempt$(System.JobAttempt) - condition: always() - - - task: PublishBuildArtifacts@1 - displayName: 'Publish build binlogs' - inputs: - PathtoPublish: '$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)' - ArtifactName: Build_Binlogs_$(_BuildConfig)_Attempt$(System.JobAttempt) - condition: always() diff --git a/eng/Versions.props b/eng/Versions.props index 4a3acb57bc..a9b5a211ac 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -15,44 +15,37 @@ + TEMPORARY WORKAROUND for https://github.com/dotnet/msbuild/issues/14026 (MSB4216). + + On some hosted CI agents the .NET Framework MSBuild host and the .NET task host child derive the + SDK tools directory with different drive-letter casing ("D:" vs "d:"). Because the task host + handshake salt is a case-sensitive hash of that path, the salts differ and the .NET task host + handshake fails with MSB4216 (seen here in Arcade's InstallDotNetCore during the CodeQL build). + + This rewrites ONLY the drive letter of $(NetCoreSdkRoot) to the casing the child will use, + learned by launching the SDK's dotnet host (dotnet.exe, same volume as the SDK) and reading + GetModuleFileNameW(NULL) - the API behind the child's Environment.ProcessPath. Casing-only and + safe on every agent. Knobs: NetCoreSdkRootDriveCasingOverride=lower|upper, + DisableNetCoreSdkRootDriveCasingWorkaround=true. + This is a temporary stopgap; remove it once the permanent fix (dotnet/arcade#17039, building on + dotnet/arcade#17002, and the child-side dotnet/msbuild#14027) flows into this repo's Arcade and SDK. + --> + s_driveCache = new Dictionary(); public override bool Execute() { + // Best-effort: never fail the build over a casing tweak. Default to a no-op. Result = SdkRoot; try @@ -88,63 +79,210 @@ public class NormalizeSdkRootDriveCasing : Task return true; } - string image = null; - foreach (string candidate in new[] { "MSBuild.exe", "MSBuild.dll" }) + // Deterministic escape hatch: the NetCoreSdkRootDriveCasingOverride property = "lower" or + // "upper" forces the drive-letter casing without probing the SDK, for emergencies or testing. + if (!string.IsNullOrEmpty(Override)) { - string p = Path.Combine(SdkRoot, candidate); - if (File.Exists(p)) + char forced = SdkRoot[0]; + if (string.Equals(Override, "lower", StringComparison.OrdinalIgnoreCase)) + { + forced = char.ToLowerInvariant(SdkRoot[0]); + } + else if (string.Equals(Override, "upper", StringComparison.OrdinalIgnoreCase)) { - image = p; - break; + forced = char.ToUpperInvariant(SdkRoot[0]); } + + if (forced != SdkRoot[0]) + { + Result = forced + SdkRoot.Substring(1); + Log.LogMessage(MessageImportance.High, + "Forced NetCoreSdkRoot drive casing '{0}' -> '{1}' via NetCoreSdkRootDriveCasingOverride='{2}' (#14026 workaround).", + SdkRoot[0], forced, Override); + } + + return true; } - if (image == null) + char registeredDrive = GetRegisteredSdkDrive(SdkRoot); + // Casing-only guard: only adopt the probed drive when it is the SAME letter as the SDK's + // (differing solely in case). This can never change the actual drive, only its casing. + if (registeredDrive != '\0' && + registeredDrive != SdkRoot[0] && + char.ToUpperInvariant(registeredDrive) == char.ToUpperInvariant(SdkRoot[0])) { - return true; + Result = registeredDrive + SdkRoot.Substring(1); + Log.LogMessage(MessageImportance.High, + "Normalized NetCoreSdkRoot drive casing '{0}' -> '{1}' to match the SDK task host (#14026 workaround).", + SdkRoot[0], registeredDrive); } + } + catch (Exception ex) + { + Log.LogMessage(MessageImportance.Low, "NormalizeSdkRootDriveCasing skipped: {0}", ex.Message); + } + + return true; + } - IntPtr module = LoadLibraryExW(image, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); - if (module == IntPtr.Zero) + // Returns the volume-registered drive letter for the SDK, derived the same way the .NET task host + // child does: by launching the SDK's dotnet host and reading the drive-letter casing + // GetModuleFileNameW(NULL) - the API behind Environment.ProcessPath - resolves for that process. + // Returns '\0' if it cannot be determined. + private char GetRegisteredSdkDrive(string sdkRoot) + { + char driveKey = sdkRoot[0]; + lock (s_lock) + { + if (s_driveCache.TryGetValue(driveKey, out char cached)) { - return true; + return cached; } - try + char probed = ProbeSdkDrive(sdkRoot); + s_driveCache[driveKey] = probed; + return probed; + } + } + + // Finds the dotnet host (dotnet.exe) ON THE SAME VOLUME as the SDK, so its drive-letter casing + // matches what the task host child will report. Prefers the dotnet root derived from the SDK + // directory (\sdk\\ -> \dotnet.exe), then DOTNET_HOST_PATH but only + // if it is on the same drive letter as the SDK. Returns null if none exists. + private static string LocateDotnetHost(string sdkRoot) + { + try + { + // sdkRoot = \sdk\\ -> up two directories = . + string sdkVersionDir = sdkRoot.TrimEnd('\\', '/'); + string sdkDir = Path.GetDirectoryName(sdkVersionDir); + string dotnetRoot = Path.GetDirectoryName(sdkDir); + if (!string.IsNullOrEmpty(dotnetRoot)) { - var sb = new StringBuilder(1024); - uint len = GetModuleFileNameW(module, sb, (uint)sb.Capacity); - if (len == 0) + string candidate = Path.Combine(dotnetRoot, "dotnet.exe"); + if (File.Exists(candidate)) { - return true; + return candidate; + } + } + } + catch + { + // fall through to DOTNET_HOST_PATH + } + + string fromEnv = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(fromEnv) && fromEnv.Length >= 2 && fromEnv[1] == ':' && + char.ToUpperInvariant(fromEnv[0]) == char.ToUpperInvariant(sdkRoot[0]) && + File.Exists(fromEnv) && + fromEnv.EndsWith("dotnet.exe", StringComparison.OrdinalIgnoreCase)) + { + return fromEnv; + } + + return null; + } + + private char ProbeSdkDrive(string sdkRoot) + { + // Launch the dotnet host (NOT the SDK's MSBuild.exe apphost, which needs a matching runtime + // that may not be resolvable in this process's environment). dotnet.exe is the runtime host + // itself, lives on the same volume as the SDK, and always launches; its own process-path + // drive-letter casing (GetModuleFileNameW) therefore equals the casing the .NET task host + // child will report for the SDK directory. + string dotnetExe = LocateDotnetHost(sdkRoot); + if (dotnetExe == null) + { + Log.LogMessage(MessageImportance.Low, "NormalizeSdkRootDriveCasing: dotnet host not found for SDK '{0}'.", sdkRoot); + return '\0'; + } + + string tasksDll = Path.Combine(sdkRoot, "Microsoft.Build.Tasks.Core.dll"); + string tempDir = Path.Combine(Path.GetTempPath(), "sdkdrvprobe_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + string proj = Path.Combine(tempDir, "probe.proj"); + + // Bare (no Sdk attribute / no imports) so it does NOT re-import Arcade and cannot + // recurse into this workaround. The inline task P/Invokes GetModuleFileNameW(NULL) - the exact + // API behind the child task host's Environment.ProcessPath - so we read the same drive-letter + // casing the child will. No CDATA in the inner task code (it would close the outer CDATA). + var probe = new StringBuilder(); + probe.Append(""); + probe.Append(""); + probe.Append("\n"); + probe.Append("using System;\n"); + probe.Append("using System.Runtime.InteropServices;\n"); + probe.Append("using System.Text;\n"); + probe.Append("using Microsoft.Build.Framework;\n"); + probe.Append("using Microsoft.Build.Utilities;\n"); + probe.Append("public class PrintProcPath : Task {\n"); + probe.Append(" [DllImport(\"kernel32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n"); + probe.Append(" static extern uint GetModuleFileNameW(IntPtr hModule, StringBuilder lpFilename, uint nSize);\n"); + probe.Append(" public override bool Execute() {\n"); + probe.Append(" var sb = new StringBuilder(1024);\n"); + probe.Append(" GetModuleFileNameW(IntPtr.Zero, sb, 1024u);\n"); + probe.Append(" Log.LogMessage(MessageImportance.High, \"").Append(Marker).Append("\" + sb.ToString());\n"); + probe.Append(" return true;\n"); + probe.Append(" }\n"); + probe.Append("}\n"); + probe.Append(""); + probe.Append(""); + probe.Append(""); + File.WriteAllText(proj, probe.ToString()); + + try + { + var psi = new ProcessStartInfo + { + FileName = dotnetExe, + Arguments = "msbuild \"" + proj + "\" -nologo -nodeReuse:false -v:m -t:P", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = tempDir, + }; + + using (var p = Process.Start(psi)) + { + string stdout = p.StandardOutput.ReadToEnd(); + string stderr = p.StandardError.ReadToEnd(); + if (!p.WaitForExit(120000)) + { + try { p.Kill(); } catch { } + Log.LogMessage(MessageImportance.Low, "NormalizeSdkRootDriveCasing: probe timed out launching '{0}'.", dotnetExe); + return '\0'; } - string canonical = sb.ToString(); - if (canonical.Length >= 2 && canonical[1] == ':') + int idx = stdout.IndexOf(Marker, StringComparison.Ordinal); + if (idx >= 0) { - char canonicalDrive = canonical[0]; - if (canonicalDrive != SdkRoot[0]) + string resolved = stdout.Substring(idx + Marker.Length).TrimStart(); + int nl = resolved.IndexOfAny(new[] { '\r', '\n' }); + if (nl >= 0) + { + resolved = resolved.Substring(0, nl); + } + + if (resolved.Length >= 2 && resolved[1] == ':') { - Result = canonicalDrive + SdkRoot.Substring(1); - Log.LogMessage( - MessageImportance.High, - "Normalized NetCoreSdkRoot drive casing '{0}' -> '{1}' to match Environment.ProcessPath (#14026 workaround).", - SdkRoot[0], - canonicalDrive); + return resolved[0]; } } - } - finally - { - FreeLibrary(module); + else + { + Log.LogMessage(MessageImportance.Low, + "NormalizeSdkRootDriveCasing: probe produced no result (exit {0}). stderr=[{1}]", + p.ExitCode, stderr.Replace("\r", "").Replace("\n", " | ")); + } } } - catch (Exception ex) + finally { - Log.LogMessage(MessageImportance.Low, "NormalizeSdkRootDriveCasing skipped: {0}", ex.Message); + try { Directory.Delete(tempDir, recursive: true); } catch { } } - return true; + return '\0'; } } ]]> @@ -152,9 +290,9 @@ public class NormalizeSdkRootDriveCasing : Task - + Condition="'$(MSBuildRuntimeType)' == 'Full' and '$(NetCoreSdkRoot)' != '' and '$(DisableNetCoreSdkRootDriveCasingWorkaround)' != 'true'"> + - \ No newline at end of file +