diff --git a/azure-pipelines-codeql.yml b/azure-pipelines-codeql.yml index e19d3ac96a..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: diff --git a/eng/Versions.props b/eng/Versions.props index 1b53d2cfd3..a9b5a211ac 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,4 +1,4 @@ - + 4.3.0 @@ -7,10 +7,292 @@ 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 + + + + + + + + + + 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 + { + if (string.IsNullOrEmpty(SdkRoot) || SdkRoot.Length < 2 || SdkRoot[1] != ':') + { + return true; + } + + // 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)) + { + char forced = SdkRoot[0]; + if (string.Equals(Override, "lower", StringComparison.OrdinalIgnoreCase)) + { + forced = char.ToLowerInvariant(SdkRoot[0]); + } + else if (string.Equals(Override, "upper", StringComparison.OrdinalIgnoreCase)) + { + 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; + } + + 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])) + { + 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; + } + + // 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 cached; + } + + 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)) + { + string candidate = Path.Combine(dotnetRoot, "dotnet.exe"); + if (File.Exists(candidate)) + { + 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'; + } + + int idx = stdout.IndexOf(Marker, StringComparison.Ordinal); + if (idx >= 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] == ':') + { + return resolved[0]; + } + } + else + { + Log.LogMessage(MessageImportance.Low, + "NormalizeSdkRootDriveCasing: probe produced no result (exit {0}). stderr=[{1}]", + p.ExitCode, stderr.Replace("\r", "").Replace("\n", " | ")); + } + } + } + finally + { + try { Directory.Delete(tempDir, recursive: true); } catch { } + } + + return '\0'; + } +} +]]> + + + + + + + +