Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect .NET SDK & VSLANG Custom Language Settings & Apply To MSBuild #8503

Merged
merged 33 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0f58db2
Add logic to change the encoding and detect .NET SDK and VS language …
nagilson Feb 28, 2023
3a3db4d
Restore the original encoding so msbuild does not impact encoding of …
nagilson Feb 28, 2023
e4595a9
Add a unit test for external ui language overrides
nagilson Feb 28, 2023
dc17603
Merge remote-tracking branch 'upstream/main' into nagilson-cli-language
nagilson Feb 28, 2023
fa715d1
Fix the merge conflict that vscode did not fix (grumpiness)
nagilson Feb 28, 2023
cc30ce9
Use langword=null for Sandcastle and VsDocMan
nagilson Feb 28, 2023
1058b59
Respond to feedback. But it's not done yet.
nagilson Mar 6, 2023
01b381b
Limit the scope of the breaking change so windows who dont support ut…
nagilson Mar 8, 2023
8487af6
Remove comment that I didn't hit ctrl S on, automatic encoding restor…
nagilson Mar 8, 2023
747ff0b
Move encoding restorer to a non public API
nagilson Mar 9, 2023
3b3b23d
Use a name instead of _ for the discard variable bc the compiler gets…
nagilson Mar 9, 2023
d52714b
Dont use runtimeinfo as its not in net 472 full framework even tho it…
nagilson Mar 9, 2023
b20919f
Move the encoding restorer so it's not part of a public api and build…
nagilson Mar 9, 2023
8d0e14b
Consider that full framework must set all culture variables for corre…
nagilson Mar 9, 2023
d96551c
Try to make full framework correctly change the culture :(
nagilson Mar 13, 2023
9b1c074
Check to see if the windows version of build machines doesnt support …
nagilson Mar 13, 2023
5a06292
Dont look for vslang because vs can manage the lang settings itself a…
nagilson Mar 13, 2023
60ee6bc
Make the encoding change happen earlier than msbuild use server stuff
nagilson Mar 14, 2023
992fb7e
Code clean up for dotnet cli language feature
nagilson Mar 14, 2023
048bd20
Respond to PR feedback
nagilson Mar 15, 2023
18f34d5
Follow the Microsoft style of using the string alias over System.String
nagilson Mar 23, 2023
881305c
prepare to add encoding change to logs as well
nagilson May 4, 2023
cfe0c6c
Merge remote-tracking branch 'upstream/main' into nagilson-cli-language
nagilson May 4, 2023
45961ed
Get the encoding to change log files as well and move the encoding up…
nagilson May 4, 2023
3023308
Merge remote-tracking branch 'upstream/main' into nagilson-cli-language
nagilson May 9, 2023
41d9da6
Add environment variable to disable/opt-out of the new feature
nagilson May 9, 2023
aa5a55b
Move the utf8 encoding change feature flag to the change wave
nagilson May 9, 2023
5c2db93
Remove some no-longer-required usings
rainersigwald May 9, 2023
28b8bd0
Remove the english language and redirect in the help link
nagilson May 9, 2023
e61c559
Consider that a user may delete windows registry keys
nagilson May 9, 2023
16a0acd
Merge branch 'nagilson-cli-language' of https://github.com/nagilson/m…
nagilson May 10, 2023
f794639
update test to expect the fallback culture instead of the original cu…
nagilson May 10, 2023
86cf4e5
Merge remote-tracking branch 'upstream/main' into nagilson-cli-language
nagilson May 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public void Initialize(IEventSource eventSource)
ErrorUtilities.VerifyThrowArgumentNull(eventSource, nameof(eventSource));
ParseFileLoggerParameters();
string fileName = _logFile;

nagilson marked this conversation as resolved.
Show resolved Hide resolved
try
{
// Create a new file logger and pass it some parameters to make the build log very detailed
Expand Down
5 changes: 5 additions & 0 deletions src/Build/Logging/FileLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public FileLogger()
colorReset: BaseConsoleLogger.DontResetColor)
{
WriteHandler = Write;

if (EncodingUtilities.GetExternalOverriddenUILanguageIfSupportableWithEncoding() != null)
{
_encoding = Encoding.UTF8;
nagilson marked this conversation as resolved.
Show resolved Hide resolved
}
}

#endregion
Expand Down
4 changes: 0 additions & 4 deletions src/Deprecated/Engine/Logging/FileLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ public override void Initialize(IEventSource eventSource, int nodeCount)
/// <summary>
/// The handler for the write delegate of the console logger we are deriving from.
/// </summary>
/// <owner>KieranMo</owner>
/// <param name="text">The text to write to the log</param>
private void Write(string text)
{
Expand All @@ -143,7 +142,6 @@ private void Write(string text)
/// <summary>
/// Shutdown method implementation of ILogger - we need to flush and close our logfile.
/// </summary>
/// <owner>KieranMo</owner>
public override void Shutdown()
{
fileWriter?.Close();
Expand All @@ -152,7 +150,6 @@ public override void Shutdown()
/// <summary>
/// Parses out the logger parameters from the Parameters string.
/// </summary>
/// <owner>KieranMo</owner>
private void ParseFileLoggerParameters()
{
if (this.Parameters != null)
Expand Down Expand Up @@ -180,7 +177,6 @@ private void ParseFileLoggerParameters()
/// <summary>
/// Apply a parameter parsed by the file logger.
/// </summary>
/// <owner>KieranMo</owner>
private void ApplyFileLoggerParameter(string parameterName, string parameterValue)
{
switch (parameterName.ToUpperInvariant())
Expand Down
93 changes: 93 additions & 0 deletions src/Framework/EncodingUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -247,5 +251,94 @@ internal static Encoding BatchFileEncoding(string contents, string encodingSpeci
: EncodingUtilities.Utf8WithoutBom;
}
}

/// <summary>
/// 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.
/// </summary>
/// <returns>
/// The custom language that was set by the user for an 'external' tool besides MSBuild.
/// Returns <see langword="null"/> if none are set.
/// </returns>
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");
var buildNumber = windowsVersionRegistry.GetValue("CurrentBuildNumber").ToString();
nagilson marked this conversation as resolved.
Show resolved Hide resolved
const int buildNumberThatOfficialySupportsUTF8 = 18363;
return int.Parse(buildNumber) >= buildNumberThatOfficialySupportsUTF8 || ForceUniversalEncodingOptInEnabled();
}
catch (Exception ex) when (ex is SecurityException || ex is 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);
}

/// <summary>
/// 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
/// </summary>
/// <returns>The custom language that was set by the user for an 'external' tool besides MSBuild.
/// Returns null if none are set.</returns>
private static CultureInfo GetExternalOverriddenUILanguage()
{
// DOTNET_CLI_UI_LANGUAGE=<culture name> 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;
}
}
}
61 changes: 61 additions & 0 deletions src/MSBuild.UnitTests/XMake_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
nagilson marked this conversation as resolved.
Show resolved Hide resolved

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") : originalUICulture, 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();
}
}

#if FEATURE_SYSTEM_CONFIGURATION
/// <summary>
/// Invalid configuration file should not dump stack.
Expand Down Expand Up @@ -798,6 +853,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 =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
Expand Down Expand Up @@ -834,6 +893,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 = ");
Expand Down
68 changes: 68 additions & 0 deletions src/MSBuild/AutomaticEncodingRestorer.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class AutomaticEncodingRestorer : IDisposable
{
private Encoding? _originalOutputEncoding = null;
private Encoding? _originalInputEncoding = null;

public AutomaticEncodingRestorer()
{
try
{
#if NET7_0_OR_GREATER
nagilson marked this conversation as resolved.
Show resolved Hide resolved
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.
}
}
}
}
18 changes: 10 additions & 8 deletions src/MSBuild/MSBuild.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\Shared\FileSystemSources.proj" />
<Import Project="..\Shared\DebuggingSources.proj" />
Expand Down Expand Up @@ -48,7 +48,8 @@
<NoPackageAnalysis>true</NoPackageAnalysis>
<AddAppConfigToBuildOutputs>false</AddAppConfigToBuildOutputs>

<DebugType Condition="'$(Platform)' == 'x64'">full</DebugType><!-- Setting DebugType here goes hand in hand with eng\AfterSigning.targets. This is to prompt the x64 build to produce a 'full' .pdb that's `more compatible` then 'portable' and 'embedded' .pdbs. This doesn't get set on 32 bit architecture, which will default to 'embedded' and 'pdb2pdb' will convert those as needed. See https://github.com/dotnet/msbuild/pull/5070 for context. -->
<!-- Setting DebugType here goes hand in hand with eng\AfterSigning.targets. This is to prompt the x64 build to produce a 'full' .pdb that's `more compatible` then 'portable' and 'embedded' .pdbs. This doesn't get set on 32 bit architecture, which will default to 'embedded' and 'pdb2pdb' will convert those as needed. See https://github.com/dotnet/msbuild/pull/5070 for context. -->
<DebugType Condition="'$(Platform)' == 'x64'">full</DebugType>
<DefineConstants>$(DefineConstants);MSBUILDENTRYPOINTEXE</DefineConstants>
</PropertyGroup>

Expand Down Expand Up @@ -163,6 +164,7 @@
<Compile Include="AssemblyResources.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
<Compile Include="AutomaticEncodingRestorer.cs" />
<Compile Include="CommandLineSwitches.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
Expand Down Expand Up @@ -218,13 +220,13 @@
<PackageReference Include="Microsoft.IO.Redist" Condition="'$(FeatureMSIORedist)' == 'true'" />
<PackageReference Include="System.Configuration.ConfigurationManager" />
</ItemGroup>
<!-- Manually download this library for RoslynCodeTaskFactory.
<!-- Manually download this library for RoslynCodeTaskFactory.
See target AddRefAssemblies below. -->
<ItemGroup>
<PackageDownload Include="netstandard.library">
<Version>[2.0.3]</Version>
</PackageDownload>
</ItemGroup>
<ItemGroup>
<PackageDownload Include="netstandard.library">
<Version>[2.0.3]</Version>
</PackageDownload>
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<!-- File for Assemblies we depend on -->
<Reference Include="System" />
Expand Down