diff --git a/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java b/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java index 6ee0fd3f41c..c2f33c45925 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java +++ b/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java @@ -1,11 +1,13 @@ package net.dot.jni.nativeaot; import android.util.Log; +import android.content.Context; +import mono.NativeLibraryHelper; public class JavaInteropRuntime { - static { + public static void loadLibrary(Context context) { Log.d("JavaInteropRuntime", "Loading @MAIN_ASSEMBLY_NAME@.so..."); - System.loadLibrary("@MAIN_ASSEMBLY_NAME@"); + NativeLibraryHelper.loadLibrary("@MAIN_ASSEMBLY_NAME@", context); } private JavaInteropRuntime() { diff --git a/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java b/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java index 90f392160e0..c1feb566c93 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java +++ b/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java @@ -36,6 +36,7 @@ public void attachInfo(android.content.Context context, android.content.pm.Provi String cacheDir = context.getCacheDir().getAbsolutePath(); // Initialize .NET runtime + JavaInteropRuntime.loadLibrary(context); JavaInteropRuntime.init(loader, language, filesDir, cacheDir); // NOTE: only required for custom applications ApplicationRegistration.registerApplications(); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc index 8453587c3ba..30f0c810c73 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc @@ -5,7 +5,7 @@ "Size": 6652 }, "classes.dex": { - "Size": 3240220 + "Size": 3242876 }, "kotlin/annotation/annotation.kotlin_builtins": { "Size": 928 @@ -29,31 +29,31 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 11217040 + "Size": 11415872 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2761136 + "Size": 2788736 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4839560 + "Size": 4841976 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1184912 + "Size": 1184800 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { - "Size": 1258776 + "Size": 1259088 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 99776 + "Size": 99792 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { - "Size": 169768 + "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 119656 + "Size": 119776 }, "META-INF/androidx.activity_activity.version": { "Size": 6 diff --git a/src/java-runtime/java/mono/android/MonoPackageManager.java b/src/java-runtime/java/mono/android/MonoPackageManager.java index 6531bd2da3d..38927fda9d9 100644 --- a/src/java-runtime/java/mono/android/MonoPackageManager.java +++ b/src/java-runtime/java/mono/android/MonoPackageManager.java @@ -99,19 +99,19 @@ public static void LoadApplication (Context context) // below, leading to an error locating the Mono runtime // if (BuildConfig.Debug) { - System.loadLibrary ("xamarin-debug-app-helper"); + NativeLibraryHelper.loadLibrary ("xamarin-debug-app-helper", runtimePackage, apks); DebugRuntime.init (apks, runtimeDir, appDirs, haveSplitApks); } else { - System.loadLibrary("monosgen-2.0"); + NativeLibraryHelper.loadLibrary ("monosgen-2.0", runtimePackage, apks); } - System.loadLibrary("xamarin-app"); + NativeLibraryHelper.loadLibrary ("xamarin-app", runtimePackage, apks); if (!BuildConfig.DotNetRuntime) { // .net5+ APKs don't contain `libmono-native.so` - System.loadLibrary("mono-native"); + NativeLibraryHelper.loadLibrary ("mono-native", runtimePackage, apks); } - System.loadLibrary("monodroid"); + NativeLibraryHelper.loadLibrary ("monodroid", runtimePackage, apks); Runtime.initInternal ( language, diff --git a/src/java-runtime/java/mono/android/NativeLibraryHelper.java b/src/java-runtime/java/mono/android/NativeLibraryHelper.java new file mode 100644 index 00000000000..917ba240b4f --- /dev/null +++ b/src/java-runtime/java/mono/android/NativeLibraryHelper.java @@ -0,0 +1,149 @@ +package mono; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.util.Log; + +public final class NativeLibraryHelper { + static final String TAG = "monodroid"; + + private NativeLibraryHelper () + { + } + + public static void loadLibrary (String libraryName, Context context) + { + ApplicationInfo applicationInfo = context.getApplicationInfo (); + String[] splitApks = applicationInfo.splitSourceDirs; + String[] apks; + if (splitApks != null && splitApks.length > 0) { + apks = new String [splitApks.length + 1]; + apks [0] = applicationInfo.sourceDir; + System.arraycopy (splitApks, 0, apks, 1, splitApks.length); + } else { + apks = new String [] { applicationInfo.sourceDir }; + } + + loadLibrary (libraryName, applicationInfo, apks); + } + + static void loadLibrary (String libraryName, ApplicationInfo applicationInfo, String[] apks) + { + try { + System.loadLibrary (libraryName); + } catch (UnsatisfiedLinkError cause) { + String diagnosticMessage = getDiagnosticMessage (libraryName, applicationInfo, apks, cause); + Log.e (TAG, diagnosticMessage, cause); + + UnsatisfiedLinkError error = new UnsatisfiedLinkError (diagnosticMessage); + error.initCause (cause); + throw error; + } catch (SecurityException cause) { + String diagnosticMessage = getDiagnosticMessage (libraryName, applicationInfo, apks, cause); + Log.e (TAG, diagnosticMessage, cause); + throw new SecurityException (diagnosticMessage, cause); + } + } + + static String getDiagnosticMessage (String libraryName, ApplicationInfo applicationInfo, String[] apks, Throwable cause) + { + String mappedLibraryName = System.mapLibraryName (libraryName); + StringBuilder message = new StringBuilder (); + message.append ("Failed to load native library '").append (mappedLibraryName).append ("'."); + message.append (" Supported ABIs: ").append (Arrays.toString (Build.SUPPORTED_ABIS)).append ("."); + + boolean foundLibrary = appendNativeLibraryDirectoryDiagnostics (message, applicationInfo.nativeLibraryDir, mappedLibraryName); + foundLibrary |= appendApkDiagnostics (message, apks, mappedLibraryName); + + if (!foundLibrary) { + message.append (" The library was not found in the native library directory or any application APK that could be inspected."); + message.append (" The application installation may be corrupt; reinstalling the application may fix this error."); + } + + String causeMessage = cause.getMessage (); + if (causeMessage != null && causeMessage.length () > 0) { + message.append (" Original error: ").append (causeMessage); + } + + return message.toString (); + } + + static boolean appendNativeLibraryDirectoryDiagnostics (StringBuilder message, String nativeLibraryDir, String mappedLibraryName) + { + message.append (" Native library directory: "); + if (nativeLibraryDir == null) { + message.append ("."); + return false; + } + + File directory = new File (nativeLibraryDir); + File library = new File (directory, mappedLibraryName); + boolean libraryExists = library.isFile (); + message.append ('\'').append (nativeLibraryDir).append ('\''); + message.append (" (directory exists: ").append (directory.isDirectory ()); + message.append (", library exists: ").append (libraryExists); + if (libraryExists) { + message.append (", library size: ").append (library.length ()); + message.append (", library readable: ").append (library.canRead ()); + } + message.append (")."); + return libraryExists; + } + + static boolean appendApkDiagnostics (StringBuilder message, String[] apks, String mappedLibraryName) + { + boolean foundLibrary = false; + message.append (" APKs:"); + if (apks == null || apks.length == 0) { + message.append (" ."); + return false; + } + + for (String apk : apks) { + message.append (" '").append (apk).append ("'"); + if (apk == null) { + message.append (" (invalid path);"); + continue; + } + + File apkFile = new File (apk); + if (!apkFile.isFile ()) { + message.append (" (file exists: false);"); + continue; + } + + ArrayList entries = new ArrayList (); + try (ZipFile zip = new ZipFile (apkFile)) { + for (String abi : Build.SUPPORTED_ABIS) { + String entryName = "lib/" + abi + "/" + mappedLibraryName; + ZipEntry entry = zip.getEntry (entryName); + if (entry == null) + continue; + + foundLibrary = true; + String storage = entry.getMethod () == ZipEntry.STORED ? "stored" : "compressed"; + entries.add (abi + ", " + storage + ", size " + entry.getSize ()); + } + if (entries.size () == 0) + message.append (" (contains no matching native libraries);"); + else + message.append (" (contains: ").append (entries).append (");"); + } catch (IOException | SecurityException e) { + message.append (" (could not inspect: ").append (e.getClass ().getSimpleName ()); + String errorMessage = e.getMessage (); + if (errorMessage != null && errorMessage.length () > 0) + message.append (": ").append (errorMessage); + message.append (");"); + } + } + + return foundLibrary; + } +} diff --git a/src/java-runtime/java/mono/android/clr/MonoPackageManager.java b/src/java-runtime/java/mono/android/clr/MonoPackageManager.java index 5592cc2b68e..dca751a9a77 100644 --- a/src/java-runtime/java/mono/android/clr/MonoPackageManager.java +++ b/src/java-runtime/java/mono/android/clr/MonoPackageManager.java @@ -70,7 +70,7 @@ public static void LoadApplication (Context context) String[] appDirs = new String[] {filesDir, cacheDir, dataDir, codeCacheDir}; boolean haveSplitApks = runtimePackage.splitSourceDirs != null && runtimePackage.splitSourceDirs.length > 0; - System.loadLibrary("monodroid"); + NativeLibraryHelper.loadLibrary ("monodroid", runtimePackage, apks); Runtime.initInternal ( language, diff --git a/tests/MSBuildDeviceIntegration/Tests/NativeLibraryLoadTests.cs b/tests/MSBuildDeviceIntegration/Tests/NativeLibraryLoadTests.cs new file mode 100644 index 00000000000..8693b89361e --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/NativeLibraryLoadTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +[Category ("UsesDevice")] +public class NativeLibraryLoadTests : DeviceTest +{ + [Test] + public void MissingNativeLibraryHasUsefulErrorMessage ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject ( + packageName: PackageUtils.MakePackageName (runtime, "missingnativelibrary") + ) { + IsRelease = isRelease, + ProjectName = "MissingNativeLibrary", + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetDefaultTargetDevice (); + + string libraryName; + string removeLibraryItems; + if (runtime == AndroidRuntime.NativeAOT) { + libraryName = $"lib{proj.ProjectName}.so"; + removeLibraryItems = $@" + + <_ApplicationSharedLibrary Remove=""@(_ApplicationSharedLibrary)"" + Condition="" '%(_ApplicationSharedLibrary.ArchiveFileName)' == '{libraryName}' or '%(_ApplicationSharedLibrary.FileName)%(_ApplicationSharedLibrary.Extension)' == '{libraryName}' "" />"; + } else { + libraryName = "libmonodroid.so"; + removeLibraryItems = @" + + <_ApplicationSharedLibrary Remove=""@(_ApplicationSharedLibrary)"" + Condition="" '%(_ApplicationSharedLibrary.ArchiveFileName)' == 'libmonodroid.so' or '%(_ApplicationSharedLibrary.FileName)%(_ApplicationSharedLibrary.Extension)' == 'libmonodroid.so' "" />"; + } + + proj.Imports.Add (new Import (() => "Directory.Build.targets") { + TextContent = () => $""" + + + + {removeLibraryItems} + + + +""" + }); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Install (proj), "Project should have installed."); + + string outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + string[] apks = Directory.GetFiles (outputDirectory, $"{proj.PackageName}-Signed.apk", SearchOption.AllDirectories); + Assert.IsNotEmpty (apks, "The signed APK should exist."); + using (var apk = ZipHelper.OpenZip (apks [0])) { + Assert.IsFalse (apk.ContainsEntry ($"lib/{DeviceAbi}/{libraryName}"), + $"{libraryName} should have been removed from the APK."); + } + + var expectedMessages = new HashSet { + $"Failed to load native library '{libraryName}'.", + "Supported ABIs:", + "Native library directory:", + "library exists: false", + "APKs:", + "contains no matching native libraries", + "The application installation may be corrupt; reinstalling the application may fix this error.", + }; + string logcatPath = Path.Combine (Root, builder.ProjectDirectory, "native-library-load.log"); + bool foundDiagnostic = MonitorAdbLogcat ( + line => { + expectedMessages.RemoveWhere (message => line.Contains (message, StringComparison.Ordinal)); + return expectedMessages.Count == 0; + }, + logcatPath, + timeout: 45, + onMonitoringStarted: () => AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity") + ); + + Assert.IsTrue (foundDiagnostic, + $"The native library diagnostic was incomplete. Missing: {string.Join (", ", expectedMessages)}"); + } +}