diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index b9e3a345395..239ae6489bb 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -221,8 +221,6 @@ 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/xa4254.md b/Documentation/docs-mobile/messages/xa4254.md deleted file mode 100644 index 6997cf235b1..00000000000 --- a/Documentation/docs-mobile/messages/xa4254.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -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 deleted file mode 100644 index 633df761ecb..00000000000 --- a/Documentation/docs-mobile/messages/xa4255.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -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/external/Java.Interop b/external/Java.Interop index 70493645c7d..8d544738ad2 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 70493645c7d95648010a4cef948234a28744c03f +Subproject commit 8d544738ad294b4faf13d189eeeb02f0313e00b3 diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index 132f7903534..716b7ded6ff 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit 132f790353413dcaef231e720e255364a310b3bd +Subproject commit 716b7ded6ff6820e34ed6db8dadf5644b4e78204 diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 5891149578f..b920167dd23 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -61,7 +61,7 @@ static NativeAotRuntimeOptions CreateJreVM (NativeAotRuntimeOptions builder) builder.TypeManager ??= CreateDefaultTypeManager (); #endif // NET - builder.ValueManager ??= new JavaMarshalValueManager (); + builder.ValueManager ??= Android.Runtime.JNIEnvInit.CreateValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) @@ -81,7 +81,7 @@ static JniRuntime.JniTypeManager CreateDefaultTypeManager () return new TrimmableTypeMapTypeManager (); } - return new ManagedTypeManager (); + throw new NotImplementedException (); } public override string? GetCurrentManagedThreadName () diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml index 124edd61d93..29e7d7503ae 100644 --- a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml @@ -27,6 +27,7 @@ + true 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 b21afc1b6e4..a76819a61a1 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,45 +48,6 @@ OutputFile="$(_ProguardProjectConfiguration)" /> - - - <_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)" - Condition=" '%(Extension)' == '.dll' and ('%(RuntimeIdentifier)' == '' or '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' or '%(RuntimeIdentifier)' == '$(_PostTrimTypeMapFirstRuntimeIdentifier)') " /> - - - - - - - - - - - - - - - <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> - <_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" /> - - $(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java <_TypeMapAssembliesListFile>$(_TypeMapOutputDirectory)typemap-assemblies.txt - <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(IntermediateOutputPath) - <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_PostTrimTypeMapJavaBaseOutputDir)' == '' ">$(_TypeMapBaseOutputDir) - <_PostTrimTypeMapJavaOutputDirectory>$(_PostTrimTypeMapJavaBaseOutputDir)typemap/linked-java - <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(RuntimeIdentifiers)' != '' ">$([System.String]::Copy('$(RuntimeIdentifiers)').Split(';')[0]) - <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' ">$(RuntimeIdentifier) - <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTypeMapJavaOutputDirectory) - <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' ">$(_TypeMapJavaOutputDirectory) - <_PostTrimTrimmableTypeMapJavaStamp>$(_PostTrimTypeMapJavaBaseOutputDir)stamp/_GeneratePostTrimTrimmableTypeMapJavaSources.stamp - <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) - <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll + <_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp @@ -48,6 +39,8 @@ + - - + - <_TypeMapJavaFiles Include="$(_TypeMapJavaStubsSourceDirectory)/**/*.java" /> + <_TypeMapJavaFiles Remove="@(_TypeMapJavaFiles)" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 42e59329019..64bde1a7cb8 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1587,24 +1587,6 @@ 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 f41614e9e2a..278abcf70b7 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1155,17 +1155,6 @@ 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 9a03ef4ff21..8c39a2021bd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -10,6 +10,7 @@ using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; @@ -57,7 +58,6 @@ 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; } = ""; @@ -93,14 +93,14 @@ 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; } = []; [Output] public ITaskItem [] GeneratedJavaFiles { get; set; } = []; [Output] + public ITaskItem [] DeletedJavaFiles { get; set; } = []; + [Output] public string[]? AdditionalProviderSources { get; set; } public override bool RunTask () @@ -119,19 +119,8 @@ 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 (); @@ -182,16 +171,12 @@ public override bool RunTask () manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, - maxArrayRank: MaxArrayRank, - generateTypeMapAssemblies: GenerateTypeMapAssemblies); + maxArrayRank: MaxArrayRank); - if (GenerateTypeMapAssemblies) { - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); - WriteGeneratedAssembliesListFile (GeneratedAssemblies); - } - GeneratedJavaFiles = JavaSourceInputDirectory.IsNullOrEmpty () - ? WriteJavaSourcesToDisk (result.GeneratedJavaSources) - : CopyJavaSourcesFromInputDirectory (result.GeneratedJavaSources); + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies); + WriteGeneratedAssembliesListFile (GeneratedAssemblies); + GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + DeletedJavaFiles = DeleteStaleJavaSources (GeneratedJavaFiles); // Write manifest to disk if generated if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { @@ -266,40 +251,9 @@ 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) + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies) { - // Build a map from assembly name -> source path for timestamp comparison - var sourcePathByName = new Dictionary (StringComparer.Ordinal); - foreach (var path in assemblyPaths) { - var name = Path.GetFileNameWithoutExtension (path); - sourcePathByName [name] = path; - } - var items = new List (); - bool anyRegenerated = false; foreach (var assembly in assemblies) { if (assembly.Name == "_Microsoft.Android.TypeMaps") { @@ -307,50 +261,23 @@ ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, } string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); - // Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") - string originalName = assembly.Name; - if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { - originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); - } - - if (IsUpToDate (outputPath, originalName, sourcePathByName)) { - Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); - } else { - Files.CopyIfStreamChanged (assembly.Content, outputPath); - anyRegenerated = true; - Log.LogDebugMessage ($" {assembly.Name}: written"); - } + var changed = Files.CopyIfStreamChanged (assembly.Content, outputPath); + Log.LogDebugMessage ($" {assembly.Name}: {(changed ? "written" : "unchanged")}"); items.Add (new TaskItem (outputPath)); } - // Root assembly — regenerate if any per-assembly typemap changed var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); if (rootAssembly is not null) { string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); - if (anyRegenerated || !File.Exists (rootOutputPath)) { - Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); - Log.LogDebugMessage ($" Root: written"); - } else { - Log.LogDebugMessage ($" Root: up to date, skipping"); - } + var changed = Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); + Log.LogDebugMessage ($" Root: {(changed ? "written" : "unchanged")}"); items.Add (new TaskItem (rootOutputPath)); } return items.ToArray (); } - static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) - { - if (!File.Exists (outputPath)) { - return false; - } - if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { - return false; - } - return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); - } - ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) { var items = new List (); @@ -370,6 +297,30 @@ ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSour return items.ToArray (); } + ITaskItem [] DeleteStaleJavaSources (IReadOnlyCollection generatedJavaFiles) + { + var expectedFiles = new HashSet ( + generatedJavaFiles.Select (i => Path.GetFullPath (i.ItemSpec)), + Path.DirectorySeparatorChar == '\\' ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); + var deleted = new List (); + + foreach (var path in Directory.EnumerateFiles (JavaSourceOutputDirectory, "*.java", SearchOption.AllDirectories)) { + var fullPath = Path.GetFullPath (path); + if (expectedFiles.Contains (fullPath)) { + continue; + } + + File.Delete (fullPath); + Log.LogDebugMessage ($"Deleted stale generated Java source '{fullPath}'."); + + var item = new TaskItem (fullPath); + item.SetMetadata ("RelativePath", PathUtil.GetRelativePath (JavaSourceOutputDirectory, fullPath)); + deleted.Add (item); + } + + return deleted.ToArray (); + } + static Version ParseTargetFrameworkVersion (string tfv) { if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 0bce8de3328..d5d63d2078f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -1794,20 +1794,7 @@ public void XA4310 ([Values ("apk", "aab")] string packageFormat, [Values (Andro StringAssertEx.Contains ("error XA4310", builder.LastBuildOutput, "Error should be XA4310"); StringAssertEx.Contains ("`DoesNotExist`", builder.LastBuildOutput, "Error should include the name of the nonexistent file"); - if (runtime != AndroidRuntime.NativeAOT) { - builder.AssertHasNoWarnings (); - return; - } - - // NativeAOT currently (Nov 2025) produces the following warning - // warning IL3053: Assembly 'Mono.Android' produced AOT analysis warnings. - string expectedWarning = "warning IL3053:"; - Assert.IsNotNull ( - builder.LastBuildOutput - .SkipWhile (x => !x.StartsWith ("Build FAILED.", StringComparison.Ordinal)) - .FirstOrDefault (x => x.Contains (expectedWarning)), - $"Build output should contain '{expectedWarning}'." - ); + builder.AssertHasNoWarnings (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 6d92d889def..41106f7741e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -402,28 +402,7 @@ public void BuildHasNoWarnings (bool isRelease, bool multidex, string packageFor proj.SetProperty ("TrimmerSingleWarn", "false"); using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - - if (runtime == AndroidRuntime.NativeAOT) { - // NativeAOT currently (Nov 2025) produces 10 `ILC : AOT analysis warning IL3050` warnings for various - // bits of code. Even though this test expects no warnings and the above likely make the app not work - // correctly at run time, it is still worth running this test under NativeAOT to test for the absence - // of other warnings. - int numberOfExpectedWarnings = 10; - - Assert.IsTrue ( - StringAssertEx.ContainsText ( - b.LastBuildOutput, - $" {numberOfExpectedWarnings} Warning(s)" - ), - $"{b.BuildLogFile} should have exactly {numberOfExpectedWarnings} MSBuild warnings for NativeAOT." - ); - - const string expectedWarningIL3050 = "ILC : AOT analysis warning IL3050:"; - var warnings = b.LastBuildOutput.SkipWhile (x => !x.StartsWith ("Build succeeded.", StringComparison.Ordinal)).Where (x => x.Contains (expectedWarningIL3050, StringComparison.Ordinal)); - Assert.IsTrue (warnings.Count () == numberOfExpectedWarnings, $"Expected {numberOfExpectedWarnings} 'IL3050' warnings, found {warnings.Count ()}"); - } else { - b.AssertHasNoWarnings (); - } + b.AssertHasNoWarnings (); Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, "Warning: end of file not at end of a line"), "Should not get a warning from the task."); var lockFile = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, ".__lock"); @@ -437,22 +416,13 @@ static IEnumerable Get_BuildHasTrimmerWarningsData () foreach (AndroidRuntime runtime in new[] { AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT }) { AddTestData (runtime, "", new string [0], false); - - if (runtime == AndroidRuntime.NativeAOT) { - AddTestData (runtime, "", new [] { "IL2055", "IL3050" }, true, 2); - } else { - AddTestData (runtime, "", new string [0], true); - } - AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, 2); - AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, false, 1); - AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, 2); - AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, false); - - if (runtime == AndroidRuntime.NativeAOT) { - AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 2); - } else { - AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 3); - } + AddTestData (runtime, "", new string [0], true); + AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new [] { "IL2055" }, true); + AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [0], true); + AddTestData (runtime, "TrimMode=full", new string [0], false); + AddTestData (runtime, "TrimMode=full", new string [0], true); + AddTestData (runtime, "IsAotCompatible=true", new string [0], false); + AddTestData (runtime, "IsAotCompatible=true", new string [0], true); } return ret; @@ -491,9 +461,11 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, proj.ItemGroupList.Add (ignoreIlcWarnings); } proj.SetRuntimeIdentifier ("arm64-v8a"); - proj.MainActivity = proj.DefaultMainActivity - .Replace ("//${FIELDS}", "Type type = typeof (List<>);") - .Replace ("//${AFTER_ONCREATE}", "Console.WriteLine (type.MakeGenericType (typeof (object)));"); + if (codes.Length != 0) { + proj.MainActivity = proj.DefaultMainActivity + .Replace ("//${FIELDS}", "Type type = typeof (List<>);") + .Replace ("//${AFTER_ONCREATE}", "Console.WriteLine (type.MakeGenericType (typeof (object)));"); + } proj.SetProperty ("TrimmerSingleWarn", "false"); if (!string.IsNullOrEmpty (properties)) { @@ -511,8 +483,9 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, if (codes.Length == 0) { b.AssertHasNoWarnings (); } else { - totalWarnings ??= codes.Length; - Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, $"{totalWarnings} Warning(s)"), $"Should receive {totalWarnings} warnings"); + if (totalWarnings.HasValue) { + Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, $"{totalWarnings} Warning(s)"), $"Should receive {totalWarnings} warnings"); + } foreach (var code in codes) { Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, code), $"Should receive {code} warning"); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 7a6f121832d..5a76c868702 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Mono.Cecil; using NUnit.Framework; using Xamarin.Android.Tasks; using Xamarin.ProjectTools; @@ -77,6 +79,46 @@ public void Execute_WithMonoAndroid_ProducesOutputs () } } + [Test] + public void Execute_SameInputs_ProducesByteStableAssemblies () + { + var path = Path.Combine ("temp", TestName); + var firstOutputDir = Path.Combine (Root, path, "first", "typemap"); + var firstJavaDir = Path.Combine (Root, path, "first", "java"); + var secondOutputDir = Path.Combine (Root, path, "second", "typemap"); + var secondJavaDir = Path.Combine (Root, path, "second", "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var assemblies = new [] { monoAndroidItem }; + var task1 = CreateTask (assemblies, firstOutputDir, firstJavaDir); + Assert.IsTrue (task1.Execute (), "First run should succeed."); + var task2 = CreateTask (assemblies, secondOutputDir, secondJavaDir); + Assert.IsTrue (task2.Execute (), "Second run should succeed."); + + var firstAssemblies = ReadGeneratedAssemblyBytes (task1.GeneratedAssemblies); + var secondAssemblies = ReadGeneratedAssemblyBytes (task2.GeneratedAssemblies); + + CollectionAssert.AreEquivalent (firstAssemblies.Keys, secondAssemblies.Keys, "Generated assembly set should be stable."); + foreach (var name in firstAssemblies.Keys) { + CollectionAssert.AreEqual (firstAssemblies [name], secondAssemblies [name], $"{name} should be byte-stable for identical inputs."); + } + } + + [Test] + public void RootTypeMapAssembly_SystemRuntimeVersion_ChangesMvid () + { + var first = GenerateRootTypeMapAssembly (new Version (11, 0)); + var second = GenerateRootTypeMapAssembly (new Version (11, 1)); + + Assert.AreNotEqual (ReadMvid (first), ReadMvid (second), + "Root typemap assembly MVID should change when emitted System.Runtime references change."); + } + [Test] public void Execute_SecondRun_OutputsAreUpToDate () { @@ -110,6 +152,34 @@ public void Execute_SecondRun_OutputsAreUpToDate () "Typemap assembly should NOT be rewritten when content hasn't changed."); } + [Test] + public void Execute_MaxArrayRankChange_RewritesGeneratedAssemblies () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var assemblies = new [] { monoAndroidItem }; + + var task1 = CreateTask (assemblies, outputDir, javaDir); + task1.MaxArrayRank = 0; + Assert.IsTrue (task1.Execute (), "First run should succeed."); + Assert.IsFalse (GeneratedAssembliesContainType (task1.GeneratedAssemblies, "__ArrayMapRank1"), + "MaxArrayRank=0 should not emit array-rank sentinel types."); + + var task2 = CreateTask (assemblies, outputDir, javaDir); + task2.MaxArrayRank = 1; + Assert.IsTrue (task2.Execute (), "Second run should succeed."); + Assert.IsTrue (GeneratedAssembliesContainType (task2.GeneratedAssemblies, "__ArrayMapRank1"), + "Changing MaxArrayRank should rewrite generated typemap assemblies even when source assemblies did not change."); + } + [Test] public void Execute_WritesGeneratedAssembliesListFile () { @@ -139,6 +209,32 @@ public void Execute_WritesGeneratedAssembliesListFile () CollectionAssert.DoesNotContain (listedAssemblies, staleAssembly); } + [Test] + public void Execute_DeletesStaleGeneratedJavaSources () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var staleJavaFile = Path.Combine (javaDir, "stale", "Old.java"); + var staleJavaDirectory = Path.GetDirectoryName (staleJavaFile); + + if (staleJavaDirectory is null) { + throw new InvalidOperationException ("Could not determine stale Java directory."); + } + Directory.CreateDirectory (staleJavaDirectory); + File.WriteAllText (staleJavaFile, "class Old {}"); + + var task = CreateTask ([], outputDir, javaDir); + + Assert.IsTrue (task.Execute (), "Task should succeed."); + FileAssert.DoesNotExist (staleJavaFile); + + var deletedFile = task.DeletedJavaFiles.SingleOrDefault (); + Assert.IsNotNull (deletedFile); + Assert.AreEqual (staleJavaFile, deletedFile.ItemSpec); + Assert.AreEqual (Path.Combine ("stale", "Old.java"), deletedFile.GetMetadata ("RelativePath")); + } + [Test] public void Execute_GeneratesFrameworkJcws () { @@ -308,5 +404,39 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, item.SetMetadata ("HasMonoAndroidReference", "True"); return item; } + + static bool GeneratedAssembliesContainType (IEnumerable assemblies, string typeName) + { + foreach (var assemblyPath in assemblies.Select (a => a.ItemSpec)) { + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath); + if (assembly.Modules.SelectMany (m => m.Types).Any (t => t.Name == typeName)) { + return true; + } + } + return false; + } + + static Dictionary ReadGeneratedAssemblyBytes (IEnumerable assemblies) + { + return assemblies.ToDictionary ( + a => Path.GetFileName (a.ItemSpec), + a => File.ReadAllBytes (a.ItemSpec), + StringComparer.Ordinal); + } + + static byte [] GenerateRootTypeMapAssembly (Version systemRuntimeVersion) + { + using var stream = new MemoryStream (); + var generator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + generator.Generate (new [] { "_Mono.Android.TypeMap" }, useSharedTypemapUniverse: false, stream, maxArrayRank: 3); + return stream.ToArray (); + } + + static Guid ReadMvid (byte [] assemblyBytes) + { + using var stream = new MemoryStream (assemblyBytes); + using var assembly = AssemblyDefinition.ReadAssembly (stream); + return assembly.MainModule.Mvid; + } } } 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 0315b20bf34..0c2bd2d81d8 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 @@ -17,6 +17,52 @@ namespace Xamarin.Android.Build.Tests { [Category ("Node-2")] public class TrimmableTypeMapBuildTests : BaseTest { + [Test] + public void NativeAot_DefaultsToTrimmableTypeMap () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.RunTarget (proj, "_CreatePropertiesCache"), "Property cache target should have succeeded."); + + var buildProps = builder.Output.GetIntermediaryPath ("build.props"); + FileAssert.Exists (buildProps); + StringAssert.Contains ( + "_androidtypemapimplementation=trimmable", + File.ReadAllText (buildProps), + "NativeAOT should default to trimmable typemaps."); + } + + [Test] + public void NativeAot_NonTrimmableTypeMap_FailsValidation ([Values ("managed", "llvm-ir")] string typemapImplementation) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + + Assert.IsFalse (builder.RunTarget (proj, "_ValidateAndroidTypeMapImplementation"), + "NativeAOT with a non-trimmable typemap should fail validation."); + Assert.IsTrue ( + StringAssertEx.ContainsText (builder.LastBuildOutput, "NativeAOT requires _AndroidTypeMapImplementation=trimmable."), + $"{builder.BuildLogFile} should contain the NativeAOT trimmable typemap validation error."); + } + [Test] public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { @@ -67,6 +113,126 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease } } + [Test] + public void GenerateJavaStubsTarget_WithTrimmableTypeMap_DoesNotCleanIntermediateAndroidDirectory () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + + var androidDir = builder.Output.GetIntermediaryPath ("android"); + DirectoryAssert.Exists (androidDir, "First build should have populated the intermediate android directory."); + var sentinel = Path.Combine (androidDir, "no-clean-sentinel.txt"); + File.WriteAllText (sentinel, "do not clean"); + + var cleanStamp = builder.Output.GetIntermediaryPath (Path.Combine ("stamp", "_CleanIntermediateIfNeeded.stamp")); + if (File.Exists (cleanStamp)) { + File.Delete (cleanStamp); + } + + var typeMapStamp = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "_GenerateTrimmableTypeMap.stamp")); + var javaStubsStamp = builder.Output.GetIntermediaryPath (Path.Combine ("stamp", "_GenerateJavaStubs.stamp")); + FileAssert.Exists (typeMapStamp); + FileAssert.Exists (javaStubsStamp); + var stampTime = DateTime.UtcNow; + File.SetLastWriteTimeUtc (typeMapStamp, stampTime); + File.SetLastWriteTimeUtc (javaStubsStamp, stampTime.AddSeconds (-5)); + + Assert.IsTrue (builder.RunTarget (proj, "_GenerateJavaStubs", doNotCleanupOnUpdate: true), + "_GenerateJavaStubs target should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateJavaStubs"); + FileAssert.Exists (sentinel, "_GenerateJavaStubs should not run _CleanIntermediateIfNeeded when invoked outside a full Build/DeployToDevice graph."); + } + + [Test] + public void Build_WithTrimmableTypeMap_DeletesStaleGeneratedJavaSourcesAndCopies () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + + var staleRelativePath = Path.Combine ("crc64stale", "Old.java"); + var staleClassPath = Path.Combine ("crc64stale", "Old.class"); + var staleGeneratedJava = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "java", staleRelativePath)); + var staleCopiedJava = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", staleRelativePath)); + var staleCompiledClass = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes", staleClassPath)); + var staleGeneratedJavaDirectory = Path.GetDirectoryName (staleGeneratedJava); + var staleCopiedJavaDirectory = Path.GetDirectoryName (staleCopiedJava); + var staleCompiledClassDirectory = Path.GetDirectoryName (staleCompiledClass); + if (staleGeneratedJavaDirectory is null || staleCopiedJavaDirectory is null || staleCompiledClassDirectory is null) { + throw new InvalidOperationException ("Could not determine stale Java output directories."); + } + Directory.CreateDirectory (staleGeneratedJavaDirectory); + Directory.CreateDirectory (staleCopiedJavaDirectory); + Directory.CreateDirectory (staleCompiledClassDirectory); + File.WriteAllText (staleGeneratedJava, "package crc64stale; public class Old {}"); + File.WriteAllText (staleCopiedJava, "package crc64stale; public class Old {}"); + File.WriteAllBytes (staleCompiledClass, []); + + proj.MainActivity += Environment.NewLine + "// Force trimmable typemap regeneration."; + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Second build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); + builder.Output.AssertTargetIsNotSkipped ("_CompileJava"); + + FileAssert.DoesNotExist (staleGeneratedJava, "Regenerated trimmable typemap should delete stale Java sources."); + FileAssert.DoesNotExist (staleCopiedJava, "Regenerated trimmable typemap should delete stale android/src Java copies."); + FileAssert.DoesNotExist (staleCompiledClass, "Deleting stale copied Java sources should force Java recompilation and remove stale class outputs."); + } + + [Test] + public void Build_WithTrimmableTypeMap_CopiesUpdatedGeneratedJavaSources () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + + var generatedJavaDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "java")); + var generatedJavaFiles = Directory.GetFiles (generatedJavaDirectory, "*.java", SearchOption.AllDirectories); + Assert.IsNotEmpty (generatedJavaFiles, "Test setup should have generated trimmable typemap Java sources."); + + var generatedJava = generatedJavaFiles [0]; + var relativePath = Path.GetRelativePath (generatedJavaDirectory, generatedJava); + var copiedJava = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativePath)); + var typeMapStamp = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "_GenerateTrimmableTypeMap.stamp")); + var javaStubsStamp = builder.Output.GetIntermediaryPath (Path.Combine ("stamp", "_GenerateJavaStubs.stamp")); + FileAssert.Exists (copiedJava, "First build should have copied generated Java sources to android/src."); + FileAssert.Exists (typeMapStamp, "First build should have written the trimmable typemap output stamp."); + FileAssert.Exists (javaStubsStamp, "First build should have written the Java stubs output stamp."); + + var updatedJava = File.ReadAllText (generatedJava) + "\n// Force generated Java copy regression.\n"; + File.WriteAllText (generatedJava, updatedJava); + var stampTime = DateTime.UtcNow; + File.SetLastWriteTimeUtc (typeMapStamp, stampTime); + File.SetLastWriteTimeUtc (javaStubsStamp, stampTime.AddSeconds (-5)); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "Second build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateJavaStubs"); + builder.Output.AssertTargetIsNotSkipped ("_CompileJava"); + Assert.AreEqual (updatedJava, File.ReadAllText (copiedJava), "Updated generated Java sources should be copied to android/src even when typemap assemblies do not change."); + } + [Test] public void Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap () { @@ -316,11 +482,6 @@ public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_PackagesLinke var typeMapDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "typemap")); var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); var readyToRunAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "R2R")); - 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."); @@ -361,8 +522,6 @@ public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_PackagesLinke expectedHash.SequenceEqual (packagedHash), $"{apkPath} should package post-link typemap assembly {pair.Key} from {pair.Value}, not the generated pre-link copy."); } - - AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (dexFile, javaSourceDirectory, acwMapPath, proguardPrimaryPath); } [Test] @@ -650,35 +809,6 @@ ISet ReadPackagedManagedAssemblyNames (string apkPath, AndroidTargetArch .ToHashSet (StringComparer.Ordinal); } - void AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (string dexFile, string javaSourceDirectory, string acwMapPath, string proguardPrimaryPath) - { - const string deadManagedType = "Android.Animation.Animator+IAnimatorListenerImplementor"; - const string deadJavaName = "Lmono/android/animation/Animator_AnimatorListenerImplementor;"; - const string deadJavaDotName = "mono.android.animation.Animator_AnimatorListenerImplementor"; - - 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", "animation", "Animator_AnimatorListenerImplementor.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); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 7a7af12254a..3cbdc4d9663 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -329,8 +329,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot if (runtime != AndroidRuntime.NativeAOT) { dotnet.AssertHasNoWarnings (); } else { - // NativeAOT currently issues 1 warning - dotnet.AssertHasSomeWarnings (1); + dotnet.AssertHasNoWarnings (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc index 588ab6f17a5..99c24688174 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc @@ -11,31 +11,34 @@ "Size": 18224 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 92568 + "Size": 93856 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 119168 + "Size": 119984 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26872 + "Size": 26912 }, "lib/arm64-v8a/lib_System.Console.dll.so": { "Size": 24360 }, + "lib/arm64-v8a/lib_System.IO.Hashing.dll.so": { + "Size": 24280 + }, "lib/arm64-v8a/lib_System.Linq.dll.so": { "Size": 25552 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 704944 + "Size": 705536 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20224 + "Size": 20232 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { "Size": 19760 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { - "Size": 20096 + "Size": 19976 }, "lib/arm64-v8a/libarc.bin.so": { "Size": 19296 @@ -44,7 +47,7 @@ "Size": 36416 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1387064 + "Size": 1387008 }, "lib/arm64-v8a/libmonosgen-2.0.so": { "Size": 3111632 @@ -62,7 +65,7 @@ "Size": 162000 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 19680 + "Size": 19824 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -89,5 +92,5 @@ "Size": 1904 } }, - "PackageSize": 3320142 + "PackageSize": 3328422 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc index 79351723c31..ac323054b31 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc @@ -8,7 +8,7 @@ "Size": 22016 }, "lib/arm64-v8a/libUnnamedProject.so": { - "Size": 5999448 + "Size": 5207328 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -35,5 +35,5 @@ "Size": 1904 } }, - "PackageSize": 2474779 + "PackageSize": 2159387 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.MonoVM.apkdesc index f0d9684ace8..9ab91e039fd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.MonoVM.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.MonoVM.apkdesc @@ -35,13 +35,13 @@ "Size": 25360 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 101872 + "Size": 103352 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 545344 + "Size": 561072 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26864 + "Size": 26912 }, "lib/arm64-v8a/lib_mscorlib.dll.so": { "Size": 21400 @@ -94,6 +94,9 @@ "lib/arm64-v8a/lib_System.IO.Compression.dll.so": { "Size": 34680 }, + "lib/arm64-v8a/lib_System.IO.Hashing.dll.so": { + "Size": 24280 + }, "lib/arm64-v8a/lib_System.IO.IsolatedStorage.dll.so": { "Size": 28240 }, @@ -116,7 +119,7 @@ "Size": 26976 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 1003920 + "Size": 1002512 }, "lib/arm64-v8a/lib_System.Private.DataContractSerialization.dll.so": { "Size": 217808 @@ -131,7 +134,7 @@ "Size": 35472 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20352 + "Size": 20376 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { "Size": 19752 @@ -257,7 +260,7 @@ "Size": 168080 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 350464 + "Size": 350608 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -2435,5 +2438,5 @@ "Size": 794696 } }, - "PackageSize": 11032423 + "PackageSize": 11052991 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index ea3cf49c141..6d7b5a13940 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2932,6 +2932,8 @@ because xbuild doesn't support framework reference assemblies. BeforeTargets="_CheckForInvalidConfigurationAndPlatform"> + $(OutputPath)java_runtime_trimmable.dex $(IntermediateOutputPath)release-trimmable $(IntermediateOutputPath)release-trimmable.txt - java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\MonoPackageManager.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyObject.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyThrowable.java + java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\MonoPackageManager.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\ManagedPeer.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyObject.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyThrowable.java java-trimmable\net\dot\jni\internal\JavaProxyObject.java;java-trimmable\net\dot\jni\internal\JavaProxyThrowable.java <_RuntimeOutput Include="$(OutputPath)java_runtime_fastdev.jar"> diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index dc3e1a83644..e81a04b0cd6 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -89,8 +89,8 @@ public void DotNetRun (bool isRelease, string typemapImplementation, AndroidRunt Assert.Ignore ("dotnet run --no-build breaks marshal methods (both managed and llvm-ir) on MonoVM"); } - if (runtime == AndroidRuntime.NativeAOT && typemapImplementation == "llvm-ir") { - Assert.Ignore ("NativeAOT doesn't work with LLVM-IR typemaps"); + if (runtime == AndroidRuntime.NativeAOT && typemapImplementation != "trimmable") { + Assert.Ignore ("NativeAOT requires trimmable typemaps."); } var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 0a19aa073ac..2d6a16b6d28 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -975,7 +975,8 @@ public void Build_EmitArrayEntries_HonoursMaxArrayRank () Assert.Equal (5, model5.MaxArrayRank); var rank5Entries = model5.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (5, rank5Entries.Count); - Assert.Equal ("Foo.Bar[][][][][], App", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy5, TestTypeMap", rank5Entries.Single (e => e.AnchorRank == 5).ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy5, TestTypeMap", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); var model1 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 1); Assert.Equal (1, model1.MaxArrayRank); @@ -1008,29 +1009,61 @@ public void Build_EmitArrayEntries_KeyIsElementJniName () } [Fact] - public void Build_EmitArrayEntries_TrimTargetIsClosedArrayType () + public void Build_EmitArrayEntries_MapToGeneratedArrayProxy () { - // 3rd ctor arg = the closed array type itself, so ILC's per-shape conditional - // drops the entry when the array shape is never constructed. var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModelWithArrays (new [] { peer }); var rank1 = model.Entries.Single (e => e.AnchorRank == 1); - Assert.Equal ("Foo.Bar[], App", rank1.ProxyTypeReference); - Assert.Equal ("Foo.Bar[], App", rank1.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap", rank1.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap", rank1.TargetTypeReference); var rank2 = model.Entries.Single (e => e.AnchorRank == 2); - Assert.Equal ("Foo.Bar[][], App", rank2.ProxyTypeReference); - Assert.Equal ("Foo.Bar[][], App", rank2.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap", rank2.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap", rank2.TargetTypeReference); var rank3 = model.Entries.Single (e => e.AnchorRank == 3); - Assert.Equal ("Foo.Bar[][][], App", rank3.ProxyTypeReference); - Assert.Equal ("Foo.Bar[][][], App", rank3.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, TestTypeMap", rank3.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, TestTypeMap", rank3.TargetTypeReference); + + Assert.Equal (3, model.ArrayProxyTypes.Count); + Assert.Equal ("Foo_Bar_ArrayProxy1", model.ArrayProxyTypes [0].TypeName); + Assert.Equal ("Foo_Bar_ArrayProxy2", model.ArrayProxyTypes [1].TypeName); + Assert.Equal ("Foo_Bar_ArrayProxy3", model.ArrayProxyTypes [2].TypeName); + } + + [Fact] + public void Build_EmitArrayEntries_AssociationsMatchGetArrayTypes () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + var rank1Proxy = "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap"; + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop" && + a.AliasProxyTypeReference == rank1Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[Foo.Bar, App]], Java.Interop" && + a.AliasProxyTypeReference == rank1Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Foo.Bar[], App" && + a.AliasProxyTypeReference == rank1Proxy); + + var rank2Proxy = "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap"; + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop]], Java.Interop" && + a.AliasProxyTypeReference == rank2Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[Foo.Bar, App]][], Java.Interop" && + a.AliasProxyTypeReference == rank2Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Foo.Bar[][], App" && + a.AliasProxyTypeReference == rank2Proxy); } [Fact] public void Build_EmitArrayEntries_AllConditional () { // 2-arg unconditional makes no sense for arrays — the trim conditioning on the - // array shape is the whole point. + // generated array proxy is the whole point. var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModelWithArrays (new [] { peer }); @@ -1107,6 +1140,35 @@ public void Build_EmitArrayEntries_PrimitiveJniKeyword_Skipped (string jniKeywor Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); } + [Fact] + public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAssembly () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Java.Interop"); + var model = BuildModelWithArrays (new [] { peer }); + + var primitiveEntries = model.Entries + .Where (e => e.JniName.Length == 1 && e.AnchorRank is not null) + .ToList (); + Assert.Equal (24, primitiveEntries.Count); // 8 primitive keywords × 3 ranks + + var sbyteRank1 = primitiveEntries.Single (e => e.JniName == "B" && e.AnchorRank == 1); + Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy1, TestTypeMap", sbyteRank1.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy1, TestTypeMap", sbyteRank1.TargetTypeReference); + Assert.False (sbyteRank1.IsUnconditional); + + var sbyteRank2 = primitiveEntries.Single (e => e.JniName == "B" && e.AnchorRank == 2); + Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy2, TestTypeMap", sbyteRank2.TargetTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[System.SByte, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaPrimitiveArray`1[[System.SByte, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaSByteArray, Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + } + [Fact] public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () { @@ -1197,10 +1259,24 @@ public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () EmitAndVerify (model, "ArrBlobs", (pe, reader) => { var attrs = ReadAllTypeMapAttributeBlobs (reader); - // Three array entries should round-trip with the same JNI key + array trim targets. - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[], App"); - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][], App"); - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][][], App"); + // Three array entries should round-trip with the same JNI key + generated array proxy refs. + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, ArrBlobs"); + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, ArrBlobs"); + + var assocAttrs = ReadAllTypeMapAssociationAttributeBlobs (reader); + Assert.Contains (assocAttrs, a => + a.sourceRef == "Java.Interop.JavaArray`1[[Foo.Bar, App]], Java.Interop" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); + Assert.Contains (assocAttrs, a => + a.sourceRef == "Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); }); } } @@ -1279,6 +1355,35 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio return result; } + static List<(string? sourceRef, string? proxyRef)> ReadAllTypeMapAssociationAttributeBlobs (MetadataReader reader) + { + var result = new List<(string?, string?)> (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + if (attr.Constructor.Kind != HandleKind.MemberReference) + continue; + + var ctor = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor); + if (ctor.Parent.Kind != HandleKind.TypeSpecification) + continue; + + var parent = reader.GetTypeSpecification ((TypeSpecificationHandle) ctor.Parent); + var parentName = parent.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + if (!parentName.StartsWith ("System.Runtime.InteropServices.TypeMapAssociationAttribute`1", StringComparison.Ordinal)) { + continue; + } + + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + if (prolog != 1) + continue; + + result.Add ((blobReader.ReadSerializedString (), blobReader.ReadSerializedString ())); + } + return result; + } + public class UcoMethods { [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 00c8be4e5bb..c2a087a68c9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -60,6 +60,38 @@ public void Scan_MarksFrameworkAssemblyPeers () Assert.All (peers, p => Assert.False (p.GenerateArrayEntries, $"{p.ManagedTypeName} should not emit array entries unless referenced from a non-framework assembly.")); } + [Fact] + public void Scan_JniAddNativeMethodRegistrationAttribute_ReportsXA4251 () + { + var errors = new List (); + var logger = new RecordingLogger (errors); + + using var scanner = new JavaPeerScanner (logger: logger); + using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + _ = scanner.Scan (new List<(string, PEReader)> { (assemblyName, peReader) }); + + Assert.Contains (errors, e => e.Contains ("HandWrittenNativeRegistrationPeer")); + Assert.Contains (errors, e => e.Contains ("NonPeerNativeRegistration")); + } + + sealed class RecordingLogger (List errors) : ITrimmableTypeMapLogger + { + public void LogNoJavaPeerTypesFound () { } + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } + public void LogDeferredRegistrationTypesInfo (int typeCount) { } + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } + public void LogGeneratedJcwFilesInfo (int sourceCount) { } + public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } + public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => + errors.Add ($"XA4251: {managedTypeName}"); + } + [Fact] public void Scan_TypeMetadata_IsCorrect () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index bfd4175bab4..0da6502bce5 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -171,6 +171,25 @@ public void NonGenericCollection_CopyTo_ObjectArray_PreservesNullElement () } } + [Test] + public void NativeObjectArray_GetArray_UsesRankOneTypeMapEntry () + { + AssumeTrimmableTypeMapEnabled (); + + using var first = new View (Android.App.Application.Context); + using var second = new View (Android.App.Application.Context); + var handle = JNIEnv.NewArray (new [] { first, second }); + + try { + var values = JNIEnv.GetArray (handle); + Assert.AreEqual (2, values.Length); + Assert.IsTrue (JNIEnv.IsSameObject (first.Handle, values [0].Handle)); + Assert.IsTrue (JNIEnv.IsSameObject (second.Handle, values [1].Handle)); + } finally { + JNIEnv.DeleteLocalRef (handle); + } + } + [Test] public void NonGenericCollection_CopyTo_StringArray_ConvertsJavaString () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 3ccae0da3ad..e6dbd323ca2 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -179,22 +179,6 @@ public void RegisteredPeer_CanCreateGenericHolder () Assert.AreEqual (42, holder.Value); } - [Test] - public void JavaProxyObject_ValueMarshalerUsesProxyType () - { - AssumeTrimmableTypeMapEnabled (); - - var value = new object (); - var marshaler = JniEnvironment.Runtime.ValueManager.GetValueMarshaler (typeof (object)); - var state = marshaler.CreateObjectReferenceArgumentState (value); - - try { - Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JNIEnv.GetClassNameFromInstance (state.ReferenceValue.Handle)); - } finally { - marshaler.DestroyArgumentState (value, ref state); - } - } - [Test] public void JavaProxyObject_CanBeUsedInObjectArray () { @@ -207,66 +191,83 @@ public void JavaProxyObject_CanBeUsedInObjectArray () } [Test] + [Category ("TrimmableTypeMapUnsupported")] // TODO: https://github.com/dotnet/android/issues/11703 public void JavaProxyObject_ObjectMethodsUseJavaIdentitySemantics () { AssumeTrimmableTypeMapEnabled (); var value = new object (); var other = new object (); - var marshaler = JniEnvironment.Runtime.ValueManager.GetValueMarshaler (typeof (object)); - var state = marshaler.CreateObjectReferenceArgumentState (value); - var otherState = marshaler.CreateObjectReferenceArgumentState (other); + using var values = new JavaObjectArray (2); + values [0] = value; + values [1] = other; + var localProxy = JniEnvironment.Arrays.GetObjectArrayElement (values.PeerReference, 0); + var localOtherProxy = JniEnvironment.Arrays.GetObjectArrayElement (values.PeerReference, 1); try { - var localProxy = state.ReferenceValue.NewLocalRef (); - var localOtherProxy = otherState.ReferenceValue.NewLocalRef (); - + IntPtr proxyClass = JNIEnv.GetObjectClass (localProxy.Handle); try { - IntPtr proxyClass = JNIEnv.GetObjectClass (localProxy.Handle); + IntPtr equals = JNIEnv.GetMethodID (proxyClass, "equals", "(Ljava/lang/Object;)Z"); + IntPtr hashCode = JNIEnv.GetMethodID (proxyClass, "hashCode", "()I"); + IntPtr toString = JNIEnv.GetMethodID (proxyClass, "toString", "()Ljava/lang/String;"); + var systemClass = JniEnvironment.Types.FindClass ("java/lang/System"); + try { - IntPtr equals = JNIEnv.GetMethodID (proxyClass, "equals", "(Ljava/lang/Object;)Z"); - IntPtr hashCode = JNIEnv.GetMethodID (proxyClass, "hashCode", "()I"); - IntPtr toString = JNIEnv.GetMethodID (proxyClass, "toString", "()Ljava/lang/String;"); - var systemClass = JniEnvironment.Types.FindClass ("java/lang/System"); - - try { - IntPtr identityHashCode = JNIEnv.GetStaticMethodID (systemClass.Handle, "identityHashCode", "(Ljava/lang/Object;)I"); - - Assert.IsTrue (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localProxy.Handle))); - Assert.IsFalse (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localOtherProxy.Handle))); - Assert.AreEqual ( - JNIEnv.CallStaticIntMethod (systemClass.Handle, identityHashCode, new JValue (localProxy.Handle)), - JNIEnv.CallIntMethod (localProxy.Handle, hashCode)); - var proxyString = JNIEnv.GetString (JNIEnv.CallObjectMethod (localProxy.Handle, toString), JniHandleOwnership.TransferLocalRef); - Assert.IsTrue ( - proxyString.StartsWith ("net.dot.jni.internal.JavaProxyObject@", StringComparison.Ordinal), - proxyString); - } finally { - JniObjectReference.Dispose (ref systemClass); - } + IntPtr identityHashCode = JNIEnv.GetStaticMethodID (systemClass.Handle, "identityHashCode", "(Ljava/lang/Object;)I"); + + Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JNIEnv.GetClassNameFromInstance (localProxy.Handle)); + Assert.IsTrue (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localProxy.Handle))); + Assert.IsFalse (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localOtherProxy.Handle))); + Assert.AreEqual ( + JNIEnv.CallStaticIntMethod (systemClass.Handle, identityHashCode, new JValue (localProxy.Handle)), + JNIEnv.CallIntMethod (localProxy.Handle, hashCode)); + var proxyString = JNIEnv.GetString (JNIEnv.CallObjectMethod (localProxy.Handle, toString), JniHandleOwnership.TransferLocalRef); + Assert.IsTrue ( + proxyString.StartsWith ("net.dot.jni.internal.JavaProxyObject@", StringComparison.Ordinal), + proxyString); } finally { - JNIEnv.DeleteLocalRef (proxyClass); + JniObjectReference.Dispose (ref systemClass); } } finally { - JniObjectReference.Dispose (ref localProxy); - JniObjectReference.Dispose (ref localOtherProxy); + JNIEnv.DeleteLocalRef (proxyClass); } } finally { - marshaler.DestroyArgumentState (other, ref otherState); - marshaler.DestroyArgumentState (value, ref state); + JniObjectReference.Dispose (ref localProxy); + JniObjectReference.Dispose (ref localOtherProxy); } } [Test] - public void TryGetArrayType_PrimitiveLeaf_DoesNotRequireRankMapEntry () + public void TryGetArrayProxy_ObjectLeaf_ReturnsAllRankTypes () + { + AssumeTrimmableTypeMapEnabled (); + + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (Java.Lang.Object), additionalRank: 1, out var objectArrayProxy)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Lang.Object[])); + + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (Java.Lang.Object), additionalRank: 2, out var jaggedObjectArrayProxy)); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray>)); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray[])); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (Java.Lang.Object[][])); + } + + [Test] + public void TryGetArrayProxy_PrimitiveLeaf_ReturnsAllRankTypes () { AssumeTrimmableTypeMapEnabled (); - Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayType (typeof (byte), out var byteArrayType)); - Assert.AreEqual (typeof (byte[]), byteArrayType); + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (sbyte), additionalRank: 1, out var byteArrayProxy)); + CollectionAssert.Contains (byteArrayProxy.GetArrayTypes (), typeof (sbyte[])); + CollectionAssert.Contains (byteArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray)); + CollectionAssert.Contains (byteArrayProxy.GetArrayTypes (), typeof (JavaPrimitiveArray)); + CollectionAssert.Contains (byteArrayProxy.GetArrayTypes (), typeof (JavaSByteArray)); - Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayType (typeof (byte[]), out var jaggedByteArrayType)); - Assert.AreEqual (typeof (byte[][]), jaggedByteArrayType); + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (sbyte), additionalRank: 2, out var jaggedByteArrayProxy)); + CollectionAssert.Contains (jaggedByteArrayProxy.GetArrayTypes (), typeof (sbyte[][])); + CollectionAssert.Contains (jaggedByteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray>)); + CollectionAssert.Contains (jaggedByteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); } static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index da97716db5d..2f76e15dca3 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -30,6 +30,7 @@ protected override IEnumerable? ExcludedCategories { if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { categories.Add ("NativeTypeMap"); categories.Add ("Export"); + categories.Add ("TrimmableTypeMapUnsupported"); } // Build-time flags flow in via runtimeconfig.json properties @@ -66,35 +67,13 @@ protected override IEnumerable? IncludedCategories { var value = AppContext.GetData ("IncludeCategories") as string; if (string.IsNullOrEmpty (value)) return null; - return value!.Split (new [] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + return value.Split (new [] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); } } static bool HasAppContextSwitch (string key) => AppContext.TryGetSwitch (key, out var value) && value; - protected override IEnumerable? ExcludedTestNames { - get { - if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) - return null; - - // Tests from the external Java.Interop-Tests assembly that fail under the - // trimmable typemap. These cannot use [Category] because we don't control - // that assembly — they must be excluded by name here. - return new [] { - // Known limitation: [JniAddNativeMethodRegistrationAttribute] is not - // supported by design under the trimmable typemap. This Java.Interop-Tests - // fixture uses that attribute to register native callbacks on a hand-written - // Java peer (an obsolete code path whose primary consumer, jnimarshalmethod-gen, - // was removed in dotnet/java-interop#1405). The trimmable typemap generator - // emits XA4251 when it encounters the attribute and instructs users to either - // avoid it or switch off the trimmable typemap. - // See https://github.com/dotnet/android/issues/11170. - "Java.InteropTests.InvokeVirtualFromConstructorTests", - }; - } - } - public override void OnCreate (Bundle? arguments) { Java.Lang.JavaSystem.LoadLibrary ("reuse-threads"); diff --git a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt index 0feba3a1457..e6771e2a22e 100644 --- a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt +++ b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt @@ -1,5 +1,4 @@ Compat issues with assembly Mono.Android: -CannotChangeAttribute : Attribute 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute' on parameter 'targetType' on member 'Android.Graphics.ColorValueMarshaler.CreateGenericValue(Java.Interop.JniObjectReference, Java.Interop.JniObjectReferenceOptions, System.Type)' changed from '[DynamicallyAccessedMembersAttribute(8199)]' in the contract to '[DynamicallyAccessedMembersAttribute(7)]' in the implementation. CannotChangeAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' on 'Android.Accounts.AccountManager.RemoveAccount(Android.Accounts.Account, Android.App.Activity, Android.Accounts.IAccountManagerCallback, Android.OS.Handler)' changed from '[RequiresPermissionAttribute("android.permission.MANAGE_ACCOUNTS")]' in the contract to '[RequiresPermissionAttribute("android.permission.REMOVE_ACCOUNTS")]' in the implementation. CannotRemoveAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' exists on 'Android.Bluetooth.BluetoothDevice.CreateInsecureL2capChannel(System.Int32)' in the contract but not the implementation. CannotRemoveAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' exists on 'Android.Bluetooth.BluetoothDevice.CreateInsecureRfcommSocketToServiceRecord(Java.Util.UUID)' in the contract but not the implementation. @@ -48,3 +47,4 @@ MembersMustExist : Member 'public void Android.Telecom.CallControl.RequestVideoS TypesMustExist : Type 'Android.Text.IInputType' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Xamarin.Android.Net.AndroidClientHandler' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Android.Views.InputMethods.EditorInfo' does not implement interface 'Android.Text.IInputType' in the implementation but it does in the contract. +CannotRemoveAttribute : Attribute 'System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute' exists on 'Java.Interop.ExportAttribute' in the contract but not the implementation. \ No newline at end of file diff --git a/tests/api-compatibility/api-compat-exclude-attributes.txt b/tests/api-compatibility/api-compat-exclude-attributes.txt index bb1d43bbe88..0ca120ff8dd 100644 --- a/tests/api-compatibility/api-compat-exclude-attributes.txt +++ b/tests/api-compatibility/api-compat-exclude-attributes.txt @@ -16,3 +16,4 @@ T:System.Runtime.CompilerServices.IteratorStateMachineAttribute T:System.Runtime.CompilerServices.NullableAttribute T:System.Runtime.CompilerServices.NullableContextAttribute T:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute +T:System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute