diff --git a/newc/Program.cs b/newc/Program.cs new file mode 100644 index 00000000000..3751555cbd3 --- /dev/null +++ b/newc/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/newc/newc.csproj b/newc/newc.csproj new file mode 100644 index 00000000000..2150e3797ba --- /dev/null +++ b/newc/newc.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs index 4df6b2830f9..7f90d035fd6 100644 --- a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs +++ b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs @@ -97,6 +97,7 @@ public void Initialize(IEventSource eventSource) ErrorUtilities.VerifyThrowArgumentNull(eventSource, nameof(eventSource)); ParseFileLoggerParameters(); string fileName = _logFile; + try { // Create a new file logger and pass it some parameters to make the build log very detailed diff --git a/src/Build/Logging/FileLogger.cs b/src/Build/Logging/FileLogger.cs index 156ee0c58e5..180d58a2a08 100644 --- a/src/Build/Logging/FileLogger.cs +++ b/src/Build/Logging/FileLogger.cs @@ -39,6 +39,11 @@ public FileLogger() colorReset: BaseConsoleLogger.DontResetColor) { WriteHandler = Write; + + if (EncodingUtilities.GetExternalOverriddenUILanguageIfSupportableWithEncoding() != null) + { + _encoding = Encoding.UTF8; + } } #endregion diff --git a/src/Deprecated/Engine/Logging/FileLogger.cs b/src/Deprecated/Engine/Logging/FileLogger.cs index f7fd9fdf988..1f574b8af5e 100644 --- a/src/Deprecated/Engine/Logging/FileLogger.cs +++ b/src/Deprecated/Engine/Logging/FileLogger.cs @@ -117,7 +117,6 @@ public override void Initialize(IEventSource eventSource, int nodeCount) /// /// The handler for the write delegate of the console logger we are deriving from. /// - /// KieranMo /// The text to write to the log private void Write(string text) { @@ -143,7 +142,6 @@ private void Write(string text) /// /// Shutdown method implementation of ILogger - we need to flush and close our logfile. /// - /// KieranMo public override void Shutdown() { fileWriter?.Close(); @@ -152,7 +150,6 @@ public override void Shutdown() /// /// Parses out the logger parameters from the Parameters string. /// - /// KieranMo private void ParseFileLoggerParameters() { if (this.Parameters != null) @@ -180,7 +177,6 @@ private void ParseFileLoggerParameters() /// /// Apply a parameter parsed by the file logger. /// - /// KieranMo private void ApplyFileLoggerParameter(string parameterName, string parameterValue) { switch (parameterName.ToUpperInvariant()) diff --git a/src/Framework/EncodingUtilities.cs b/src/Framework/EncodingUtilities.cs index 9ad987bd730..298c740da96 100644 --- a/src/Framework/EncodingUtilities.cs +++ b/src/Framework/EncodingUtilities.cs @@ -3,11 +3,15 @@ using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Security; using System.Text; using Microsoft.Build.Framework; +using Microsoft.Win32; #nullable disable @@ -247,5 +251,96 @@ internal static Encoding BatchFileEncoding(string contents, string encodingSpeci : EncodingUtilities.Utf8WithoutBom; } } +#nullable enable + /// + /// The .NET SDK and Visual Studio both have environment variables that set a custom language. MSBuild should respect the SDK variable. + /// To use the corresponding UI culture, in certain cases the console encoding must be changed. This function will change the encoding in these cases. + /// This code introduces a breaking change in .NET 8 due to the encoding of the console being changed. + /// If the environment variables are undefined, this function should be a no-op. + /// + /// + /// The custom language that was set by the user for an 'external' tool besides MSBuild. + /// Returns if none are set. + /// + public static CultureInfo? GetExternalOverriddenUILanguageIfSupportableWithEncoding() + { + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_8)) + { + return null; + } + + CultureInfo? externalLanguageSetting = GetExternalOverriddenUILanguage(); + if (externalLanguageSetting != null) + { + if ( + !externalLanguageSetting.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase) && + CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding() + ) + { + // Setting both encodings causes a change in the CHCP, making it so we don't need to P-Invoke CHCP ourselves. + Console.OutputEncoding = Encoding.UTF8; + // If the InputEncoding is not set, the encoding will work in CMD but not in PowerShell, as the raw CHCP page won't be changed. + Console.InputEncoding = Encoding.UTF8; + return externalLanguageSetting; + } + else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return externalLanguageSetting; + } + } + + return null; + } + + public static bool CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10) // UTF-8 is only officially supported on 10+. + { + try + { + using RegistryKey? windowsVersionRegistry = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + string? buildNumber = windowsVersionRegistry?.GetValue("CurrentBuildNumber")?.ToString(); + const int buildNumberThatOfficiallySupportsUTF8 = 18363; + return buildNumber != null && (int.Parse(buildNumber) >= buildNumberThatOfficiallySupportsUTF8 || ForceUniversalEncodingOptInEnabled()); + } + catch (Exception ex) when (ex is SecurityException or ObjectDisposedException) + { + // We don't want to break those in VS on older versions of Windows with a non-en language. + // Allow those without registry permissions to force the encoding, however. + return ForceUniversalEncodingOptInEnabled(); + } + } + + return false; + } + + private static bool ForceUniversalEncodingOptInEnabled() + { + return string.Equals(Environment.GetEnvironmentVariable("DOTNET_CLI_FORCE_UTF8_ENCODING"), "true", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Look at UI language overrides that can be set by known external invokers. (DOTNET_CLI_UI_LANGUAGE.) + /// Does NOT check System Locale or OS Display Language. + /// Ported from the .NET SDK: https://github.com/dotnet/sdk/blob/bcea1face15458814b8e53e8785b52ba464f6538/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs + /// + /// The custom language that was set by the user for an 'external' tool besides MSBuild. + /// Returns null if none are set. + private static CultureInfo? GetExternalOverriddenUILanguage() + { + // DOTNET_CLI_UI_LANGUAGE= is the main way for users to customize the CLI's UI language via the .NET SDK. + string? dotnetCliLanguage = Environment.GetEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE"); + if (dotnetCliLanguage != null) + { + try + { + return new CultureInfo(dotnetCliLanguage); + } + catch (CultureNotFoundException) { } + } + + return null; + } } } + diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 86379f90295..dd838ef172f 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -9,6 +9,7 @@ using System.IO.Compression; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; using Microsoft.Build.CommandLine; using Microsoft.Build.Framework; @@ -642,6 +643,60 @@ public void SetConsoleUICulture() thisThread.CurrentUICulture = originalUICulture; } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConsoleUIRespectsSDKLanguage(bool enableFeature) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !EncodingUtilities.CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding()) + { + return; // The feature to detect .NET SDK Languages is not enabled on this machine, so don't test it. + } + + const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE); + using TestEnvironment testEnvironment = TestEnvironment.Create(); + // Save the current environment info so it can be restored. + var originalUILanguage = Environment.GetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE); + + var originalOutputEncoding = Console.OutputEncoding; + var originalInputEncoding = Console.InputEncoding; + Thread thisThread = Thread.CurrentThread; + CultureInfo originalUICulture = thisThread.CurrentUICulture; + + try + { + // Set the UI language based on the SDK environment var. + testEnvironment.SetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE, "ja"); // Japanese chose arbitrarily. + ChangeWaves.ResetStateForTests(); + if (!enableFeature) + { + testEnvironment.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave17_8.ToString()); + } + MSBuildApp.SetConsoleUI(); + + Assert.Equal(enableFeature ? new CultureInfo("ja") : CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture(), thisThread.CurrentUICulture); + if (enableFeature) + { + Assert.Equal(65001, Console.OutputEncoding.CodePage); // UTF-8 enabled for correct rendering. + } + } + finally + { + // Restore the current UI culture back to the way it was at the beginning of this unit test. + thisThread.CurrentUICulture = originalUICulture; + // Restore for full framework + CultureInfo.CurrentCulture = originalUICulture; + CultureInfo.DefaultThreadCurrentUICulture = originalUICulture; + + // MSBuild should also restore the encoding upon exit, but we don't create that context here. + Console.OutputEncoding = originalOutputEncoding; + Console.InputEncoding = originalInputEncoding; + + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + } + /// /// We shouldn't change the UI culture if the current UI culture is invariant. /// In other cases, we can get an exception on CultureInfo creation when System.Globalization.Invariant enabled. @@ -822,6 +877,10 @@ public void TestEnvironmentTest() [Fact] public void MSBuildEngineLogger() { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + testEnvironment.SetEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en"); // build machines may have other values. + CultureInfo.CurrentUICulture = new CultureInfo("en"); // Validate that the thread will produce an english log regardless of the machine OS language + string oldValueForMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"); string projectString = "" + @@ -858,6 +917,8 @@ public void MSBuildEngineLogger() var logFileContents = File.ReadAllText(logFile); + Assert.Equal(new CultureInfo("en"), Thread.CurrentThread.CurrentUICulture); + logFileContents.ShouldContain("Process = "); logFileContents.ShouldContain("MSBuild executable path = "); logFileContents.ShouldContain("Command line arguments = "); diff --git a/src/MSBuild/AutomaticEncodingRestorer.cs b/src/MSBuild/AutomaticEncodingRestorer.cs new file mode 100644 index 00000000000..b5696d62ab8 --- /dev/null +++ b/src/MSBuild/AutomaticEncodingRestorer.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +namespace Microsoft.Build.CommandLine +{ + /// + /// Ported from https://github.com/dotnet/sdk/blob/bcea1face15458814b8e53e8785b52ba464f6538/src/Cli/dotnet/AutomaticEncodingRestorer.cs. + /// A program can change the encoding of the console which would affect other programs. + /// We would prefer to have a pattern where the program does not affect encoding of other programs. + /// Create this class in a function akin to Main and let it manage the console encoding resources to return it to the state before execution upon destruction. + /// + public class AutomaticEncodingRestorer : IDisposable + { + private Encoding? _originalOutputEncoding = null; + private Encoding? _originalInputEncoding = null; + + public AutomaticEncodingRestorer() + { + try + { +#if NET7_0_OR_GREATER + if (OperatingSystem.IsIOS() || OperatingSystem.IsAndroid() || OperatingSystem.IsTvOS()) // Output + Input Encoding are unavailable on these platforms per docs, and they're only available past net 5. + { + return; + } +#endif + _originalOutputEncoding = Console.OutputEncoding; + +#if NET7_0_OR_GREATER + if (OperatingSystem.IsBrowser()) // Input Encoding is also unavailable in this platform. (No concern for net472 as browser is unavailable.) + { + return; + } +#endif + _originalInputEncoding = Console.InputEncoding; + } + catch (Exception ex) when (ex is IOException || ex is SecurityException) + { + // The encoding is unavailable. Do nothing. + } + } + + public void Dispose() + { + try + { + if (_originalOutputEncoding != null) + { + Console.OutputEncoding = _originalOutputEncoding; + } + if (_originalInputEncoding != null) + { + Console.InputEncoding = _originalInputEncoding; + } + } + catch (Exception ex) when (ex is IOException || ex is SecurityException) + { + // The encoding is unavailable. Do nothing. + } + } + } +} diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 2e320bdfc9a..dfff888c132 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -1,4 +1,4 @@ - + @@ -48,7 +48,8 @@ true false - full + + full $(DefineConstants);MSBUILDENTRYPOINTEXE @@ -163,6 +164,7 @@ true + true @@ -218,13 +220,13 @@ - - - - [2.0.3] - - + + + [2.0.3] + + diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index dc714fe60e7..06c53027f78 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -220,6 +220,10 @@ string[] args ) #pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter { + // Setup the console UI. + using AutomaticEncodingRestorer _ = new(); + SetConsoleUI(); + DebuggerLaunchCheck(); // Initialize new build telemetry and record start of this build. @@ -663,9 +667,6 @@ private static void DebuggerLaunchCheck() // check the operating system the code is running on VerifyThrowSupportedOS(); - // Setup the console UI. - SetConsoleUI(); - // reset the application state for this new build ResetBuildState(); @@ -1672,14 +1673,20 @@ internal static void SetConsoleUI() Thread thisThread = Thread.CurrentThread; // Eliminate the complex script cultures from the language selection. - thisThread.CurrentUICulture = CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture(); + var desiredCulture = EncodingUtilities.GetExternalOverriddenUILanguageIfSupportableWithEncoding() ?? CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture(); + thisThread.CurrentUICulture = desiredCulture; + + // For full framework, both the above and below must be set. This is not true in core, but it is a no op in core. + // https://learn.microsoft.com/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#remarks + CultureInfo.CurrentUICulture = desiredCulture; + CultureInfo.DefaultThreadCurrentUICulture = desiredCulture; // Determine if the language can be displayed in the current console codepage, otherwise set to US English int codepage; try { - codepage = System.Console.OutputEncoding.CodePage; + codepage = Console.OutputEncoding.CodePage; } catch (NotSupportedException) {