From 6e3bdb5eeb619b281041fa4b22b27137adf12657 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Jun 2026 17:34:08 +0200 Subject: [PATCH 1/5] [Mono.Android] Compile RuntimeFeature into a single assembly `Microsoft.Android.Runtime.RuntimeFeature` was compiled into both `Mono.Android.Runtime.dll` and `Mono.Android.dll`. Because `Mono.Android` references `Mono.Android.Runtime` (which exposes its internals to `Mono.Android` via `InternalsVisibleTo`), building `Mono.Android.dll` emitted 58 `CS0436` warnings: warning CS0436: The type 'RuntimeFeature' in 'src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs' conflicts with the imported type 'RuntimeFeature' in 'Mono.Android.Runtime'. Compile `RuntimeFeature.cs` only into `Mono.Android.Runtime.dll` (the lower-level assembly that already owns it) and let the other assemblies use it through `InternalsVisibleTo`: * Remove the duplicate `` from `Mono.Android.csproj`. * Grant `Mono.Android.Runtime` internals to `Microsoft.Android.Runtime.NativeAOT` and `Mono.Android.NET-Tests`, which previously accessed `RuntimeFeature` through `Mono.Android.dll`. * Once `RuntimeFeature` is only imported (no longer source-compiled) into `Mono.Android`, the unqualified name `RuntimeFeature` becomes ambiguous with `System.Runtime.CompilerServices.RuntimeFeature`. Add `using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature;` to the three affected files, matching the alias already used in `JNIEnvInit.cs`. `Microsoft.Android.Runtime.RuntimeFeature` is now defined in exactly one assembly (`Mono.Android.Runtime.dll`), and the 58 `CS0436` warnings are gone. This is a focused subset of #11625 that addresses only the `RuntimeFeature` duplication, without that PR's class rename or unrelated warning cleanups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in | 2 ++ src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 1 + src/Mono.Android/Android.Runtime/JNIEnv.cs | 1 + src/Mono.Android/Java.Interop/TypeManager.cs | 1 + src/Mono.Android/Mono.Android.csproj | 1 - 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in b/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in index fee83c88b9c..5f28384bdc6 100644 --- a/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in +++ b/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in @@ -19,3 +19,5 @@ using System.Runtime.Versioning; [assembly: SupportedOSPlatform("Android@MIN_API_LEVEL@.0")] [assembly: InternalsVisibleTo("Mono.Android, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo("Microsoft.Android.Runtime.NativeAOT, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo("Mono.Android.NET-Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 918b8377b54..8bc52554abd 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -11,6 +11,7 @@ using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; using System.Diagnostics.CodeAnalysis; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; #if JAVA_INTEROP namespace Android.Runtime { diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index ebfc81cc39b..471296b6ac6 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -14,6 +14,7 @@ using Java.Interop; using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; namespace Android.Runtime { public static partial class JNIEnv { diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index cc0b22936bd..1339d295633 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -9,6 +9,7 @@ using Android.Runtime; using Microsoft.Android.Runtime; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; namespace Java.Interop { diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 611ef7edf07..1918821f5c7 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -366,7 +366,6 @@ - From 7824101428647bfc6a49ff186f631a31c861b565 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:14:53 +0000 Subject: [PATCH 2/5] [Xamarin.Android.Build.Tests] Improve trimmer warning assertions and adjust warning totals Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) 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 05a317d6669..d79d459cb00 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 @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Text; using System.Threading.Tasks; using System.Xml; @@ -501,9 +502,9 @@ static IEnumerable Get_BuildHasTrimmerWarningsData () } else { AddTestData (runtime, "", new string [0], true); } - AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, 2); + AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 1); AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, false, 1); - AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, 2); + AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 1); AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, false); if (runtime == AndroidRuntime.NativeAOT) { @@ -570,9 +571,30 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, b.AssertHasNoWarnings (); } else { totalWarnings ??= codes.Length; - Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, $"{totalWarnings} Warning(s)"), $"Should receive {totalWarnings} warnings"); + + string warningSummaryLine = b.LastBuildOutput.LastOrDefault (line => + line.Contains (" Warning(s)", StringComparison.Ordinal) && + line.Contains (" Error(s)", StringComparison.Ordinal) + ) ?? ""; + Match totalWarningMatch = Regex.Match (warningSummaryLine, @"\s(?\d+)\sWarning\(s\)", RegexOptions.CultureInvariant); + var actualWarnings = totalWarningMatch.Success ? int.Parse (totalWarningMatch.Groups ["count"].Value) : -1; + + var allWarningLines = b.LastBuildOutput + .Where (line => line.Contains (": warning ", StringComparison.OrdinalIgnoreCase)) + .Take (25) + .ToArray (); + Assert.AreEqual ( + totalWarnings.Value, + actualWarnings, + $"{b.BuildLogFile} should have {totalWarnings} warnings. Summary line: '{warningSummaryLine}'. " + + $"Warnings found ({allWarningLines.Length} shown):{Environment.NewLine}{string.Join (Environment.NewLine, allWarningLines)}" + ); foreach (var code in codes) { - Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, code), $"Should receive {code} warning"); + Assert.True ( + StringAssertEx.ContainsText (b.LastBuildOutput, code), + $"{b.BuildLogFile} should contain warning {code}. Summary line: '{warningSummaryLine}'. " + + $"Warnings found ({allWarningLines.Length} shown):{Environment.NewLine}{string.Join (Environment.NewLine, allWarningLines)}" + ); } } } From f54cbc808f3f1471e7c41529d4c92f619133f349 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:22:06 +0000 Subject: [PATCH 3/5] [Xamarin.Android.Build.Tests] Refine warning summary parsing in BuildHasTrimmerWarnings Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 d79d459cb00..58b13f5f292 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 @@ -5,7 +5,6 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Text; using System.Threading.Tasks; using System.Xml; @@ -532,6 +531,8 @@ void AddTestData (AndroidRuntime runtime, string properties, string [] codes, bo [TestCaseSource (nameof (Get_BuildHasTrimmerWarningsData))] public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, string [] codes, bool isRelease, int? totalWarnings = null) { + const int maxWarningLinesToShow = 25; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } @@ -572,16 +573,12 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, } else { totalWarnings ??= codes.Length; - string warningSummaryLine = b.LastBuildOutput.LastOrDefault (line => - line.Contains (" Warning(s)", StringComparison.Ordinal) && - line.Contains (" Error(s)", StringComparison.Ordinal) - ) ?? ""; - Match totalWarningMatch = Regex.Match (warningSummaryLine, @"\s(?\d+)\sWarning\(s\)", RegexOptions.CultureInvariant); - var actualWarnings = totalWarningMatch.Success ? int.Parse (totalWarningMatch.Groups ["count"].Value) : -1; + string warningSummaryLine = b.LastBuildOutput.LastOrDefault (line => line.Contains ("Warning(s)", StringComparison.Ordinal)) ?? ""; + var actualWarnings = GetWarningCount (warningSummaryLine); var allWarningLines = b.LastBuildOutput .Where (line => line.Contains (": warning ", StringComparison.OrdinalIgnoreCase)) - .Take (25) + .Take (maxWarningLinesToShow) .ToArray (); Assert.AreEqual ( totalWarnings.Value, @@ -597,6 +594,17 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, ); } } + + static int GetWarningCount (string warningSummaryLine) + { + string [] tokens = warningSummaryLine.Split (new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 1; i < tokens.Length; i++) { + if (tokens [i] == "Warning(s)" && int.TryParse (tokens [i - 1], out var warningCount)) { + return warningCount; + } + } + return -1; + } } [Test] From 83781e4c675daa88ef7ed0f1bfc206b42ff9ab7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:06:02 +0000 Subject: [PATCH 4/5] [Xamarin.Android.Build.Tests] Align trimmer warning totals with current CI output Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/BuildTest2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 58b13f5f292..1c7b01625c6 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 @@ -501,15 +501,15 @@ static IEnumerable Get_BuildHasTrimmerWarningsData () } else { AddTestData (runtime, "", new string [0], true); } - AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 1); + AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 3); AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, false, 1); - AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 1); + AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 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, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 4); } } From b6b41847e7fc40dea2c00ae6936898782e260fce Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Jul 2026 23:15:30 +0200 Subject: [PATCH 5/5] [Mono.Android] Fix NativeAOT link error for xamarin_app_init Moving RuntimeFeature into Mono.Android.Runtime made the IsNativeAotRuntime guard in JNIEnvInit.Initialize a cross-assembly feature switch, so ILLink no longer eliminates Initialize under NativeAOT. Initialize is force-preserved by the ILLink descriptor and contains a [LibraryImport] to xamarin_app_init, which becomes a direct native reference (xa-internal-api is a DirectPInvoke). That symbol is only defined by the MonoVM/CoreCLR marshal methods native code, which is never generated for NativeAOT, so the link failed with XA3007 "undefined symbol: xamarin_app_init". Emit a no-op xamarin_app_init definition into the NativeAOT jni-init assembler source so the linker can resolve the reference. Initialize is never invoked under NativeAOT (InitializeNativeAotRuntime is used instead), so the no-op is safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/LlvmIrGeneratorTests.cs | 28 +++++++++++++++++++ ...NativeAotJniInitNativeAssemblyGenerator.cs | 22 +++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs index 7013ebc9367..1e06b5f52c7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.Build.Utilities; using NUnit.Framework; +using Xamarin.Android.Build.Tasks; using Xamarin.Android.Tasks.LLVMIR; using Xamarin.Android.Tools; @@ -79,5 +80,32 @@ public void GeneratedIR_FunctionWithWhitespaceParameterName_ProducesValidOutput Assert.That (output, Does.Not.Contain ("%\t)"), "Generated LLVM IR should not contain 'ptr noundef %\\t)' pattern"); Assert.That (output, Does.Contain ("@test_function"), "Generated LLVM IR should contain the function name"); } + + /// + /// Regression test for the NativeAOT link failure in https://github.com/dotnet/android/pull/11669. + /// + /// `Android.Runtime.JNIEnvInit.Initialize` (used only by MonoVM/CoreCLR) has a `[LibraryImport]` + /// p/invoke to the native `xamarin_app_init` function, which becomes a direct native reference + /// under NativeAOT (`xa-internal-api` is a `DirectPInvoke`). Since `Initialize` is force-preserved + /// by the ILLink descriptor and is never trimmed, the reference reaches the linker even though + /// NativeAOT never generates the marshal methods native code that defines `xamarin_app_init`. + /// The NativeAOT jni-init assembler source must therefore emit a no-op definition so the linker + /// can resolve the symbol. + /// + [Test] + public void NativeAotJniInit_EmitsNoOpXamarinAppInit () + { + var log = new TaskLoggingHelper (new MockBuildEngine (TestContext.Out, [], [], []), "test"); + var composer = new NativeAotJniInitNativeAssemblyGenerator (log, runtimeComponentsJniOnLoadHandlers: null, customJniOnLoadHandlers: null); + LlvmIrModule module = composer.Construct (); + + var generator = LlvmIrGenerator.Create (AndroidTargetArch.Arm64, "jniinit.ll"); + using var writer = new StringWriter (); + generator.Generate (writer, module); + + string output = writer.ToString (); + Assert.That (output, Does.Contain ("define"), "Generated LLVM IR should contain a function definition"); + Assert.That (output, Does.Contain ("@xamarin_app_init"), "Generated LLVM IR should define xamarin_app_init so the NativeAOT linker can resolve JNIEnvInit.Initialize's DirectPInvoke"); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs index a865295d8d9..834ebf31a50 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs @@ -29,6 +29,8 @@ protected override void Construct (LlvmIrModule module) CollectHandlers (customJniOnLoadHandlers); JniOnLoadNativeAssemblerHelper.GenerateJniOnLoadHandlerCode (jniOnLoadNames, module); + DefineNoOpXamarinAppInit (module); + void CollectHandlers (List? handlers) { if (handlers == null || handlers.Count == 0) { @@ -44,4 +46,24 @@ void CollectHandlers (List? handlers) } } } + + // The managed method `Android.Runtime.JNIEnvInit.Initialize` (used only by MonoVM and CoreCLR) has a + // `[LibraryImport]` p/invoke to the native `xamarin_app_init` function, which becomes a direct native + // reference because the `xa-internal-api` "library" is registered as a `DirectPInvoke` for NativeAOT. + // That symbol is only ever defined by the MonoVM/CoreCLR marshal methods native code, which is never + // generated for NativeAOT. `Initialize` is unreachable under NativeAOT (which uses + // `InitializeNativeAotRuntime` instead), but it is force-preserved by the ILLink descriptor + // (`PreserveLists/Mono.Android.xml`), so the unresolved reference reaches the linker and fails with + // XA3007 "undefined symbol: xamarin_app_init". Emit an empty definition to satisfy the linker; it is + // never actually called under NativeAOT. + static void DefineNoOpXamarinAppInit (LlvmIrModule module) + { + var parameters = new List { + new LlvmIrFunctionParameter (typeof (IntPtr), "env"), + new LlvmIrFunctionParameter (typeof (IntPtr), "fn"), + }; + var func = new LlvmIrFunction ("xamarin_app_init", typeof (void), parameters); + func.Body.Ret (typeof (void)); + module.Add (func); + } }