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 +