Skip to content

Commit b45929f

Browse files
[Xamarin.Android.Build.Tasks] fix detection of "Android libraries" (#8904)
Fixes: dotnet/maui#18819 Context: https://github.com/kernshen/NoJavaPeer.git Context: #4225 (comment) An assembly's assembly references do not include transitive dependencies. Given: // Mono.Android.dll namespace Java.Lang { public partial class Object {} } // MauiLib1.dll namespace MauiLib1 { public class BaseClass : Java.Lang.Object {} } // MauiLib2.dll namespace MauiLib2 { public class DerivedClass3 : MauiLib1.BaseClass {} } then the assembly references for `MauiLib1.dll` will include `Mono.Android.dll` (it directly references a type from it), while the assembly references for `MauiLib2.dll` will include `MauiLib1.dll` (it directly references a type from it) *but* `MauiLib2.dll` *will not* have an assembly reference to `Mono.Android.dll`. This is how things have worked since .NET Framework 1.0. This should not be surprising. [As part of the .NET for Android][0] [`SignAndroidPackage`][1] target, Java Callable Wrappers (JCWs) need to be emitted for all `Java.Lang.Object` subclasses. This in turn requires *loading all assemblies* to *find* the `Java.Lang.Object` subclasses. As a performance optimization, we only load assemblies which we believed could contain `Java.Lang.Object` subclasses: 1. Assemblies with `'%(TargetFrameworkIdentifier)' == 'MonoAndroid'`, which is "carry over" from how Xamarin.Android did things, and works if a .NET for Android project references a Xamarin.Android project. 2. Assemblies with an assembly reference to `Mono.Android.dll`. Assemblies with transitive dependencies were caught by (1)… in Xamarin.Android. With .NET for Android, that is no longer the case: `%(TargetFrameworkIdentifier)` is now always `.NETCoreApp`. This in turn meant that the only assemblies that could be used to generate JCWs were those which directly referenced `Mono.Android.dll`! Enter dotnet/maui#18819 and kernshen/NoJavaPeer, which contains MAUI and .NET for Android solutions with the above transitive reference structure: 1. `MauiLib1.dll` / `AndroidLib1.dll` references `Mono.Android.dll`, exports `BaseClass` 2. `MauiLib2.dll` / `AndroidLib2.dll` references `*Lib1.dll` *and not* `Mono.Android.dll`; exports `DerivedClass3` which inherits `BaseClass`. 3. App project attempts to instantiate `DerivedClass3`. The result: a runtime exception: Only System.NotSupportedException Message=Cannot create instance of type 'MauiLib2.DerivedClass3': no Java peer type found. at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 22 at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 77 at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 139 at Java.Lang.Object..ctor() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Java.Lang.Object.cs:line 37 at MauiLib1.BaseClass..ctor() at MauiLib2.DerivedClass..ctor() in C:\Project\Maui\MauiApp1\MauiLib2\Platforms\Android\DerivedClass.cs:line 7 at MauiApp1.App..ctor(IServiceProvider serviceProvider) in C:\Project\Maui\MauiApp1\MauiApp1\App.xaml.cs:line 18 The exception occurs because there is no JCW for `DerivedClass3`, and there isn't a JCW for `DerivedClass3` because `MauiLib2.dll` was not processed at all, because it had no assembly reference to `Mono.Android.dll`. As a workaround, update `MauiLib2.dll` to contain an assembly reference to `Mono.Android.dll`. *Fix* this scenario by updating `MonoAndroidHelper.IsMonoAndroidAssembly()` to consider these to be .NET for Android assemblies: 1. Assemblies with `%(TargetFrameworkIdentifier)` *containing* `Android`. (This doesn't actually change anything; it's a simplification.) 2. Assemblies with `%(TargetPlatformIdentifier)` *containing* `Android`. *This* causes `MauiLib2.dll` to be treated as a .NET for Android assembly, fixing the bug. 3. Assemblies with an assembly reference to `Mono.Android.dll`. The addition of check (2) allows assemblies with only transitive (non-) references to `Mono.Android.dll` to be properly considered, allowing JCWs to be emitted for types within them. Update the `BuildWithLibraryTests.ProjectDependencies()` unit test to better check for this scenario. [0]: https://github.com/xamarin/xamarin-android/wiki/Blueprint#after-build [1]: https://learn.microsoft.com/en-us/dotnet/android/building-apps/build-targets#signandroidpackage
1 parent 272887a commit b45929f

File tree

2 files changed

+25
-9
lines changed

2 files changed

+25
-9
lines changed

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,7 @@ public void ProjectDependencies ([Values(true, false)] bool projectReference)
269269
};
270270
libB.Sources.Clear ();
271271
libB.Sources.Add (new BuildItem.Source ("Foo.cs") {
272-
TextContent = () => @"public class Foo {
273-
public Foo () {
274-
var bar = new Bar();
275-
}
276-
}",
272+
TextContent = () => "public class Foo : Bar { }",
277273
});
278274

279275
var libC = new XamarinAndroidLibraryProject () {
@@ -283,7 +279,7 @@ public Foo () {
283279
};
284280
libC.Sources.Clear ();
285281
libC.Sources.Add (new BuildItem.Source ("Bar.cs") {
286-
TextContent = () => "public class Bar { }",
282+
TextContent = () => "public class Bar : Java.Lang.Object { }",
287283
});
288284
libC.Sources.Add (new BuildItem ("EmbeddedResource", "Foo.resx") {
289285
TextContent = () => InlineData.ResxWithContents ("<data name=\"CancelButton\"><value>Cancel</value></data>")
@@ -309,8 +305,8 @@ public Foo () {
309305
ProjectName = "AppA",
310306
IsRelease = true,
311307
Sources = {
312-
new BuildItem.Source ("Bar.cs") {
313-
TextContent = () => "public class Bar : Foo { }",
308+
new BuildItem.Source ("Baz.cs") {
309+
TextContent = () => "public class Baz : Foo { }",
314310
},
315311
new BuildItem ("EmbeddedResource", "Foo.resx") {
316312
TextContent = () => InlineData.ResxWithContents ("<data name=\"CancelButton\"><value>Cancel</value></data>")
@@ -321,6 +317,10 @@ public Foo () {
321317
}
322318
};
323319
appA.AddReference (libB);
320+
if (!projectReference) {
321+
// @(ProjectReference) implicits adds this reference. For `class Baz : Foo : Bar`:
322+
appA.OtherBuildItems.Add (new BuildItem.Reference ($@"..\{libC.ProjectName}\bin\Release\{libC.TargetFramework}\{libC.ProjectName}.dll"));
323+
}
324324
var appBuilder = CreateApkBuilder (Path.Combine (path, appA.ProjectName));
325325
Assert.IsTrue (appBuilder.Build (appA), $"{appA.ProjectName} should succeed");
326326

@@ -332,6 +332,17 @@ public Foo () {
332332
helper.AssertContainsEntry ($"assemblies/{libC.ProjectName}.dll");
333333
helper.AssertContainsEntry ($"assemblies/es/{appA.ProjectName}.resources.dll");
334334
helper.AssertContainsEntry ($"assemblies/es/{libC.ProjectName}.resources.dll");
335+
336+
var intermediate = Path.Combine (Root, appBuilder.ProjectDirectory, appA.IntermediateOutputPath);
337+
var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
338+
FileAssert.Exists (dexFile);
339+
340+
// NOTE: the crc hashes here might change one day, but if we used [Android.Runtime.Register("")]
341+
// LibraryB.dll would have a reference to Mono.Android.dll, which invalidates the test.
342+
string className = "Lcrc6414a4b78410c343a2/Bar;";
343+
Assert.IsTrue (DexUtils.ContainsClass (className, dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
344+
className = "Lcrc646d2d82b4d8b39bd8/Foo;";
345+
Assert.IsTrue (DexUtils.ContainsClass (className, dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
335346
}
336347

337348
[Test]

src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,13 @@ public static void LogWarning (object log, string msg, params object [] args)
277277
#if MSBUILD
278278
public static bool IsMonoAndroidAssembly (ITaskItem assembly)
279279
{
280+
// NOTE: look for both MonoAndroid and Android
280281
var tfi = assembly.GetMetadata ("TargetFrameworkIdentifier");
281-
if (string.Compare (tfi, "MonoAndroid", StringComparison.OrdinalIgnoreCase) == 0)
282+
if (tfi.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1)
283+
return true;
284+
285+
var tpi = assembly.GetMetadata ("TargetPlatformIdentifier");
286+
if (tpi.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1)
282287
return true;
283288

284289
var hasReference = assembly.GetMetadata ("HasMonoAndroidReference");

0 commit comments

Comments
 (0)