Skip to content
1 change: 1 addition & 0 deletions documentation/wiki/ChangeWaves.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t
- [Exec task does not trim leading whitespaces for ConsoleOutput](https://github.com/dotnet/msbuild/pull/9722)
- [Introduce [MSBuild]::StableStringHash overloads](https://github.com/dotnet/msbuild/issues/9519)
- [Keep the encoding of standard output & error consistent with the console code page for ToolTask](https://github.com/dotnet/msbuild/pull/9539)
- [Convert.ToString during a property evaluation uses the InvariantCulture for all types](https://github.com/dotnet/msbuild/pull/9874)


### 17.8
Expand Down
96 changes: 96 additions & 0 deletions src/Build.UnitTests/Evaluation/Expander_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
Expand Down Expand Up @@ -4893,5 +4894,100 @@ public void ExpandItemVectorFunctions_Exists_Directories()
squiggleItems.Select(i => i.EvaluatedInclude).ShouldBe(new[] { alphaBetaPath, alphaDeltaPath }, Case.Insensitive);
}
}

[Fact]
public void ExpandItem_ConvertToStringUsingInvariantCultureForNumberData()
{
var currentThread = Thread.CurrentThread;
var originalCulture = currentThread.CurrentCulture;
var originalUICulture = currentThread.CurrentUICulture;

try
{
var svSECultureInfo = new CultureInfo("sv-SE");
using (var env = TestEnvironment.Create())
{
currentThread.CurrentCulture = svSECultureInfo;
currentThread.CurrentUICulture = svSECultureInfo;
var root = env.CreateFolder();

var projectFile = env.CreateFile(root, ".proj",
@"<Project>

<PropertyGroup>
<_value>$([MSBuild]::Subtract(0, 1))</_value>
<_otherValue Condition=""'$(_value)' &gt;= -1"">test-value</_otherValue>
</PropertyGroup>
<Target Name=""Build"" />
</Project>");
ProjectInstance projectInstance = new ProjectInstance(projectFile.Path);
projectInstance.GetPropertyValue("_value").ShouldBe("-1");
projectInstance.GetPropertyValue("_otherValue").ShouldBe("test-value");
}
}
finally
{
currentThread.CurrentCulture = originalCulture;
currentThread.CurrentUICulture = originalUICulture;
}
}

[Fact]
public void ExpandItem_ConvertToStringUsingInvariantCultureForNumberData_RespectingChangeWave()
{
// Note: Skipping the test since it is not a valid scenario when ICU mode is not used.
if (!ICUModeAvailable())
{
return;
}

var currentThread = Thread.CurrentThread;
var originalCulture = currentThread.CurrentCulture;
var originalUICulture = currentThread.CurrentUICulture;

try
{
var svSECultureInfo = new CultureInfo("sv-SE");
using (var env = TestEnvironment.Create())
{
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave17_10.ToString());
currentThread.CurrentCulture = svSECultureInfo;
currentThread.CurrentUICulture = svSECultureInfo;
var root = env.CreateFolder();

var projectFile = env.CreateFile(root, ".proj",
@"<Project>

<PropertyGroup>
<_value>$([MSBuild]::Subtract(0, 1))</_value>
<_otherValue Condition=""'$(_value)' &gt;= -1"">test-value</_otherValue>
</PropertyGroup>
<Target Name=""Build"" />
</Project>");
var exception = Should.Throw<InvalidProjectFileException>(() =>
{
new ProjectInstance(projectFile.Path);
});
exception.BaseMessage.ShouldContain("A numeric comparison was attempted on \"$(_value)\"");
}
}
finally
{
currentThread.CurrentCulture = originalCulture;
currentThread.CurrentUICulture = originalUICulture;
}
}

/// <summary>
/// Determines if ICU mode is enabled.
/// Copied from: https://learn.microsoft.com/en-us/dotnet/core/extensions/globalization-icu#determine-if-your-app-is-using-icu
/// </summary>
private static bool ICUModeAvailable()
{
SortVersion sortVersion = CultureInfo.InvariantCulture.CompareInfo.Version;
byte[] bytes = sortVersion.SortId.ToByteArray();
int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
return version != 0 && version == sortVersion.FullVersion;
}
}
}
10 changes: 9 additions & 1 deletion src/Build/Evaluation/Expander.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,15 @@ internal static string ConvertToString(object valueToConvert)
else
{
// The fall back is always to just convert to a string directly.
convertedString = valueToConvert.ToString();
// Issue: https://github.com/dotnet/msbuild/issues/9757
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))
{
convertedString = Convert.ToString(valueToConvert, CultureInfo.InvariantCulture);
}
else
{
convertedString = valueToConvert.ToString();
}
}

return convertedString;
Expand Down