From d3cfd1029c43b3100c25350e28a9e6dfc1332e88 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 10:43:43 +0200 Subject: [PATCH 1/4] [TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When switching MonoAndroidTypeMapFlavor between legacy and trimmable without a clean build, stale legacy JCWs with Runtime.register(typeName, klass, methods) calls persisted in the android/src/ intermediate directory alongside trimmable JCWs that only use Runtime.registerNatives(). Both got compiled into the APK, causing the legacy registration path to execute at runtime — a path incompatible with trimming. Root cause: the trimmable JCW generator wrote to typemap/java/ and then copied to android/src/, but never cleaned android/src/ first. Legacy JCWs with different package names survived the copy overlay. Three fixes: 1. Write trimmable JCWs directly to _AndroidIntermediateJavaSourceDirectory (android/src/) instead of a separate typemap/java/ directory, eliminating the copy step entirely. This is the same directory that _FindJavaStubFiles globs from, so no files can be missed or stale. 2. Add MonoAndroidTypeMapFlavor to the build properties cache so switching between legacy and trimmable triggers _CleanIntermediateIfNeeded, removing all stale artifacts from the previous flavor. 3. Always wire registerJniNativesFn so that if a stale JCW somehow calls Runtime.register(), TrimmableTypeMapTypeManager.RegisterNativeMembers throws UnreachableException with a clear diagnostic instead of the C++ side silently dropping the call. Also adds a unit test verifying trimmable JCWs never emit Runtime.register(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 4 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 12 +----- .../Xamarin.Android.Build.Tests/BuildTest.cs | 43 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 1 + .../Generator/JcwJavaSourceGeneratorTests.cs | 10 +++++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 097c8e5d0ab..dc4fb8f5823 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -178,9 +178,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap) { - args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; - } + args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; RunStartupHooksIfNeeded (); SetSynchronizationContext (); } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index d38aad44fb9..d83ac767f5d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -14,7 +14,6 @@ <_TypeMapBaseOutputDir>$(IntermediateOutputPath) <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ - <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java @@ -55,7 +54,7 @@ - - <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - - - - - - - <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) 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 a8a90ae9ff3..e7b92eb1930 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 @@ -845,6 +845,49 @@ public void BuildAfterUpgradingNuget ([Values] AndroidRuntime runtime) } } + [Test] + [NonParallelizable] + public void SwitchingTypeMapImplementationTriggersClean () + { + var proj = new XamarinAndroidApplicationProject (); + + using (var b = CreateApkBuilder ()) { + b.CleanupAfterSuccessfulBuild = b.CleanupOnDispose = false; + b.Verbosity = LoggerVerbosity.Detailed; + + var projectDir = Path.Combine (Root, b.ProjectDirectory); + if (Directory.Exists (projectDir)) + Directory.Delete (projectDir, true); + + // First build with default (llvm-ir) typemap + Assert.IsTrue (b.Build (proj), "first build should have succeeded."); + string build_props = b.Output.GetIntermediaryPath ("build.props"); + FileAssert.Exists (build_props, "build.props should exist after first build."); + var firstBuildProps = File.ReadAllText (build_props); + Assert.IsTrue (firstBuildProps.Contains ("_androidtypemapimplementation="), + "build.props should contain _AndroidTypeMapImplementation."); + + // Second build with trimmable typemap — should trigger clean + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + b.Save (proj, doNotCleanupOnUpdate: true); + Assert.IsTrue (b.Build (proj), "second build (trimmable) should have succeeded."); + Assert.IsFalse (b.Output.IsTargetSkipped ("_CleanIntermediateIfNeeded"), + "Switching _AndroidTypeMapImplementation should trigger _CleanIntermediateIfNeeded."); + + // Third build switching back to llvm-ir — should trigger clean again + proj.SetProperty ("_AndroidTypeMapImplementation", "llvm-ir"); + b.Save (proj, doNotCleanupOnUpdate: true); + Assert.IsTrue (b.Build (proj), "third build (llvm-ir) should have succeeded."); + Assert.IsFalse (b.Output.IsTargetSkipped ("_CleanIntermediateIfNeeded"), + "Switching _AndroidTypeMapImplementation back should trigger _CleanIntermediateIfNeeded."); + + // Fourth build with no changes — should NOT trigger clean + Assert.IsTrue (b.Build (proj), "fourth build (no changes) should have succeeded."); + Assert.IsTrue (b.Output.IsTargetSkipped ("_CleanIntermediateIfNeeded"), + "A build with no implementation change should skip _CleanIntermediateIfNeeded."); + } + } + [Test] [Category ("SmokeTests")] public void BuildInDesignTimeMode ( diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index c2527d1954e..1a628b82b55 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -981,6 +981,7 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidManifestPlaceholders=$(AndroidManifestPlaceholders)" /> <_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" /> <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> + <_PropertyCacheItems Include="_AndroidTypeMapImplementation=$(_AndroidTypeMapImplementation)" /> Date: Thu, 9 Apr 2026 17:44:15 +0200 Subject: [PATCH 2/4] Add registerNatives assertion to ACW test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGeneratorTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 6f075847f8d..8016f0f6b5d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,9 +138,10 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_AcwType_NeverCallsRuntimeRegister () { var java = GenerateFixture ("my/app/MainActivity"); - // Trimmable JCWs must only call Runtime.registerNatives(klass). - // Runtime.register(typeName, klass, methods) is the legacy path that - // relies on reflection-based callback registration — incompatible with trimming. + // Trimmable JCWs must call Runtime.registerNatives(klass) and avoid + // the legacy Runtime.register(typeName, klass, methods) path, which + // relies on reflection-based callback registration and breaks trimming. + AssertContainsLine ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); Assert.DoesNotContain ("Runtime.register (\"", java); } From 736d7f7ab2996f9a356c2eacf52b1c9dcf59905c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 18:19:54 +0200 Subject: [PATCH 3/4] Pin typemap switching test to CoreCLR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/BuildTest.cs | 1 + 1 file changed, 1 insertion(+) 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 e7b92eb1930..a67d06dd2c8 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 @@ -850,6 +850,7 @@ public void BuildAfterUpgradingNuget ([Values] AndroidRuntime runtime) public void SwitchingTypeMapImplementationTriggersClean () { var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); using (var b = CreateApkBuilder ()) { b.CleanupAfterSuccessfulBuild = b.CleanupOnDispose = false; From 3e87fc2e313427f726f76bc430dde4170a9846eb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 14:07:41 +0200 Subject: [PATCH 4/4] [TrimmableTypeMap] Update stale _GenerateJavaStubs comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index d83ac767f5d..1845b9198ad 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -90,7 +90,7 @@