diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_static.c b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_static.c index c81ce59e50aca..5c2c462cbb5c3 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_static.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_static.c @@ -47,7 +47,7 @@ static void U_CALLCONV icu_trace_data(const void* context, int32_t fnNumber, int #ifdef __EMSCRIPTEN__ #include -static int32_t load_icu_data(void* pData); +static int32_t load_icu_data(void* pData, int32_t type); EMSCRIPTEN_KEEPALIVE const char* mono_wasm_get_icudt_name(const char* culture); @@ -56,11 +56,11 @@ EMSCRIPTEN_KEEPALIVE const char* mono_wasm_get_icudt_name(const char* culture) return GlobalizationNative_GetICUDTName(culture); } -EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void* pData); +EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void* pData, int32_t type); -EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void* pData) +EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void* pData, int32_t type) { - return load_icu_data(pData); + return load_icu_data(pData, type); } @@ -77,14 +77,22 @@ void mono_wasm_link_icu_shim(void) #endif -static int32_t load_icu_data(void* pData) +static int32_t load_icu_data(void* pData, int32_t type) { UErrorCode status = 0; - udata_setCommonData(pData, &status); + if (type == 0) { + udata_setAppData(NULL, pData, &status); + } + if (type == 1) { + udata_setCommonData(pData, &status); + } if (U_FAILURE(status)) { - log_icu_error("udata_setCommonData", status); + if (type) + log_icu_error("udata_setCommonData", status); + else + log_icu_error("udata_setAppData", status); return 0; } else { @@ -146,7 +154,7 @@ int32_t GlobalizationNative_LoadICUData(const char* path) fclose(fp); - if (load_icu_data(icu_data) == 0) { + if (load_icu_data(icu_data, strstr(path, "app") == NULL) == 0) { log_shim_error("ICU BAD EXIT %d.", ret); return ret; } diff --git a/src/mono/sample/wasm/console/Makefile b/src/mono/sample/wasm/console/Makefile index d68cd314be01a..95e212ee50c5e 100644 --- a/src/mono/sample/wasm/console/Makefile +++ b/src/mono/sample/wasm/console/Makefile @@ -1,4 +1,5 @@ TOP=../../../../.. +CULTURE=en-us include ../wasm.mk @@ -10,6 +11,10 @@ ifneq ($(V),) DOTNET_MONO_LOG_LEVEL=--setenv=MONO_LOG_LEVEL=debug endif +ifneq ($(SHARD),) +override MSBUILD_ARGS+=/p:EnableSharding=true /p:ICUDefaultCulture=$(CULTURE) +endif + PROJECT_NAME=Wasm.Console.Sample.csproj run: run-console diff --git a/src/mono/sample/wasm/console/Program.cs b/src/mono/sample/wasm/console/Program.cs index 6af1fa68e82d4..5ec6927d8ef70 100644 --- a/src/mono/sample/wasm/console/Program.cs +++ b/src/mono/sample/wasm/console/Program.cs @@ -3,12 +3,15 @@ using System; using System.Threading.Tasks; +using System.Globalization; public class Test { public static async Task Main(string[] args) { await Task.Delay(1); + CultureInfo cr = CultureInfo.GetCultureInfo("zh"); + Console.WriteLine(cr.CompareInfo); Console.WriteLine("Hello World!"); for (int i = 0; i < args.Length; i++) { Console.WriteLine($"args[{i}] = {args[i]}"); diff --git a/src/mono/wasm/Makefile b/src/mono/wasm/Makefile index 8a69822976bfe..000939a98af46 100644 --- a/src/mono/wasm/Makefile +++ b/src/mono/wasm/Makefile @@ -178,7 +178,7 @@ run-tests-%: EMSDK_PATH=$(EMSDK_PATH) PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) run-build-tests: - PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/tests/BuildWasmApps/Wasm.Build.Tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) + PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/tests/BuildWasmApps/Wasm.Build.Tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) /p:XUnitMethodName=Wasm.Build.Tests.InvariantGlobalizationTests.Invariant_WithSharding run-browser-tests-%: PATH="$(GECKODRIVER):$(CHROMEDRIVER):$(PATH)" XHARNESS_COMMAND="test-browser --browser=$(XHARNESS_BROWSER)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 2141cce41b69b..f49cc81f25b3b 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -122,8 +122,6 @@ - icudt.dat - <_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.wasm'">true <_HasDotnetJs Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js'">true @@ -132,11 +130,17 @@ - - + + + + + + + + { + icu_files.push(...files[feat]) + }) + } + } + } + icu_assets = []; + icu_files.forEach(file => { + var type = "common"; + // if (file.includes("locales")) + // type = "app"; + icu_assets.push({ + "behavior": "icu", + "name": file, + "load_remote": false, + "data_type": type + }) + }); + return icu_assets; + }, + mono_wasm_get_details: function (objectId, args={}) { let id = this._parse_object_id (objectId, true); @@ -1620,7 +1673,7 @@ var MonoSupportLib = { } } else if (asset.behavior === "icu") { - if (this.mono_wasm_load_icu_data (offset)) + if (this.mono_wasm_load_icu_data (offset, +(asset.data_type == "common"))) ctx.num_icu_assets_loaded_successfully += 1; else console.error ("Error loading ICU asset", asset.name); @@ -1699,6 +1752,7 @@ var MonoSupportLib = { // "icu": load ICU globalization data from any runtime assets with behavior "icu". // "invariant": operate in invariant globalization mode. // "auto" (default): if "icu" behavior assets are present, use ICU, otherwise invariant. + // application_culture: (optional) current browser culture // diagnostic_tracing: (optional) enables diagnostic log messages during startup mono_load_runtime_and_bcl_args: function (args) { try { @@ -1722,9 +1776,9 @@ var MonoSupportLib = { // @offset must be the address of an ICU data archive in the native heap. // returns true on success. - mono_wasm_load_icu_data: function (offset) { - var fn = Module.cwrap ('mono_wasm_load_icu_data', 'number', ['number']); - var ok = (fn (offset)) === 1; + mono_wasm_load_icu_data: function (offset, type) { + var fn = Module.cwrap ('mono_wasm_load_icu_data', 'number', ['number', 'number']); + var ok = (fn (offset, type)) === 1; if (ok) this.num_icu_assets_loaded_successfully++; return ok; @@ -1790,7 +1844,10 @@ var MonoSupportLib = { throw new Error ("Invalid args (runtime_asset_sources was replaced by remote_sources)"); if (!args.loaded_cb) throw new Error ("loaded_cb not provided"); - + + var icu_files = this._get_list_of_icu_files(args.icu_dictionary, args.application_culture); + if (icu_files != null) + args.assets = args.assets.concat(icu_files); var ctx = { tracing: args.diagnostic_tracing || false, pending_count: args.assets.length, @@ -1838,7 +1895,6 @@ var MonoSupportLib = { var attemptNextSource; var sourceIndex = 0; var sourcesList = asset.load_remote ? args.remote_sources : [""]; - var handleFetchResponse = function (response) { if (!response.ok) { try { diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index 562091ed6a26d..67867c9778afc 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -197,6 +197,7 @@ $(MonoArtifactsPath)include/mono-2.0 $(RepoRoot)src\libraries\Native\Unix\System.Native + diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 9ef7679c292b6..60d73c5867e77 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -41,7 +41,6 @@ public class WasmAppBuilder : Task // full list of ICU data files we produce can be found here: // https://github.com/dotnet/icu/tree/maint/maint-67/icu-filters - public string? IcuDataFileName { get; set; } public int DebugLevel { get; set; } public ITaskItem[]? SatelliteAssemblies { get; set; } @@ -119,14 +118,22 @@ private class IcuData : AssetEntry public IcuData(string name) : base(name, "icu") {} [JsonPropertyName("load_remote")] public bool LoadRemote { get; set; } + [JsonPropertyName("data_type")] + public string? DataType { get; set; } } public override bool Execute () { - if (!File.Exists(MainJS)) - throw new ArgumentException($"File MainJS='{MainJS}' doesn't exist."); - if (!InvariantGlobalization && string.IsNullOrEmpty(IcuDataFileName)) - throw new ArgumentException("IcuDataFileName property shouldn't be empty if InvariantGlobalization=false"); + if (!File.Exists(MainJS)) { + Log.LogError($"File MainJS='{MainJS}' doesn't exist."); + return false; + } + + // if (!InvariantGlobalization && string.IsNullOrEmpty(IcuDictionary)) + // { + // Log.LogError("IcuDictionary property shouldn't be empty if InvariantGlobalization=false"); + // return false; + // } if (Assemblies?.Length == 0) { @@ -225,9 +232,6 @@ public override bool Execute () } } - if (!InvariantGlobalization) - config.Assets.Add(new IcuData(IcuDataFileName!) { LoadRemote = RemoteSources?.Length > 0 }); - config.Assets.Add(new VfsEntry ("dotnet.timezones.blat") { VirtualPath = "/usr/share/zoneinfo/"}); if (RemoteSources?.Length > 0) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/ICUShardingTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/ICUShardingTests.cs new file mode 100644 index 0000000000000..ed64ef28dd675 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/ICUShardingTests.cs @@ -0,0 +1,114 @@ +// 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.Collections.Generic; +using Xunit; +using Xunit.Abstractions; +using System.Globalization; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class ICUShardingTests : BuildTestBase + { + public ICUShardingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + public static IEnumerable ICUShardingTestData_EFIGS(bool aot, RunHost host) + => ConfigWithAOTData(aot) + .Multiply( + new object?[] { "en", "en-GB", false}, + new object?[] { "es", "fr-FR", false}, + new object?[] { "en", "zh", true}, + new object?[] {"es-ES", "am-ET", true}) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); + + public static IEnumerable ICUShardingTestData_CJK(bool aot, RunHost host) + => ConfigWithAOTData(aot) + .Multiply( + new object?[] { "zh", "zh-Hans", false}, + new object?[] { "ko", "en-US", false}, + new object?[] { "ja-JP", "es-ES", true}, + new object?[] { "ja-JP", "fr-FR", true}) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); + + public static IEnumerable ICUShardingTestData_no_CJK(bool aot, RunHost host) + => ConfigWithAOTData(aot) + .Multiply( + new object?[] { "am-ET", "de-DE", false}, + new object?[] { "am-ET", "vi-VN", false}, + new object?[] { "am-ET", "ko", true}, + new object?[] { "am-ET", "ja-JP", true}) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); + + [Theory] + [MemberData(nameof(ICUShardingTestData_EFIGS), parameters: new object[] { true, RunHost.All })] + [MemberData(nameof(ICUShardingTestData_EFIGS), parameters: new object[] { false, RunHost.All })] + [MemberData(nameof(ICUShardingTestData_CJK), parameters: new object[] { true, RunHost.All })] + [MemberData(nameof(ICUShardingTestData_CJK), parameters: new object[] { false, RunHost.All })] + [MemberData(nameof(ICUShardingTestData_no_CJK), parameters: new object[] { true, RunHost.All })] + [MemberData(nameof(ICUShardingTestData_no_CJK), parameters: new object[] { false, RunHost.All })] + public void ShardingTests(BuildArgs buildArgs, string defaultCulture, string testCulture, bool expectToThrow, RunHost host, string id) + => TestICUSharding(buildArgs, defaultCulture, testCulture, expectToThrow, host, id); + + void TestICUSharding(BuildArgs buildArgs, + string defaultCulture, + string testCulture, + bool expectToThrow, + RunHost host, + string id, + string projectContents="", + bool? dotnetWasmFromRuntimePack=null) + { + string projectName = $"sharding_{defaultCulture}_{testCulture}"; + string programText = $@" + using System; + using System.Globalization; + using System.Text; + + public class TestClass {{ + public static int Main() + {{ + try {{ + var culture = new CultureInfo(""{testCulture}"", false); + string s = new string( new char[] {{'\u0063', '\u0301', '\u0327', '\u00BE'}}); + string normalized = s.Normalize(); + Console.WriteLine($""{{culture.NativeName}} - {{culture.NumberFormat.CurrencySymbol}} - {{culture.DateTimeFormat.FullDateTimePattern}} - {{culture.CompareInfo.LCID}} - {{normalized.IsNormalized(NormalizationForm.FormC)}}""); + }} + catch (CultureNotFoundException e){{ + Console.WriteLine($""Culture Not Found {{e.Message}}""); + }} + + return 42; + }} + }}"; + + buildArgs = buildArgs with { ProjectName = projectName, ProjectFileContents = programText }; + buildArgs = GetBuildArgsWith(buildArgs, extraProperties: $"true{defaultCulture}"); + if (dotnetWasmFromRuntimePack == null) + dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + + BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), + id: id, + dotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack); + + var culture = CultureInfo.GetCultureInfo(testCulture); + + string expectedOutputString = expectToThrow == true + ? "Culture Not Found" + : $"{culture.NativeName} - {culture.NumberFormat.CurrencySymbol} - {culture.DateTimeFormat.FullDateTimePattern} - {culture.CompareInfo.LCID} - True"; + + RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, + test: output => Assert.Contains(expectedOutputString, output), host: host, id: id); + } + } +} \ No newline at end of file diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 6d01f20a8420e..6bb84032fd170 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -33,6 +33,14 @@ public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestC public void AOT_InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id); + + [Theory] + [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void Invariant_WithSharding(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) + => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id, + extraProperties: "true"); + // TODO: What else should we use to verify a relinked build? [Theory] [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })]