diff --git a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs index 90d27ad7287c76..c0bc8895e093fb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs @@ -47,10 +47,12 @@ public async Task LoadSatelliteAssembly(bool loadAllSatelliteResources) // So there messages are should be present only when we are lazily loading satellites. expectedOutput.Add(m => Assert.Equal("default: hello", m)); expectedOutput.Add(m => Assert.Equal("es-ES without satellite: hello", m)); + expectedOutput.Add(m => Assert.Equal("fr-FR without satellite: hello", m)); } expectedOutput.Add(m => Assert.Equal("default: hello", m)); expectedOutput.Add(m => Assert.Equal("es-ES with satellite: hola", m)); + expectedOutput.Add(m => Assert.Equal("fr-FR with satellite: bonjour", m)); Assert.Collection( result.TestOutput, @@ -68,7 +70,7 @@ public async Task LoadSatelliteAssemblyFromReference() var appCsprojPath = Path.Combine(_projectDir, "WasmBasicTestApp.csproj"); var appCsproj = XDocument.Load(appCsprojPath); - var projectReference = appCsproj.Descendants("ProjectReference").Where(pr => pr.Attribute("Include")?.Value?.Contains("ResourceLibrary") ?? false).Single(); + var projectReference = appCsproj.Descendants("ProjectReference").Single(pr => pr.Attribute("Include")?.Value?.Contains("ResourceLibrary") ?? false); var itemGroup = projectReference.Parent!; projectReference.Remove(); @@ -94,8 +96,32 @@ public async Task LoadSatelliteAssemblyFromReference() result.TestOutput, m => Assert.Equal("default: hello", m), m => Assert.Equal("es-ES without satellite: hello", m), + m => Assert.Equal("fr-FR without satellite: hello", m), m => Assert.Equal("default: hello", m), - m => Assert.Equal("es-ES with satellite: hola", m) + m => Assert.Equal("es-ES with satellite: hola", m), + m => Assert.Equal("fr-FR with satellite: bonjour", m) ); } + + [Fact, TestCategory("no-fingerprinting")] + public void SatelliteAssembliesFromPackageReference() + { + Configuration config = Configuration.Release; + ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "SatelliteLoadingTestsFromPackageReference"); + BuildProject(info, config, new BuildOptions(ExtraMSBuildArgs: "-p:TestSatelliteAssembliesFromPackage=true")); + + string binFrameworkDir = GetBinFrameworkDir(config, forPublish: false); + + // Microsoft.CodeAnalysis.CSharp has satellite assemblies for multiple locales + // Verify that at least some of them are present in the AppBundle + string[] expectedLocales = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant"]; + foreach (string locale in expectedLocales) + { + string satelliteDir = Path.Combine(binFrameworkDir, locale); + Assert.True(Directory.Exists(satelliteDir), $"Expected satellite directory '{locale}' to exist in {binFrameworkDir}"); + + string[] satelliteFiles = Directory.GetFiles(satelliteDir, "Microsoft.CodeAnalysis.CSharp.resources*"); + Assert.True(satelliteFiles.Length > 0, $"Expected Microsoft.CodeAnalysis.CSharp.resources.dll in {satelliteDir}"); + } + } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/SatelliteAssembliesTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/SatelliteAssembliesTest.cs index 8d7631e0c8f6ce..92a695d284e0cd 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/SatelliteAssembliesTest.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/SatelliteAssembliesTest.cs @@ -13,7 +13,7 @@ public static async Task Run(bool loadSatelliteAssemblies) if (loadSatelliteAssemblies) { ResourceLibrary.ResourceAccessor.Read(TestOutput.WriteLine, false); - await LoadSatelliteAssemblies(new[] { "es-ES" }); + await LoadSatelliteAssemblies(new[] { "es-ES", "fr-FR" }); } ResourceLibrary.ResourceAccessor.Read(TestOutput.WriteLine, true); diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceAccessor.cs b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceAccessor.cs index 27073846c66ae7..7c48551cabb3d8 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceAccessor.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceAccessor.cs @@ -15,5 +15,6 @@ public static void Read(Action testOuput, bool hasSatellites) var rm = new ResourceManager("ResourceLibrary.words", typeof(ResourceAccessor).Assembly); testOuput($"default: {rm.GetString("hello", CultureInfo.CurrentCulture)}"); testOuput($"es-ES {(hasSatellites ? "with" : "without")} satellite: {rm.GetString("hello", new CultureInfo("es-ES"))}"); + testOuput($"fr-FR {(hasSatellites ? "with" : "without")} satellite: {rm.GetString("hello", new CultureInfo("fr-FR"))}"); } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceLibrary.csproj b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceLibrary.csproj index 9a4419fdc08de5..8511ea8189bd03 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceLibrary.csproj +++ b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/ResourceLibrary.csproj @@ -4,4 +4,8 @@ Library true + + + + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/words.fr-FR.resx b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/words.fr-FR.resx new file mode 100644 index 00000000000000..61280687cdc2bd --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/ResourceLibrary/words.fr-FR.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + au revoir + + + + bonjour + + diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs index 2eafa784282373..34dcf1647aa3e2 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs @@ -66,7 +66,7 @@ public override bool Execute() { var filesToRemove = new List(); var assetCandidates = new List(); - var uniqueFileNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var uniqueRelativePaths = new HashSet(StringComparer.OrdinalIgnoreCase); try { @@ -117,46 +117,49 @@ public override bool Execute() assetCandidate.SetMetadata("RelatedAsset", Path.GetFullPath(Path.Combine(OutputPath, "wwwroot", "_framework", Path.GetFileName(resolvedFrom)))); - assetCandidates.Add(assetCandidate); - continue; + candidate = assetCandidate; } - - string relativePath = AssetsComputingHelper.GetCandidateRelativePath(candidate, FingerprintAssets, FingerprintDotNetJs); - candidate.SetMetadata("RelativePath", relativePath); - - // Workaround for https://github.com/dotnet/aspnetcore/issues/37574. - // For items added as "Reference" in project references, the OriginalItemSpec is incorrect. - // Ignore it, and use the FullPath instead. - if (candidate.GetMetadata("ReferenceSourceTarget") == "ProjectReference") + else { - candidate.SetMetadata("OriginalItemSpec", candidate.ItemSpec); + var culture = candidate.GetMetadata("Culture"); + if (!string.IsNullOrEmpty(culture)) + { + candidate.SetMetadata("AssetKind", "Build"); + candidate.SetMetadata("AssetRole", "Related"); + candidate.SetMetadata("AssetTraitName", "Culture"); + candidate.SetMetadata("AssetTraitValue", culture); + + var fileName = candidate.GetMetadata("FileName"); + var suffixIndex = fileName.Length - ".resources".Length; + var relatedAssetPath = Path.GetFullPath(Path.Combine( + OutputPath, + "wwwroot", + "_framework", + fileName.Substring(0, suffixIndex) + ProjectAssembly[0].GetMetadata("Extension"))); + + candidate.SetMetadata("RelatedAsset", relatedAssetPath); + candidate.SetMetadata("RelativePath", $"_framework/{culture}/{fileName}{candidate.GetMetadata("Extension")}"); + + Log.LogMessage(MessageImportance.Low, "Found satellite assembly '{0}' asset for inferred candidate '{1}' with culture '{2}'", candidate.ItemSpec, relatedAssetPath, culture); + } + else + { + // Workaround for https://github.com/dotnet/aspnetcore/issues/37574. + // For items added as "Reference" in project references, the OriginalItemSpec is incorrect. + // Ignore it, and use the FullPath instead. + if (candidate.GetMetadata("ReferenceSourceTarget") == "ProjectReference") + { + candidate.SetMetadata("OriginalItemSpec", candidate.ItemSpec); + } + + candidate.SetMetadata("RelativePath", AssetsComputingHelper.GetCandidateRelativePath(candidate, FingerprintAssets, FingerprintDotNetJs)); + } } - var culture = candidate.GetMetadata("Culture"); - if (!string.IsNullOrEmpty(culture)) + var relativePath = candidate.GetMetadata("RelativePath"); + if (!uniqueRelativePaths.Add(relativePath)) { - candidate.SetMetadata("AssetKind", "Build"); - candidate.SetMetadata("AssetRole", "Related"); - candidate.SetMetadata("AssetTraitName", "Culture"); - candidate.SetMetadata("AssetTraitValue", culture); - var fileName = candidate.GetMetadata("FileName"); - var suffixIndex = fileName.Length - ".resources".Length; - var relatedAssetPath = Path.GetFullPath(Path.Combine( - OutputPath, - "wwwroot", - "_framework", - fileName.Substring(0, suffixIndex) + ProjectAssembly[0].GetMetadata("Extension"))); - - candidate.SetMetadata("RelatedAsset", relatedAssetPath); - - Log.LogMessage(MessageImportance.Low, "Found satellite assembly '{0}' asset for inferred candidate '{1}' with culture '{2}'", candidate.ItemSpec, relatedAssetPath, culture); - } - - // Check for unique file name before adding candidate - var candidateFileName = Path.GetFileName(candidate.ItemSpec); - if (!uniqueFileNames.Add(candidateFileName)) - { - Log.LogMessage(MessageImportance.Low, "Skipping duplicate file name '{0}' for candidate '{1}'", candidateFileName, candidate.ItemSpec); + Log.LogMessage(MessageImportance.Low, "Skipping duplicate relative path '{0}' for candidate '{1}'", relativePath, candidate.ItemSpec); continue; } @@ -191,13 +194,18 @@ public override bool Execute() "_framework", ProjectAssembly[0].GetMetadata("FileName") + ProjectAssembly[0].GetMetadata("Extension"))); - var normalizedPath = assetCandidate.GetMetadata("TargetPath").Replace('\\', '/'); + var relativePath = $"_framework/{assetCandidate.GetMetadata("TargetPath").Replace('\\', '/')}"; + if (!uniqueRelativePaths.Add(relativePath)) + { + Log.LogMessage(MessageImportance.Low, "Skipping duplicate relative path '{0}' for candidate '{1}'", relativePath, projectSatelliteAssembly.ItemSpec); + continue; + } assetCandidate.SetMetadata("AssetKind", "Build"); assetCandidate.SetMetadata("AssetRole", "Related"); assetCandidate.SetMetadata("AssetTraitName", "Culture"); assetCandidate.SetMetadata("AssetTraitValue", candidateCulture); - assetCandidate.SetMetadata("RelativePath", Path.Combine("_framework", normalizedPath)); + assetCandidate.SetMetadata("RelativePath", relativePath); assetCandidate.SetMetadata("RelatedAsset", projectAssemblyAssetPath); assetCandidates.Add(assetCandidate);