From de65cd0243a0e3e8713d7face15349531423b2ac Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Apr 2026 10:40:11 -0500 Subject: [PATCH] Fix wrong-TFM assembly loading producing empty JCW .jlo.xml files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In .NET 9, `GenerateJavaStubs` scanned for Java types using `XAJavaTypeScanner.GetJavaTypes`, which called `resolver.Load(asmItem.ItemSpec)` to load each assembly from its exact file path. This ensured the correct TFM version was always used. In ea399edb (#9893), JCW scanning was moved into `AssemblyModifierPipeline` as `FindJavaObjectsStep`. The pipeline's `RunPipeline` method uses `resolver.GetAssembly(source.ItemSpec)`, which strips the path and resolves by assembly **name** through search directories. When a multi-TFM library (e.g. `net11.0;net11.0-android`) is transitively referenced through a `net11.0`-only intermediary, the intermediary's output directory contains a CopyLocal'd `net11.0` copy of the assembly. If that directory appears in the resolver's search path before the correct `net11.0-android` directory, `GetAssembly` loads the wrong version — one without any Android types — producing empty `.jlo.xml` files with 0 JCW types. The fix pre-loads all `ResolvedAssemblies` into the resolver cache from their exact `ItemSpec` paths during resolver setup. This ensures that both `GetAssembly` (which checks the cache first) and Cecil's lazy reference resolution always find the correct TFM version. A regression test `MultiTfmTransitiveReference` reproduces the exact scenario: App -> CoreLib (net11.0) -> MultiTfmLib (net11.0; net11.0-android), where MultiTfmLib has an Android `BroadcastReceiver` behind `#if ANDROID`. Fixes https://github.com/dotnet/android/issues/10858 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/AssemblyModifierPipeline.cs | 8 +- .../BuildWithLibraryTests.cs | 126 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 08a558de918..69cfedb54b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -102,13 +102,19 @@ public override bool RunTask () var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: ReadSymbols, loadReaderParameters: readerParameters); - // Add SearchDirectories for the current architecture's ResolvedAssemblies + // Add SearchDirectories and pre-load ResolvedAssemblies into the resolver cache. + // Pre-loading ensures the correct TFM version is cached before any Cecil lazy + // reference resolution can find wrong-TFM copies from search directories (e.g., + // a net11.0 copy in a referencing project's output directory). foreach (var kvp in perArchAssemblies [sourceArch]) { ITaskItem assembly = kvp.Value; var path = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec)); if (!resolver.SearchDirectories.Contains (path)) { resolver.SearchDirectories.Add (path); } + if (resolver.Load (assembly.ItemSpec) == null) { + Log.LogDebugMessage ($"Could not pre-load assembly '{assembly.ItemSpec}' into resolver cache."); + } } // Set up the FixAbstractMethodsStep and AddKeepAlivesStep diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index 29d950a2c92..59e2adf47a2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using Xamarin.ProjectTools; using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; namespace Xamarin.Android.Build.Tests { @@ -1074,5 +1075,130 @@ public void DotNetLibraryAarChanges ([Values] AndroidRuntime runtime) } } + /// + /// Regression test for https://github.com/dotnet/android/issues/10858 + /// + /// When a solution has the structure: + /// App (net11.0-android) → CoreLib (net11.0) → MultiTfmLib (net11.0;net11.0-android) + /// + /// And MultiTfmLib contains Java-interop types (e.g. BroadcastReceiver) that only + /// exist in the net11.0-android TFM, the build tasks must load the Android-TFM assembly + /// and generate JCWs for those types. Previously, the net11.0 (non-Android) assembly + /// could be loaded instead, causing FindJavaObjectsStep to report "Found 0 Java types" + /// and producing empty .jlo.xml files. + /// + [Test] + public void MultiTfmTransitiveReference ([Values] AndroidRuntime runtime) + { + if (IgnoreUnsupportedConfiguration (runtime, release: false)) { + return; + } + + var path = Path.Combine ("temp", TestName); + var dotnetVersion = XABuildConfig.LatestDotNetTargetFramework; + + // 1. Multi-TFM library (net11.0 + net11.0-android) with a BroadcastReceiver + var multiTfmLib = new DotNetStandard { + ProjectName = "MultiTfmLib", + Sdk = "Microsoft.NET.Sdk", + Sources = { + new BuildItem.Source ("MyReceiver.cs") { + TextContent = () => +@"#if ANDROID +using Android.Content; + +namespace MultiTfmLib +{ + public class MyReceiver : BroadcastReceiver + { + public override void OnReceive (Context? context, Intent? intent) { } + } +} +#endif +" + }, + new BuildItem.Source ("SharedClass.cs") { + TextContent = () => +@"namespace MultiTfmLib +{ + public class SharedClass + { + public string Name => ""Hello""; + } +} +" + }, + }, + }; + multiTfmLib.TargetFrameworks = $"{dotnetVersion};{dotnetVersion}-android"; + multiTfmLib.SetProperty ("Nullable", "enable"); + multiTfmLib.SetProperty ("AndroidGenerateResourceDesigner", "false"); + multiTfmLib.SetProperty ("SupportedOSPlatformVersion", "24", + "$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'"); + if (runtime != AndroidRuntime.NativeAOT) { + multiTfmLib.SetRuntime (runtime); + } + + // 2. Non-Android library (net11.0 only) referencing the multi-TFM library + var coreLib = new DotNetStandard { + ProjectName = "CoreLib", + Sdk = "Microsoft.NET.Sdk", + Sources = { + new BuildItem.Source ("CoreClass.cs") { + TextContent = () => +@"namespace CoreLib +{ + public class CoreClass + { + public MultiTfmLib.SharedClass Shared => new (); + } +} +" + }, + }, + }; + coreLib.TargetFramework = dotnetVersion; + coreLib.AddReference (multiTfmLib); + + // 3. Android app that references CoreLib (NOT MultiTfmLib directly) + var app = new XamarinAndroidApplicationProject { + ProjectName = "MyApp", + Sources = { + new BuildItem.Source ("Usage.cs") { + TextContent = () => +@"public class Usage +{ + public CoreLib.CoreClass Core => new (); +} +" + }, + }, + }; + app.SetRuntime (runtime); + app.AddReference (coreLib); + + using var multiTfmBuilder = CreateDllBuilder (Path.Combine (path, multiTfmLib.ProjectName)); + Assert.IsTrue (multiTfmBuilder.Build (multiTfmLib), $"{multiTfmLib.ProjectName} should build"); + + using var coreBuilder = CreateDllBuilder (Path.Combine (path, coreLib.ProjectName)); + Assert.IsTrue (coreBuilder.Build (coreLib), $"{coreLib.ProjectName} should build"); + + using var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName)); + Assert.IsTrue (appBuilder.Build (app), $"{app.ProjectName} should build"); + + // Verify: MultiTfmLib.jlo.xml should NOT be empty (i.e. the assembly was scanned as an Android assembly) + var jloXml = appBuilder.Output.GetIntermediaryPath ( + Path.Combine ("android", "assets", "arm64-v8a", "MultiTfmLib.jlo.xml")); + FileAssert.Exists (jloXml); + + var jloXmlInfo = new FileInfo (jloXml); + Assert.IsTrue (jloXmlInfo.Length > 0, + "MultiTfmLib.jlo.xml should not be empty — the Android-TFM assembly was not loaded (wrong TFM loaded instead)"); + + var jloContent = File.ReadAllText (jloXml); + Assert.IsTrue (jloContent.Contains ("MyReceiver"), + $"MultiTfmLib.jlo.xml should contain the MyReceiver JCW type, but got: {jloContent}"); + } + } }