diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 6ccfa15d29f..468df94741c 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -220,8 +220,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/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 5891149578f..f441b473c96 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) 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 @@ + $(_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 @@ -86,7 +77,7 @@ Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '@(ReferencePath->Count())' != '0' and '$(_OuterIntermediateOutputPath)' == '' " AfterTargets="CoreCompile" Inputs="@(ReferencePath);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)" - Outputs="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapAssembliesListFile)"> + Outputs="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapAssembliesListFile);$(_TrimmableTypeMapOutputStamp)"> <_TypeMapInputAssemblies Include="@(ReferencePath)" /> @@ -125,13 +116,25 @@ ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"> + + <_DeletedCopiedJavaFiles Remove="@(_DeletedCopiedJavaFiles)" /> + <_DeletedCopiedJavaFiles Include="@(_DeletedJavaFiles->'$(IntermediateOutputPath)android/src/%(RelativePath)')" /> + + + + + + + + + @@ -184,6 +187,7 @@ + @@ -249,21 +253,21 @@ so this target only handles JCW file copying, manifest, assembly store setup, and native config. We keep the name _GenerateJavaStubs because BuildOrder.targets references it. - Inputs uses the TypeMap DLL as a focused sentinel — _GenerateTrimmableTypeMap regenerates - the DLL whenever any of its own inputs (assemblies, manifest, etc.) change, so the DLL - timestamp is a reliable proxy for "something changed that requires re-copying". + Inputs uses the TypeMap output stamp as a focused sentinel — _GenerateTrimmableTypeMap + touches it whenever any of its own inputs (assemblies, manifest, etc.) change. The + Copy task below then updates android/src only for Java sources whose content changed. We keep _GetGenerateJavaStubsInputs in DependsOnTargets so that downstream targets (_GetGeneratePackageManagerJavaInputs) can still read @(_GenerateJavaStubsInputs). --> - <_TypeMapJavaFiles Include="$(_TypeMapJavaStubsSourceDirectory)/**/*.java" /> + <_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 601b39347c6..34d3c923a5a 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1569,24 +1569,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 e7b8c06d6a8..ad3fc72a257 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1148,17 +1148,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 e78b7a7eaa2..2c4ecc0b330 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 @@ -1882,20 +1882,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 c3ccb71e5a7..dc5e38d7c74 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 @@ -460,28 +460,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"); @@ -495,22 +474,13 @@ static IEnumerable Get_BuildHasTrimmerWarningsData () foreach (AndroidRuntime runtime in Enum.GetValues (typeof (AndroidRuntime))) { 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; @@ -549,9 +519,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)) { @@ -569,8 +541,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..ddcce7164b8 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 @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Mono.Cecil; using NUnit.Framework; using Xamarin.Android.Tasks; using Xamarin.ProjectTools; @@ -110,6 +111,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 +168,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 +363,16 @@ 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; + } } } 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..e8d8d19d16e 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 @@ -67,6 +67,88 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease } } + [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 +398,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 +438,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 +725,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.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 8ed27d03e2b..313555e62ea 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": 103344 + "Size": 103352 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 559248 + "Size": 561072 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26856 + "Size": 26912 }, "lib/arm64-v8a/lib_mscorlib.dll.so": { "Size": 21408 @@ -94,6 +94,9 @@ "lib/arm64-v8a/lib_System.IO.Compression.dll.so": { "Size": 34616 }, + "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": 26984 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 1002392 + "Size": 1002512 }, "lib/arm64-v8a/lib_System.Private.DataContractSerialization.dll.so": { "Size": 217816 @@ -131,7 +134,7 @@ "Size": 35480 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20360 + "Size": 20376 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { "Size": 19760 @@ -161,7 +164,7 @@ "Size": 19248 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { - "Size": 22032 + "Size": 22048 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.Activity.dll.so": { "Size": 34896 @@ -257,7 +260,7 @@ "Size": 162000 }, "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": 11044711 + "PackageSize": 11052991 } \ No newline at end of file diff --git a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs index 888636f60f0..c7db9433b8d 100644 --- a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs +++ b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs @@ -21,5 +21,3 @@ public ExportFieldAttribute (string name) public string Name {get; set;} } } - - 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/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 3ccae0da3ad..22ab8df548b 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,53 +191,49 @@ 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); } } 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");