diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 1e0909c2b53..4cf450cdeb0 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -219,6 +219,8 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type. + [XA4252](xa4252.md): Insecure HTTP Maven repository URL '{url}' is not allowed. Use an HTTPS URL, or set AllowInsecureHttp="true" metadata on the item to override this check. + [XA4253](xa4253.md): Generated Java callable wrapper code changed: '{path}' ++ [XA4254](xa4254.md): Trimmable type map Java source input directory '{input}' and output directory '{output}' must be different. ++ [XA4255](xa4255.md): Generated trimmable type map Java source '{path}' was not found. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4253.md b/Documentation/docs-mobile/messages/xa4253.md index 74632be962c..8c27b5fb802 100644 --- a/Documentation/docs-mobile/messages/xa4253.md +++ b/Documentation/docs-mobile/messages/xa4253.md @@ -10,7 +10,7 @@ f1_keywords: ## Example messages -``` +```text error XA4253: Generated Java callable wrapper code changed: 'obj/Release/android/src/mono/MonoRuntimeProvider.java' ``` diff --git a/Documentation/docs-mobile/messages/xa4254.md b/Documentation/docs-mobile/messages/xa4254.md new file mode 100644 index 00000000000..6997cf235b1 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4254.md @@ -0,0 +1,25 @@ +--- +title: .NET for Android error XA4254 +description: XA4254 error code +ms.date: 05/20/2026 +f1_keywords: + - "XA4254" +--- + +# .NET for Android error XA4254 + +## Example message + +```text +error XA4254: Trimmable type map Java source input directory 'obj/Release/net11.0-android/typemap/java' and output directory 'obj/Release/net11.0-android/typemap/java' must be different. +``` + +## Issue + +The trimmable type map build tried to clean the Java source output directory, but the configured input and output directories resolved to the same path. + +Cleaning the output directory in this configuration would delete the input Java sources before they can be copied. + +## Solution + +This error indicates an internal build configuration problem. File an issue at and include the full build log. diff --git a/Documentation/docs-mobile/messages/xa4255.md b/Documentation/docs-mobile/messages/xa4255.md new file mode 100644 index 00000000000..633df761ecb --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4255.md @@ -0,0 +1,27 @@ +--- +title: .NET for Android error XA4255 +description: XA4255 error code +ms.date: 05/20/2026 +f1_keywords: + - "XA4255" +--- + +# .NET for Android error XA4255 + +## Example message + +```text +error XA4255: Generated trimmable type map Java source 'obj/Release/net11.0-android/typemap/java/my/app/MainActivity.java' was not found. +``` + +## Issue + +The post-trim trimmable type map scan expected to copy a generated Java source file from the pre-trim Java source directory, but the file was missing. + +This can happen if intermediate build outputs are stale or if the generated Java source list no longer matches the files on disk. + +## Solution + +Delete the project's `obj` and `bin` directories, then rebuild. + +If the error persists after a clean rebuild, file an issue at and include the full build log. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 752e1083157..7382a4ed60a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -48,7 +48,7 @@ public sealed record JavaPeerInfo /// Framework ACWs are generated by the SDK and can be trimmed like bindings unless /// another rule explicitly roots them. /// - public bool IsFrameworkAssembly { get; init; } + public bool IsFrameworkAssembly { get; set; } /// /// True when per-rank array typemap entries should be generated for this peer. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5a391f520b6..95d0446b205 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -34,7 +34,8 @@ public TrimmableTypeMapResult Execute ( ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, string? packageNamingPolicy = null, - int maxArrayRank = 0) + int maxArrayRank = 0, + bool generateTypeMapAssemblies = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -48,16 +49,15 @@ public TrimmableTypeMapResult Execute ( logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } + MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies ( - allPeers, - systemRuntimeVersion, - useSharedTypemapUniverse, - maxArrayRank); + var generatedAssemblies = generateTypeMapAssemblies + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); @@ -212,6 +212,15 @@ List GenerateTypeMapAssemblies ( return generatedAssemblies; } + static void MarkFrameworkAssemblyPeers (List allPeers, HashSet frameworkAssemblyNames) + { + foreach (var peer in allPeers) { + if (frameworkAssemblyNames.Contains (peer.AssemblyName)) { + peer.IsFrameworkAssembly = true; + } + } + } + /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index a76819a61a1..293f19585ef 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -48,6 +48,45 @@ OutputFile="$(_ProguardProjectConfiguration)" /> + + + <_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)" + Condition=" '%(Extension)' == '.dll' " /> + + + + + + + + + + + + + + + <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> + <_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" /> + + $(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java <_TypeMapAssembliesListFile>$(_TypeMapOutputDirectory)typemap-assemblies.txt + <_PostTrimTypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/linked-java + <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTypeMapJavaOutputDirectory) + <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' ">$(_TypeMapJavaOutputDirectory) + <_PostTrimTrimmableTypeMapJavaStamp>$(_TypeMapBaseOutputDir)stamp/_GeneratePostTrimTrimmableTypeMapJavaSources.stamp + <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) + <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll @@ -251,7 +257,7 @@ Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp"> - <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaStubsSourceDirectory)/**/*.java" /> diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 6d32f29e314..8ac81bd0e32 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1551,6 +1551,24 @@ public static string XA4251 { } } + /// + /// Looks up a localized string similar to Trimmable type map Java source input directory '{0}' and output directory '{1}' must be different.. + /// + public static string XA4254 { + get { + return ResourceManager.GetString("XA4254", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generated trimmable type map Java source '{0}' was not found.. + /// + public static string XA4255 { + get { + return ResourceManager.GetString("XA4255", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 2fd54e571e5..a817ea46d97 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1141,6 +1141,17 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Generated Java callable wrapper code changed: '{0}' {0} - The path to the generated Java callable wrapper file + + Trimmable type map Java source input directory '{0}' and output directory '{1}' must be different. + The following are literal names and should not be translated: Trimmable type map, Java. +{0} - Full path to the Java source input directory +{1} - Full path to the Java source output directory + + + Generated trimmable type map Java source '{0}' was not found. + The following are literal names and should not be translated: trimmable type map, Java. +{0} - Full path to the generated Java source file + Command '{0}' failed.\n{1} '{0}' is a failed command name (potentially with path) followed by all the arguments passed to it. {1} is the combined output on the standard error and standard output streams. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 6184f36b467..9a03ef4ff21 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -57,6 +57,7 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string OutputDirectory { get; set; } = ""; [Required] public string JavaSourceOutputDirectory { get; set; } = ""; + public string? JavaSourceInputDirectory { get; set; } [Required] public string TargetFrameworkVersion { get; set; } = ""; @@ -92,6 +93,8 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } + public bool GenerateTypeMapAssemblies { get; set; } = true; + public bool CleanJavaSourceOutputDirectory { get; set; } [Output] public ITaskItem [] GeneratedAssemblies { get; set; } = []; @@ -116,8 +119,19 @@ public override bool RunTask () foreach (var assemblyName in FrameworkAssemblyNames) { frameworkAssemblyNames.Add (assemblyName); } + if (CleanJavaSourceOutputDirectory && !JavaSourceInputDirectory.IsNullOrEmpty ()) { + var inputDirectory = Path.GetFullPath (JavaSourceInputDirectory); + var outputDirectory = Path.GetFullPath (JavaSourceOutputDirectory); + if (string.Equals (inputDirectory, outputDirectory, StringComparison.OrdinalIgnoreCase)) { + Log.LogCodedError ("XA4254", Properties.Resources.XA4254, inputDirectory, outputDirectory); + return false; + } + } Directory.CreateDirectory (OutputDirectory); + if (CleanJavaSourceOutputDirectory && Directory.Exists (JavaSourceOutputDirectory)) { + Directory.Delete (JavaSourceOutputDirectory, recursive: true); + } Directory.CreateDirectory (JavaSourceOutputDirectory); var peReaders = new List (); @@ -168,11 +182,16 @@ public override bool RunTask () manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, - maxArrayRank: MaxArrayRank); + maxArrayRank: MaxArrayRank, + generateTypeMapAssemblies: GenerateTypeMapAssemblies); - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); - WriteGeneratedAssembliesListFile (GeneratedAssemblies); - GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + if (GenerateTypeMapAssemblies) { + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); + WriteGeneratedAssembliesListFile (GeneratedAssemblies); + } + GeneratedJavaFiles = JavaSourceInputDirectory.IsNullOrEmpty () + ? WriteJavaSourcesToDisk (result.GeneratedJavaSources) + : CopyJavaSourcesFromInputDirectory (result.GeneratedJavaSources); // Write manifest to disk if generated if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { @@ -247,6 +266,29 @@ void WriteGeneratedAssembliesListFile (IReadOnlyList assemblies) Files.CopyIfStringChanged (text, GeneratedAssembliesListFile); } + ITaskItem [] CopyJavaSourcesFromInputDirectory (IReadOnlyList javaSources) + { + var items = new List (); + foreach (var source in javaSources) { + string inputPath = Path.Combine (JavaSourceInputDirectory ?? "", source.RelativePath); + if (!File.Exists (inputPath)) { + Log.LogCodedError ("XA4255", Properties.Resources.XA4255, inputPath); + continue; + } + + string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); + string? dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + using (var stream = File.OpenRead (inputPath)) { + Files.CopyIfStreamChanged (stream, outputPath); + } + items.Add (new TaskItem (outputPath)); + } + return items.ToArray (); + } + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) { // Build a map from assembly name -> source path for timestamp comparison diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 148641dc855..e9c180cb794 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using Mono.Cecil; using NUnit.Framework; +using Xamarin.Android.AssemblyStore; using Xamarin.Android.Tasks; using Xamarin.Android.Tools; using Xamarin.ProjectTools; @@ -288,6 +289,56 @@ public void ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff "trimmable typemap builds should emit array typemap sentinels when dynamic code is disabled."); } + [Test] + public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_DoesNotPackageUnlinkedTypeMapAssemblies () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + PackageName = "com.xamarin.typemapcomparison", + ProjectName = "TypemapComparison", + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (Path.Combine ("temp", $"TypemapComparison_trimmable_single_rid_{Guid.NewGuid ():N}")); + Assert.IsTrue (builder.Build (proj), "trimmable single-RID build should have succeeded."); + + var apkDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var apkPath = Directory.GetFiles (apkDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); + var typeMapDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "typemap")); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); + var javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "android", "src")); + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "android", "bin", "classes.dex")); + var acwMapPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "acw-map.txt")); + var proguardPrimaryPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "proguard", "proguard_project_primary.cfg")); + + DirectoryAssert.Exists (typeMapDirectory, "trimmable build should generate typemap assemblies."); + DirectoryAssert.Exists (linkedAssemblyDirectory, "Release trimmable build should run ILLink."); + + var unlinkedTypeMapAssemblies = Directory.GetFiles (typeMapDirectory, "*.dll") + .Where (file => !File.Exists (Path.Combine (linkedAssemblyDirectory, Path.GetFileName (file)))) + .Select (Path.GetFileName) + .OrderBy (name => name, StringComparer.Ordinal) + .ToArray (); + Assert.IsNotEmpty (unlinkedTypeMapAssemblies, "Test setup should include typemap assemblies that ILLink removed."); + + var packagedAssemblyNames = ReadPackagedManagedAssemblyNames (apkPath, AndroidTargetArch.Arm64); + var packagedUnlinkedTypeMapAssemblies = packagedAssemblyNames.Intersect (unlinkedTypeMapAssemblies, StringComparer.Ordinal).OrderBy (name => name, StringComparer.Ordinal).ToArray (); + Assert.IsEmpty ( + packagedUnlinkedTypeMapAssemblies, + $"{apkPath} should package linked typemap assemblies, not untrimmed typemap assemblies removed by ILLink."); + + AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (dexFile, javaSourceDirectory, acwMapPath, proguardPrimaryPath); + } + [Test] public void TrimmableTypeMap_PreserveLists_ArePackagedInSdk () { @@ -558,6 +609,50 @@ DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplemen TypeMapAssembliesContainType (linkedAssemblyDirectory, "__ArrayMapRank1")); } + ISet ReadPackagedManagedAssemblyNames (string apkPath, AndroidTargetArch targetArch) + { + (var explorers, var errorMessage) = AssemblyStoreExplorer.Open (apkPath); + Assert.IsNull (errorMessage, $"{apkPath} should contain readable assembly stores."); + Assert.IsNotNull (explorers, $"{apkPath} should contain assembly stores."); + + var explorer = explorers.FirstOrDefault (e => e.TargetArch == targetArch); + Assert.IsNotNull (explorer, $"{apkPath} should contain an {targetArch} assembly store."); + + return explorer.Assemblies + .Where (a => !a.Ignore && a.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase) && !a.Name.EndsWith (".ni.dll", StringComparison.OrdinalIgnoreCase)) + .Select (a => a.Name) + .ToHashSet (StringComparer.Ordinal); + } + + void AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (string dexFile, string javaSourceDirectory, string acwMapPath, string proguardPrimaryPath) + { + const string deadManagedType = "Android.Speech.IRecognitionListenerImplementor"; + const string deadJavaName = "Lmono/android/speech/RecognitionListenerImplementor;"; + const string deadJavaDotName = "mono.android.speech.RecognitionListenerImplementor"; + + Assert.IsTrue ( + Directory.EnumerateFiles (javaSourceDirectory, "MainActivity.java", SearchOption.AllDirectories).Any (), + "Post-trim Java source generation should keep the app activity JCW."); + FileAssert.DoesNotExist ( + Path.Combine (javaSourceDirectory, "mono", "android", "speech", "RecognitionListenerImplementor.java"), + "Post-trim Java source generation should not copy framework listener implementors removed by ILLink."); + + FileAssert.Exists (acwMapPath, "Post-trim scan should rewrite acw-map.txt for R8."); + var acwMap = File.ReadAllText (acwMapPath); + Assert.IsFalse (acwMap.Contains (deadManagedType, StringComparison.Ordinal), $"{acwMapPath} should be based on linked assemblies."); + Assert.IsFalse (acwMap.Contains (deadJavaDotName, StringComparison.Ordinal), $"{acwMapPath} should not keep removed framework listener implementors."); + + FileAssert.Exists (proguardPrimaryPath, "R8 should generate a primary proguard configuration from the post-trim acw-map."); + Assert.IsFalse ( + File.ReadAllText (proguardPrimaryPath).Contains (deadJavaDotName, StringComparison.Ordinal), + $"{proguardPrimaryPath} should not keep removed framework listener implementors."); + + FileAssert.Exists (dexFile, "R8 should produce classes.dex."); + Assert.IsFalse ( + DexUtils.ContainsClass (deadJavaName, dexFile, AndroidSdkPath), + $"{dexFile} should not contain the removed framework listener implementor."); + } + string FindOutputFile (ProjectBuilder builder, XamarinAndroidApplicationProject proj, string fileName) { var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath);