diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml
index 67f7b46489ab2f..629afffc730089 100644
--- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml
+++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml
@@ -6,8 +6,12 @@
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs
index 3ad23b92f4a1ba..2cc6fd74b8201c 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs
@@ -7,42 +7,49 @@ internal static partial class GlobalizationMode
{
private static partial class Settings
{
- internal static readonly bool Invariant = GetGlobalizationInvariantMode();
- }
-
- internal static bool Invariant => Settings.Invariant;
-
- internal static bool UseNls => false;
+ ///
+ /// Load ICU (when not in Invariant mode) in a static cctor to ensure it is loaded early in the process.
+ /// Other places, e.g. CompareInfo.GetSortKey, rely on ICU already being loaded before they are called.
+ ///
+ static Settings()
+ {
+ if (!Invariant)
+ {
+ if (TryGetAppLocalIcuSwitchValue(out string? icuSuffixAndVersion))
+ {
+ LoadAppLocalIcu(icuSuffixAndVersion);
+ }
+ else
+ {
+ int loaded = LoadICU();
+ if (loaded == 0)
+ {
+ Environment.FailFast(GetIcuLoadFailureMessage());
+ }
+ }
+ }
+ }
- private static bool GetGlobalizationInvariantMode()
- {
- bool invariantEnabled = GetInvariantSwitchValue();
- if (!invariantEnabled)
+ private static string GetIcuLoadFailureMessage()
{
- if (TryGetAppLocalIcuSwitchValue(out string? icuSuffixAndVersion))
+ // These strings can't go into resources, because a resource lookup requires globalization, which requires ICU
+ if (OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() ||
+ OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS() || OperatingSystem.IsMacCatalyst())
{
- LoadAppLocalIcu(icuSuffixAndVersion);
+ return "Unable to load required ICU Globalization data. Please see https://aka.ms/dotnet-missing-libicu for more information";
}
else
{
- int loaded = LoadICU();
- if (loaded == 0 && !OperatingSystem.IsBrowser())
- {
- // This can't go into resources, because a resource lookup requires globalization, which requires ICU
- string message = "Couldn't find a valid ICU package installed on the system. " +
- "Please install libicu using your package manager and try again. " +
- "Alternatively you can set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. " +
- "Please see https://aka.ms/dotnet-missing-libicu for more information.";
- Environment.FailFast(message);
- }
-
- // fallback to Invariant mode if LoadICU failed (Browser).
- return loaded == 0;
+ return "Couldn't find a valid ICU package installed on the system. " +
+ "Please install libicu using your package manager and try again. " +
+ "Alternatively you can set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. " +
+ "Please see https://aka.ms/dotnet-missing-libicu for more information.";
}
}
- return invariantEnabled;
}
+ internal static bool UseNls => false;
+
private static void LoadAppLocalIcuCore(ReadOnlySpan version, ReadOnlySpan suffix)
{
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs
index 590aae9b3bdc71..e81ef6234e7b9e 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs
@@ -5,10 +5,6 @@ namespace System.Globalization
{
internal static partial class GlobalizationMode
{
- // Order of these properties in Windows matter because GetUseIcuMode is dependent on Invariant.
- // So we need Invariant to be initialized first.
- internal static bool Invariant { get; } = GetInvariantSwitchValue();
-
internal static bool UseNls { get; } = !Invariant &&
(AppContextConfigHelper.GetBooleanConfig("System.Globalization.UseNls", "DOTNET_SYSTEM_GLOBALIZATION_USENLS") ||
!LoadIcu());
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs
index e68195ea569850..5afa2a4e2b9792 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs
@@ -8,11 +8,18 @@ namespace System.Globalization
{
internal static partial class GlobalizationMode
{
+ // split from GlobalizationMode so the whole class can be trimmed when Invariant=true.
private static partial class Settings
{
internal static readonly bool PredefinedCulturesOnly = AppContextConfigHelper.GetBooleanConfig("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY");
+ internal static bool Invariant { get; } = GetInvariantSwitchValue();
}
+ // Note: Invariant=true and Invariant=false are substituted at different levels in the ILLink.Substitutions file.
+ // This allows for the whole Settings nested class to be trimmed when Invariant=true, and allows for the Settings
+ // static cctor (on Unix) to be preserved when Invariant=false.
+ internal static bool Invariant => Settings.Invariant;
+
internal static bool PredefinedCulturesOnly => !Invariant && Settings.PredefinedCulturesOnly;
private static bool GetInvariantSwitchValue() =>
diff --git a/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationFalse.cs b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationFalse.cs
new file mode 100644
index 00000000000000..fbdafa45922afc
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationFalse.cs
@@ -0,0 +1,26 @@
+// 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.Globalization;
+using System.Threading;
+
+///
+/// Ensures setting InvariantGlobalization = false still works in a trimmed app.
+///
+class Program
+{
+ static int Main(string[] args)
+ {
+ // since we are using Invariant GlobalizationMode = false, setting the culture matters.
+ // The app will always use the current culture, so in the Turkish culture, 'i' ToUpper will NOT be "I"
+ Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
+ if ("i".ToUpper() == "I")
+ {
+ // 'i' ToUpper was "I", but shouldn't be in the Turkish culture, so fail
+ return -1;
+ }
+
+ return 100;
+ }
+}
diff --git a/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs
new file mode 100644
index 00000000000000..7daa557239f3d6
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs
@@ -0,0 +1,55 @@
+// 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.Globalization;
+using System.Reflection;
+using System.Threading;
+
+///
+/// Ensures setting InvariantGlobalization = true still works in a trimmed app.
+///
+class Program
+{
+ static int Main(string[] args)
+ {
+ // since we are using Invariant GlobalizationMode = true, setting the culture doesn't matter.
+ // The app will always use Invariant mode, so even in the Turkish culture, 'i' ToUpper will be "I"
+ Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
+ if ("i".ToUpper() != "I")
+ {
+ // 'i' ToUpper was not "I", so fail
+ return -1;
+ }
+
+ // Ensure the internal GlobalizationMode class is trimmed correctly
+ Type globalizationMode = GetCoreLibType("System.Globalization.GlobalizationMode");
+ const BindingFlags allStatics = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
+ foreach (MemberInfo member in globalizationMode.GetMembers(allStatics))
+ {
+ // properties and their backing getter methods are OK
+ if (member is PropertyInfo || member.Name.StartsWith("get_"))
+ {
+ continue;
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ // Windows still contains a static cctor and a backing field for UseNls
+ if (member is ConstructorInfo || (member is FieldInfo field && field.Name.Contains("UseNls")))
+ {
+ continue;
+ }
+ }
+
+ // Some unexpected member was left on GlobalizationMode, fail
+ Console.WriteLine($"Member '{member.Name}' was not trimmed from GlobalizationMode, but should have been.");
+ return -2;
+ }
+
+ return 100;
+ }
+
+ private static Type GetCoreLibType(string name) =>
+ typeof(object).Assembly.GetType(name, throwOnError: true);
+}
diff --git a/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj b/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj
index fd556a37d675ac..02b4882782e967 100644
--- a/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj
+++ b/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj
@@ -11,6 +11,12 @@
+
+ --feature System.Globalization.Invariant false
+
+
+ --feature System.Globalization.Invariant true
+