diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 879bf42aafc..c53e1b5403a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -22,6 +22,8 @@ void LogUnresolvableJavaPeerSkippedWarning ( string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); void LogInvalidJavaNameError (string javaName, string invalidIdentifier); + void LogDuplicateJavaTypeError (string javaName); + void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName); void LogExportFieldWithParametersError (); void LogExportFieldOnGenericTypeError (); void LogExportFieldReturnsVoidError (); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 62fc324b513..afbb641a1ee 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -105,6 +105,24 @@ internal bool ValidateJavaNames (IReadOnlyList peers, string? appl { bool valid = true; var reportedNames = new HashSet (StringComparer.Ordinal); + foreach (var group in peers + .Where (ShouldGenerateJcw) + .GroupBy (peer => peer.JavaName, StringComparer.Ordinal) + .OrderBy (group => group.Key, StringComparer.Ordinal)) { + var firstAssemblyName = group.First ().AssemblyName; + if (group.All (peer => string.Equals (peer.AssemblyName, firstAssemblyName, StringComparison.Ordinal))) { + continue; + } + + var javaName = JniSignatureHelper.JniNameToJavaBinaryName (group.Key); + logger.LogDuplicateJavaTypeError (javaName); + foreach (var peer in group + .OrderBy (peer => peer.ManagedTypeName, StringComparer.Ordinal) + .ThenBy (peer => peer.AssemblyName, StringComparer.Ordinal)) { + logger.LogDuplicateJavaTypeDetailsError (javaName, $"{peer.ManagedTypeName}, {peer.AssemblyName}"); + } + valid = false; + } if (applicationJavaClass is not null && JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (applicationJavaClass, out var invalidApplicationIdentifier)) { ReportInvalidName (applicationJavaClass, invalidApplicationIdentifier); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 1c2b02aa583..ec430081f0f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -59,6 +59,10 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier); + public void LogDuplicateJavaTypeError (string javaName) => + log.LogCodedError ("XA4215", Properties.Resources.XA4215, javaName); + public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + log.LogCodedError ("XA4215", Properties.Resources.XA4215_Details, javaName, managedTypeName); public void LogExportFieldWithParametersError () => log.LogCodedError ("XA4205", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); public void LogExportFieldOnGenericTypeError () => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index dacb3c2ef96..d7378f387c5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -619,26 +619,44 @@ public void AndroidLibraryProjectsZipWithOddPaths ([Values (AndroidRuntime.CoreC } } - [Test] - public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void DuplicateJCWNames (string typemapImplementation, AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - // TODO: NativeAOT should fail the application build - if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("NativeAOT doesn't fail the application build"); - } + var source1 = """ + namespace Library1; - var source = @"[Android.Runtime.Register (""examplelib.EmptyClass"")] public class EmptyClass : Java.Lang.Object { }"; + [Android.Runtime.Register ("examplelib.DuplicatePeer")] + public class FirstPeer : Java.Lang.Object + { + public FirstPeer (int value) + { + } + } + """; + var source2 = """ + namespace Library2; + + [Android.Runtime.Register ("examplelib.DuplicatePeer")] + public class SecondPeer : Java.Lang.Object + { + public SecondPeer (string value) + { + } + } + """; var library1 = new XamarinAndroidLibraryProject () { IsRelease = isRelease, ProjectName = "Library1", Sources = { - new BuildItem.Source ("EmptyClass.cs") { - TextContent = () => source + new BuildItem.Source ("FirstPeer.cs") { + TextContent = () => source1 } } }; @@ -647,8 +665,8 @@ public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.N IsRelease = isRelease, ProjectName = "Library2", Sources = { - new BuildItem.Source ("EmptyClass.cs") { - TextContent = () => source + new BuildItem.Source ("SecondPeer.cs") { + TextContent = () => source2 } } }; @@ -662,21 +680,40 @@ public void DuplicateJCWNames ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.N }, }; app.SetRuntime (runtime); - var projectPath = Path.Combine ("temp", TestName); + app.SetProperty ("AndroidTypeMapImplementation", typemapImplementation); + var projectPath = Path.Combine ("temp", $"{TestName}_{Guid.NewGuid ():N}"); using var lib1b = CreateDllBuilder (Path.Combine (projectPath, library1.ProjectName), cleanupAfterSuccessfulBuild: false); using var lib2b = CreateDllBuilder (Path.Combine (projectPath, library2.ProjectName), cleanupAfterSuccessfulBuild: false); Assert.IsTrue (lib1b.Build (library1), "Build of Library1 should have succeeded"); Assert.IsTrue (lib2b.Build (library2), "Build of Library2 should have succeeded"); - using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName)); + using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName), cleanupAfterSuccessfulBuild: false); appb.ThrowOnBuildFailure = false; Assert.IsFalse (appb.Build (app), "Build of App1 should have failed"); - IEnumerable errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")); - Assert.NotNull (errors, "Error should be XA4215"); - StringAssertEx.Contains ("EmptyClass", errors, "Error should mention the conflicting type name"); - StringAssertEx.Contains ("Library1", errors, "Error should mention all of the assemblies with conflicts"); - StringAssertEx.Contains ("Library2", errors, "Error should mention all of the assemblies with conflicts"); +var errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")).ToList (); +Assert.IsNotEmpty (errors, "Error should be XA4215"); + StringAssertEx.Contains ("examplelib.DuplicatePeer", errors, "Error should mention the conflicting Java type name"); + StringAssertEx.Contains ("Library1.FirstPeer", errors, "Error should mention the first conflicting managed type"); + StringAssertEx.Contains ("Library2.SecondPeer", errors, "Error should mention the second conflicting managed type"); + Assert.IsFalse (appb.LastBuildOutput.Any (x => x.Contains ("warning XA4214")), + "Distinct managed type names should not produce XA4214."); + + var acwMapFile = appb.Output.GetIntermediaryPath ("acw-map.txt"); + if (typemapImplementation == "llvm-ir") { + var javaFile = appb.Output.GetIntermediaryPath (Path.Combine ("android", "src", "examplelib", "DuplicatePeer.java")); + FileAssert.Exists (javaFile, "llvm-ir generates JCW sources before XA4215 is detected."); + FileAssert.DoesNotExist (acwMapFile, "llvm-ir should not write an ambiguous acw-map."); + } else { + var typemapDirectory = appb.Output.GetIntermediaryPath ("typemap"); + var javaFile = Path.Combine (typemapDirectory, "java", "examplelib", "DuplicatePeer.java"); + FileAssert.DoesNotExist (javaFile, "A failed trimmable typemap build should not write an ambiguous JCW source."); + FileAssert.DoesNotExist (acwMapFile, "A failed trimmable typemap build should not write an ambiguous acw-map."); + if (Directory.Exists (typemapDirectory)) { + Assert.IsEmpty (Directory.GetFiles (typemapDirectory, "*.dll"), + "A failed trimmable typemap build should not write typemap assemblies."); + } + } } [Test] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 64cd0919df9..08e12037c43 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -53,6 +53,10 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); + public void LogDuplicateJavaTypeError (string javaName) => + logMessages.Add ($"XA4215: The Java type `{javaName}` is generated by more than one managed type."); + public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + logMessages.Add ($"XA4215: `{javaName}` generated by: {managedTypeName}"); public void LogExportFieldWithParametersError () => logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); public void LogExportFieldReturnsVoidError () => @@ -104,6 +108,34 @@ public void ValidateJavaNames_ContextualKeywordInPackage_IsValid () Assert.DoesNotContain (logMessages, message => message.Contains ("XA4258")); } + [Fact] + public void ValidateJavaNames_DuplicateGeneratedJcw_LogsXA4215 () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "examplelib/DuplicatePeer", + CompatJniName = "examplelib/DuplicatePeer", + ManagedTypeName = "Library1.FirstPeer", + ManagedTypeNamespace = "Library1", + ManagedTypeShortName = "FirstPeer", + AssemblyName = "Library1", + }, + new JavaPeerInfo { + JavaName = "examplelib/DuplicatePeer", + CompatJniName = "examplelib/DuplicatePeer", + ManagedTypeName = "Library2.SecondPeer", + ManagedTypeNamespace = "Library2", + ManagedTypeShortName = "SecondPeer", + AssemblyName = "Library2", + }, + }; + + Assert.False (CreateGenerator ().ValidateJavaNames (peers)); + Assert.Contains (logMessages, message => message == "XA4215: The Java type `examplelib.DuplicatePeer` is generated by more than one managed type."); + Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library1.FirstPeer, Library1"); + Assert.Contains (logMessages, message => message == "XA4215: `examplelib.DuplicatePeer` generated by: Library2.SecondPeer, Library2"); + } + [Theory] [InlineData ("com/example/Outer$for", null, "for")] [InlineData ("com/example/Outer$record", null, "record")]