From 853c7c5a0aa631f321c6311642c58360311ec70f Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Mon, 3 Sep 2018 23:54:49 +0200 Subject: [PATCH 1/2] Support DSOs embedded in the APK Context: https://github.com/xamarin/xamarin-android/issues/1906 Fixes: https://github.com/xamarin/xamarin-android/issues/1906 Android API 23 introduced a new way of dealing with the native shared libraries shipped in the APK. Before that API level, the libraries would be always extraced and placed in the application data directory, thus occupying more space than necessary. API 23 added a new manifest `` element attribute, `android:extractNativeLibs`, which if set makes Android not extract the libraries to the filesystem. API 23 added a way to load those libraries directly from the APK. In order to support that there are a few requirements which this commit implements: * DSO (`.so`) files must be stored uncompressed in the APK * `` must be present * DSOs in the APK must be aligned on the memory page boundary (the `-p` flag passed to `zipalign` takes care of that This commit also implements `libmonodroid` suport for loading our DSOs directly from the APK. This operation mode is enabled by the presence of the `__XA_DSO_IN_APK` environment variable. This variable is inserted into the application's environment by way of placing it in the environment file (a file part of the XA project that is marked with the `AndroidEnvironment` build action). In that mode, the DSOs are *no longer* looked up in the application data directory but only in the override directories (if the APK is built in Debug configuration) and in the APK itself. Currently, in order to activate the above mode, one has to perform the following actions manually: * Add the `android:extractNativeLibs="false"` attribute to the `` element in the `Properties/AndroidManifest.xml` file * Add the following property to the project file: .so * Add an android environment file to the project with a line which says __XA_DSO_IN_APK=1 After that the application should work in the embedded DSO mode without problems. A couple of tests are provided to test building and execution of embedded DSO application on device, as well as to validate the built APK. --- Xamarin.Android-Tests.sln | 12 + build-tools/scripts/RunTests.targets | 2 + .../Tasks/AndroidZipAlign.cs | 2 +- src/monodroid/jni/dylib-mono.c | 15 +- src/monodroid/jni/dylib-mono.h | 2 +- src/monodroid/jni/monodroid-glue.c | 479 ++++++++++++------ src/monodroid/jni/util.c | 16 + src/monodroid/jni/util.h | 7 +- .../EmbeddedDSO-UnitTests/BuildTests.cs | 299 +++++++++++ .../EmbeddedDSO-UnitTests/Config.cs.in | 10 + .../EmbeddedDSO-UnitTests.csproj | 73 +++ .../Properties/AssemblyInfo.cs | 26 + .../EmbeddedDSO-UnitTests/packages.config | 4 + .../EmbeddedDSOs/EmbeddedDSO-UnitTests/run.sh | 7 + .../EmbeddedDSO/Assets/AboutAssets.txt | 19 + .../EmbeddedDSO/EmbeddedDSO.csproj | 118 +++++ .../EmbeddedDSO/EmbeddedDSO.projitems | 34 ++ .../EmbeddedDSOs/EmbeddedDSO/Environment.txt | 5 + .../EmbeddedDSOs/EmbeddedDSO/MainActivity.cs | 25 + .../EmbeddedDSO/NUnitInstrumentation.cs | 57 +++ .../Properties/AndroidManifest.xml | 9 + .../EmbeddedDSO/Properties/AssemblyInfo.cs | 27 + .../EmbeddedDSO/Resources/AboutResources.txt | 44 ++ .../EmbeddedDSO/Resources/layout/Main.axml | 4 + .../Resources/mipmap-hdpi/Icon.png | Bin 0 -> 2201 bytes .../Resources/mipmap-mdpi/Icon.png | Bin 0 -> 1410 bytes .../Resources/mipmap-xhdpi/Icon.png | Bin 0 -> 3237 bytes .../Resources/mipmap-xxhdpi/Icon.png | Bin 0 -> 5414 bytes .../Resources/mipmap-xxxhdpi/Icon.png | Bin 0 -> 7825 bytes .../EmbeddedDSO/Resources/values/Strings.xml | 5 + tests/EmbeddedDSOs/EmbeddedDSO/Tests.cs | 33 ++ tests/RunApkTests.targets | 1 + 32 files changed, 1151 insertions(+), 184 deletions(-) create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/BuildTests.cs create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Config.cs.in create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/EmbeddedDSO-UnitTests.csproj create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Properties/AssemblyInfo.cs create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/packages.config create mode 100755 tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/run.sh create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Assets/AboutAssets.txt create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.projitems create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/MainActivity.cs create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/NUnitInstrumentation.cs create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Properties/AndroidManifest.xml create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Properties/AssemblyInfo.cs create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/AboutResources.txt create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/layout/Main.axml create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/mipmap-hdpi/Icon.png create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/mipmap-mdpi/Icon.png create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/mipmap-xhdpi/Icon.png create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/mipmap-xxhdpi/Icon.png create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/mipmap-xxxhdpi/Icon.png create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Resources/values/Strings.xml create mode 100644 tests/EmbeddedDSOs/EmbeddedDSO/Tests.cs diff --git a/Xamarin.Android-Tests.sln b/Xamarin.Android-Tests.sln index 308a55fd1eb..40807925892 100644 --- a/Xamarin.Android-Tests.sln +++ b/Xamarin.Android-Tests.sln @@ -76,6 +76,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Performance-Tests", "Perfor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "timing", "build-tools\timing\timing.csproj", "{37CAA28C-40BE-4253-BA68-CC5D7316A617}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedDSO", "tests\EmbeddedDSOs\EmbeddedDSO\EmbeddedDSO.csproj", "{056ED976-618F-4A3E-910E-AA25230C2296}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedDSO-UnitTests", "tests\EmbeddedDSOs\EmbeddedDSO-UnitTests\EmbeddedDSO-UnitTests.csproj", "{B160F0E7-799A-4EB9-92B8-D71623C7674A}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution tests\Xamarin.Forms-Performance-Integration\Xamarin.Forms.Performance.Integration.projitems*{195be9c2-1f91-40dc-bd6d-de860bf083fb}*SharedItemsImports = 13 @@ -194,6 +198,14 @@ Global {37CAA28C-40BE-4253-BA68-CC5D7316A617}.Debug|Any CPU.Build.0 = Debug|Any CPU {37CAA28C-40BE-4253-BA68-CC5D7316A617}.Release|Any CPU.ActiveCfg = Release|Any CPU {37CAA28C-40BE-4253-BA68-CC5D7316A617}.Release|Any CPU.Build.0 = Release|Any CPU + {056ED976-618F-4A3E-910E-AA25230C2296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {056ED976-618F-4A3E-910E-AA25230C2296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {056ED976-618F-4A3E-910E-AA25230C2296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {056ED976-618F-4A3E-910E-AA25230C2296}.Release|Any CPU.Build.0 = Release|Any CPU + {B160F0E7-799A-4EB9-92B8-D71623C7674A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B160F0E7-799A-4EB9-92B8-D71623C7674A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B160F0E7-799A-4EB9-92B8-D71623C7674A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B160F0E7-799A-4EB9-92B8-D71623C7674A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build-tools/scripts/RunTests.targets b/build-tools/scripts/RunTests.targets index 2543d7674ff..626245cde70 100644 --- a/build-tools/scripts/RunTests.targets +++ b/build-tools/scripts/RunTests.targets @@ -17,12 +17,14 @@ <_TestAssembly Include="$(_TopDir)\bin\Test$(Configuration)\Xamarin.Android.Build.Tests.dll" /> <_TestAssembly Include="$(_TopDir)\bin\Test$(Configuration)\CodeBehind\CodeBehindUnitTests.dll" /> + <_TestAssembly Include="$(_TopDir)\bin\Test$(Configuration)\EmbeddedDSO\EmbeddedDSOUnitTests.dll" /> <_ApkTestProject Include="$(_TopDir)\src\Mono.Android\Test\Mono.Android-Tests.csproj" /> <_ApkTestProject Include="$(_TopDir)\tests\CodeGen-Binding\Xamarin.Android.JcwGen-Tests\Xamarin.Android.JcwGen-Tests.csproj" /> <_ApkTestProject Include="$(_TopDir)\tests\CodeGen-MkBundle\Xamarin.Android.MakeBundle-Tests\Xamarin.Android.MakeBundle-Tests.csproj" /> <_ApkTestProject Include="$(_TopDir)\tests\locales\Xamarin.Android.Locale-Tests\Xamarin.Android.Locale-Tests.csproj" /> <_ApkTestProject Include="$(_TopDir)\tests\BCL-Tests\Xamarin.Android.Bcl-Tests\Xamarin.Android.Bcl-Tests.csproj" /> <_ApkTestProject Include="$(_TopDir)\tests\Xamarin.Forms-Performance-Integration\Droid\Xamarin.Forms.Performance.Integration.Droid.csproj" /> + <_ApkTestProject Include="$(_TopDir)\tests\EmbeddedDSOs\EmbeddedDSO\EmbeddedDSO.csproj" /> <_ApkTestProjectAot Include="$(_TopDir)\src\Mono.Android\Test\Mono.Android-Tests.csproj" /> <_ApkTestProjectBundle Include="$(_TopDir)\src\Mono.Android\Test\Mono.Android-Tests.csproj" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AndroidZipAlign.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AndroidZipAlign.cs index 57726552c35..54d15e9e7e6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AndroidZipAlign.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AndroidZipAlign.cs @@ -36,7 +36,7 @@ protected override string GenerateCommandLineCommands () string sourceFilename = Path.GetFileNameWithoutExtension (Source.ItemSpec); if (sourceFilename.EndsWith (strSignedUnaligned)) sourceFilename = sourceFilename.Remove (sourceFilename.Length - strSignedUnaligned.Length); - return string.Format ("{0} \"{1}\" \"{2}{3}{4}-Signed.apk\"", + return string.Format ("-p {0} \"{1}\" \"{2}{3}{4}-Signed.apk\"", Alignment, Source.ItemSpec, DestinationDirectory.ItemSpec, Path.DirectorySeparatorChar, sourceFilename); } diff --git a/src/monodroid/jni/dylib-mono.c b/src/monodroid/jni/dylib-mono.c index 67dd46b566d..871e00f6730 100644 --- a/src/monodroid/jni/dylib-mono.c +++ b/src/monodroid/jni/dylib-mono.c @@ -33,25 +33,18 @@ void monodroid_dylib_mono_free (struct DylibMono *mono_imports) free (mono_imports); } -int monodroid_dylib_mono_init (struct DylibMono *mono_imports, const char *libmono_path) +int monodroid_dylib_mono_init (struct DylibMono *mono_imports, void *libmono_handle) { int symbols_missing = FALSE; if (mono_imports == NULL) return FALSE; - memset (mono_imports, 0, sizeof (*mono_imports)); - - /* - * We need to use RTLD_GLOBAL so that libmono-profiler-log.so can resolve - * symbols against the Mono library we're loading. - */ - mono_imports->dl_handle = dlopen (libmono_path, RTLD_LAZY | RTLD_GLOBAL); - - if (!mono_imports->dl_handle) { + if (libmono_handle == NULL) return FALSE; - } + memset (mono_imports, 0, sizeof (*mono_imports)); + mono_imports->dl_handle = libmono_handle; mono_imports->version = sizeof (*mono_imports); log_info (LOG_DEFAULT, "Loading Mono symbols..."); diff --git a/src/monodroid/jni/dylib-mono.h b/src/monodroid/jni/dylib-mono.h index 9d95e4fb7a3..f32d82c6623 100644 --- a/src/monodroid/jni/dylib-mono.h +++ b/src/monodroid/jni/dylib-mono.h @@ -348,6 +348,6 @@ struct DylibMono { MONO_API struct DylibMono* monodroid_dylib_mono_new (const char *libmono_path); MONO_API void monodroid_dylib_mono_free (struct DylibMono *mono_imports); - int monodroid_dylib_mono_init (struct DylibMono *mono_imports, const char *libmono_path); + int monodroid_dylib_mono_init (struct DylibMono *mono_imports, void *libmono_handle); #endif /* INC_MONODROID_DYLIB_MONO_H */ diff --git a/src/monodroid/jni/monodroid-glue.c b/src/monodroid/jni/monodroid-glue.c index 229376cad94..aaefe486e60 100644 --- a/src/monodroid/jni/monodroid-glue.c +++ b/src/monodroid/jni/monodroid-glue.c @@ -72,6 +72,7 @@ #include "ioapi.h" #include "monodroid-glue.h" #include "mkbundle-api.h" +#include "cpu-arch.h" #ifndef WINDOWS #include "xamarin_getifaddrs.h" @@ -100,6 +101,22 @@ static int wait_for_gdb; static volatile int monodroid_gdb_wait = TRUE; static int android_api_level = 0; +// Values correspond to the CPU_KIND_* macros +static const char* android_abi_names[CPU_KIND_X86_64+1] = { + "unknown", + [CPU_KIND_ARM] = "armeabi-v7a", + [CPU_KIND_ARM64] = "arm64-v8a", + [CPU_KIND_MIPS] = "mips", + [CPU_KIND_X86] = "x86", + [CPU_KIND_X86_64] = "x86_64", +}; +#define ANDROID_ABI_NAMES_SIZE (sizeof(android_abi_names) / sizeof (android_abi_names[0])) + +static void* load_dso (const char *path, int dl_flags, mono_bool skip_exists_check); +static void* load_dso_from_app_lib_dirs (const char *name, int dl_flags); +static void* load_dso_from_override_dirs (const char *name, int dl_flags); +static void* load_dso_from_any_directories (const char *name, int dl_flags); + /* Can be called by a native debugger to break the wait on startup */ MONO_API void monodroid_clear_gdb_wait (void) @@ -467,7 +484,9 @@ monodroid_get_dylib (void) return &mono; } -static const char *app_libdir; +static const char **app_lib_directories; +static size_t app_lib_directories_size = 0; +static int embedded_dso_mode = 0; int file_copy(const char *to, const char *from) { @@ -639,6 +658,7 @@ static char* get_libmonosgen_path () { char *libmonoso; + int i; #ifndef RELEASE // Android 5 includes some restrictions on loading dynamic libraries via dlopen() from @@ -647,11 +667,16 @@ get_libmonosgen_path () copy_monosgen_to_internal_location (primary_override_dir, external_override_dir); copy_monosgen_to_internal_location (primary_override_dir, external_legacy_override_dir); - int i; - for (i = 0; i < MAX_OVERRIDES; ++i) - TRY_LIBMONOSGEN (override_dirs [i]) + if (!embedded_dso_mode) { + for (i = 0; i < MAX_OVERRIDES; ++i) + TRY_LIBMONOSGEN (override_dirs [i]); + } #endif - TRY_LIBMONOSGEN (app_libdir) + if (!embedded_dso_mode) { + for (i = 0; i < app_lib_directories_size; i++) { + TRY_LIBMONOSGEN (app_lib_directories [i]); + } + } libmonoso = runtime_libdir ? monodroid_strdup_printf ("%s" MONODROID_PATH_SEPARATOR MONO_SGEN_ARCH_SO, runtime_libdir, sizeof(void*) == 8 ? "64bit" : "32bit") : NULL; if (libmonoso && file_exists (libmonoso)) { @@ -686,13 +711,22 @@ get_libmonosgen_path () TRY_LIBMONOSGEN (get_libmonoandroid_directory_path ()) #endif - TRY_LIBMONOSGEN (SYSTEM_LIB_PATH) - -#ifdef RELEASE - log_fatal (LOG_DEFAULT, "cannot find libmonosgen-2.0.so in app_libdir: %s nor in previously printed locations.", app_libdir); -#else - log_fatal (LOG_DEFAULT, "cannot find libmonosgen-2.0.so in override_dir: %s, app_libdir: %s nor in previously printed locations.", override_dirs[0], app_libdir); + TRY_LIBMONOSGEN (SYSTEM_LIB_PATH); + log_fatal (LOG_DEFAULT, "Cannot find '%s'. Looked in the following locations:", MONO_SGEN_SO); + +#ifndef RELEASE + if (!embedded_dso_mode) { + for (i = 0; i < MAX_OVERRIDES; ++i) { + if (override_dirs [i] == NULL) + continue; + log_fatal (LOG_DEFAULT, " %s", override_dirs [i]); + } + } #endif + for (i = 0; i < app_lib_directories_size; i++) { + log_fatal (LOG_DEFAULT, " %s", app_lib_directories [i]); + } + log_fatal (LOG_DEFAULT, "Do you have a shared runtime build of your app with AndroidManifest.xml android:minSdkVersion < 10 while running on a 64-bit Android 5.0 target? This combination is not supported."); log_fatal (LOG_DEFAULT, "Please either set android:minSdkVersion >= 10 or use a build without the shared runtime (like default Release configuration)."); exit (FATAL_EXIT_CANNOT_FIND_LIBMONOSGEN); @@ -704,53 +738,185 @@ typedef void* (*mono_mkbundle_init_ptr) (void (*)(const MonoBundledAssembly **), mono_mkbundle_init_ptr mono_mkbundle_init; void (*mono_mkbundle_initialize_mono_api) (const BundleMonoAPI *info); -static void -setup_bundled_app (const char *libappso) +static char* +get_full_dso_path (const char *base_dir, const char *dso_path, mono_bool *needs_free) { - void *libapp; + assert (needs_free); + + *needs_free = FALSE; + if (!dso_path) + return NULL; - libapp = dlopen (libappso, RTLD_LAZY); + if (base_dir == NULL || is_path_rooted (dso_path)) + return (char*)dso_path; // Absolute path or no base path, can't do much with it - if (!libapp) { - log_fatal (LOG_BUNDLE, "bundled app initialization error: %s", dlerror ()); - exit (FATAL_EXIT_CANNOT_LOAD_BUNDLE); + char *full_path = path_combine (base_dir, dso_path); + *needs_free = TRUE; + return full_path; +} + +static void* +load_dso (const char *path, int dl_flags, mono_bool skip_exists_check) +{ + if (path == NULL) + return NULL; + + log_info (LOG_ASSEMBLY, "Trying to load shared library '%s'", path); + if (!skip_exists_check && !embedded_dso_mode && !file_exists (path)) { + log_info (LOG_ASSEMBLY, "Shared library '%s' not found", path); + return NULL; } - mono_mkbundle_initialize_mono_api = dlsym (libapp, "initialize_mono_api"); - if (!mono_mkbundle_initialize_mono_api) - log_error (LOG_BUNDLE, "Missing initialize_mono_api in the application"); + void *handle = dlopen (path, dl_flags); + if (handle == NULL) + log_info (LOG_ASSEMBLY, "Failed to load shared library '%s'. %s", path, dlerror ()); + return handle; +} - mono_mkbundle_init = dlsym (libapp, "mono_mkbundle_init"); - if (!mono_mkbundle_init) - log_error (LOG_BUNDLE, "Missing mono_mkbundle_init in the application"); - log_info (LOG_BUNDLE, "Bundled app loaded: %s", libappso); +static void* +load_dso_from_specified_dirs (const char **directories, int num_entries, const char *dso_name, int dl_flags) +{ + assert (directories); + if (dso_name == NULL) + return NULL; + + mono_bool needs_free = FALSE; + char *full_path = NULL; + for (int i = 0; i < num_entries; i++) { + full_path = get_full_dso_path (directories [i], dso_name, &needs_free); + void *handle = load_dso (full_path, dl_flags, FALSE); + if (needs_free) + free (full_path); + if (handle != NULL) + return handle; + } + + return NULL; +} + +static void* +load_dso_from_app_lib_dirs (const char *name, int dl_flags) +{ + return load_dso_from_specified_dirs (app_lib_directories, app_lib_directories_size, name, dl_flags); +} + +static void* +load_dso_from_override_dirs (const char *name, int dl_flags) +{ +#ifdef RELEASE + return NULL; +#else + return load_dso_from_specified_dirs (override_dirs, MAX_OVERRIDES, name, dl_flags); +#endif +} + +static void* load_dso_from_any_directories (const char *name, int dl_flags) +{ + void *handle = load_dso_from_override_dirs (name, dl_flags); + if (handle == NULL) + handle = load_dso_from_app_lib_dirs (name, dl_flags); + return handle; } static char* -get_bundled_app (JNIEnv *env, jstring dir) +get_existing_dso_path_on_disk (const char *base_dir, const char *dso_name, mono_bool *needs_free) { - const char *v; - char *libapp; + assert (needs_free); -#ifndef RELEASE - libapp = path_combine (override_dirs [0], "libmonodroid_bundle_app.so"); + *needs_free = FALSE; + char *dso_path = get_full_dso_path (base_dir, dso_name, needs_free); + if (file_exists (dso_path)) + return dso_path; - if (file_exists (libapp)) - return libapp; + *needs_free = FALSE; + free (dso_path); + return NULL; +} - free (libapp); + +static void +dso_alloc_cleanup (char **dso_path, mono_bool *needs_free) +{ + assert (needs_free); + if (dso_path != NULL) { + if (*needs_free) + free (*dso_path); + *dso_path = NULL; + } + *needs_free = FALSE; +} + +static char* +get_full_dso_path_on_disk (const char *dso_name, mono_bool *needs_free) +{ + assert (needs_free); + + *needs_free = FALSE; + if (embedded_dso_mode) + return NULL; +#ifndef RELEASE + char *dso_path = NULL; + for (int i = 0; i < MAX_OVERRIDES; i++) { + if (override_dirs [i] == NULL) + continue; + dso_path = get_existing_dso_path_on_disk (override_dirs [i], dso_name, needs_free); + if (dso_path != NULL) + return dso_path; + dso_alloc_cleanup (&dso_path, needs_free); + } #endif + for (int i = 0; i < app_lib_directories_size; i++) { + dso_path = get_existing_dso_path_on_disk (app_lib_directories [i], dso_name, needs_free); + if (dso_path != NULL) + return dso_path; + dso_alloc_cleanup (&dso_path, needs_free); + } - if (dir) { - v = (*env)->GetStringUTFChars (env, dir, NULL); - if (v) { - libapp = path_combine (v, "libmonodroid_bundle_app.so"); - (*env)->ReleaseStringUTFChars (env, dir, v); - if (file_exists (libapp)) - return libapp; + return NULL; +} + +// This function could be improved if we somehow marked an apk containing just the bundled app as +// such - perhaps another __XA* environment variable? Would certainly make code faster. +static void +setup_bundled_app (const char *dso_name) +{ + static int dlopen_flags = RTLD_LAZY; + void *libapp = NULL; + + if (embedded_dso_mode) { + log_info (LOG_DEFAULT, "bundle app: embedded DSO mode"); + libapp = load_dso_from_any_directories (dso_name, dlopen_flags); + } else { + mono_bool needs_free = FALSE; + log_info (LOG_DEFAULT, "bundle app: normal mode"); + char *bundle_path = get_full_dso_path_on_disk (dso_name, &needs_free); + log_info (LOG_DEFAULT, "bundle_path == %s", bundle_path ? bundle_path : ""); + if (bundle_path == NULL) + return; + log_info (LOG_BUNDLE, "Attempting to load bundled app from %s", bundle_path); + libapp = load_dso (bundle_path, dlopen_flags, TRUE); + free (bundle_path); + } + + if (libapp == NULL) { + log_info (LOG_DEFAULT, "No libapp!"); + if (!embedded_dso_mode) { + log_fatal (LOG_BUNDLE, "bundled app initialization error"); + exit (FATAL_EXIT_CANNOT_LOAD_BUNDLE); + } else { + log_info (LOG_BUNDLE, "bundled app not found in the APK, ignoring."); + return; } } - return NULL; + + mono_mkbundle_initialize_mono_api = dlsym (libapp, "initialize_mono_api"); + if (!mono_mkbundle_initialize_mono_api) + log_error (LOG_BUNDLE, "Missing initialize_mono_api in the application"); + + mono_mkbundle_init = dlsym (libapp, "mono_mkbundle_init"); + if (!mono_mkbundle_init) + log_error (LOG_BUNDLE, "Missing mono_mkbundle_init in the application"); + log_info (LOG_BUNDLE, "Bundled app loaded: %s", dso_name); } static JavaVM *jvm; @@ -2922,7 +3088,6 @@ init_android_runtime (MonoDomain *domain, JNIEnv *env, jobject loader) void *args [1]; args [0] = &init; - android_api_level = GetAndroidSdkVersion (env, loader); init.javaVm = jvm; init.env = env; init.logCategories = log_categories; @@ -3098,48 +3263,51 @@ convert_dl_flags (int flags) static void* monodroid_dlopen (const char *name, int flags, char **err, void *user_data) { + int dl_flags = convert_dl_flags (flags); + void *h = NULL; + char *full_name = NULL; + char *basename = NULL; + mono_bool libmonodroid_fallback = FALSE; + /* name is NULL when we're P/Invoking __Internal, so remap to libmonodroid */ - char *full_name = path_combine (app_libdir, name ? name : "libmonodroid.so"); - if (!name && !file_exists (full_name)) { - log_info (LOG_ASSEMBLY, "Trying to load library '%s'", full_name); - free (full_name); - full_name = path_combine (SYSTEM_LIB_PATH, "libmonodroid.so"); + if (name == NULL) { + name = "libmonodroid.so"; + libmonodroid_fallback = TRUE; } - int dl_flags = convert_dl_flags (flags); - void *h = dlopen (full_name, dl_flags); - log_info (LOG_ASSEMBLY, "Trying to load library '%s'", full_name); - if (!h && name && (strstr (name, ".dll.so") || strstr (name, ".exe.so"))) { - char *full_name2; - const char *basename; + h = load_dso_from_any_directories (name, dl_flags); + if (h != NULL) { + goto done_and_out; + } - if (strrchr (name, '/')) - basename = strrchr (name, '/') + 1; - else - basename = name; + if (libmonodroid_fallback) { + full_name = path_combine (SYSTEM_LIB_PATH, "libmonodroid.so"); + h = load_dso (full_name, dl_flags, FALSE); + goto done_and_out; + } - /* Try loading AOT modules from the override dir */ - if (override_dirs [0]) { - full_name2 = monodroid_strdup_printf ("%s" MONODROID_PATH_SEPARATOR "libaot-%s", override_dirs [0], basename); - h = dlopen (full_name2, dl_flags); - free (full_name2); - } + if (!strstr (name, ".dll.so") && !strstr (name, ".exe.so")) { + goto done_and_out; + } - /* Try loading AOT modules from the lib dir */ - if (!h) { - full_name2 = monodroid_strdup_printf ("%s" MONODROID_PATH_SEPARATOR "libaot-%s", app_libdir, basename); - h = dlopen (full_name2, dl_flags); - free (full_name2); - } + basename = strrchr (name, '/'); + if (basename != NULL) + basename++; + else + basename = (char*)name; - if (h) - log_info (LOG_ASSEMBLY, "Loaded AOT image '%s'", full_name2); - } + basename = monodroid_strdup_printf ("libaot-%s", basename); + h = load_dso_from_any_directories (basename, dl_flags); + + if (h != NULL) + log_info (LOG_ASSEMBLY, "Loaded AOT image '%s'", basename); + done_and_out: if (!h && err) { *err = monodroid_strdup_printf ("Could not load library: Library '%s' not found.", full_name); } + free (basename); free (full_name); return h; @@ -3272,57 +3440,18 @@ load_profiler (void *handle, const char *desc, const char *symbol) } static mono_bool -load_embedded_profiler (const char *desc, const char *name) +load_profiler_from_handle (void *dso_handle, const char *desc, const char *name) { - mono_bool result; - - char *full_name = path_combine (app_libdir, "libmonodroid.so"); - void *h = dlopen (full_name, RTLD_LAZY); - const char *e = dlerror (); - - log_warn (LOG_DEFAULT, "looking for embedded profiler within '%s': dlopen=%p error=%s", - full_name, - h, - h != NULL ? "" : e); - - free (full_name); - - if (!h) { - return 0; - } + if (!dso_handle) + return FALSE; char *symbol = monodroid_strdup_printf ("%s_%s", INITIALIZER_NAME, name); - if (!(result = load_profiler (h, desc, symbol))) - dlclose (h); + mono_bool result = load_profiler (dso_handle, desc, symbol); free (symbol); - - return result; -} - -static mono_bool -load_profiler_from_directory (const char *directory, const char *libname, const char *desc, const char *name) -{ - char *full_name = path_combine (directory, libname); - int exists = file_exists (full_name); - void *h = exists ? dlopen (full_name, RTLD_LAZY) : NULL; - const char *e = exists ? dlerror () : "No such file or directory"; - - log_warn (LOG_DEFAULT, "Trying to load profiler: %s: dlopen=%p error=%s", - full_name, - h, - h != NULL ? "" : e); - - free (full_name); - - if (h) { - char *symbol = monodroid_strdup_printf ("%s_%s", INITIALIZER_NAME, name); - mono_bool result = load_profiler (h, desc, symbol); - free (symbol); - if (result) - return 1; - dlclose (h); - } - return 0; + if (result) + return TRUE; + dlclose (dso_handle); + return FALSE; } static void @@ -3330,7 +3459,6 @@ monodroid_profiler_load (const char *libmono_path, const char *desc, const char { const char* col = strchr (desc, ':'); char *mname; - int oi; if (col != NULL) { mname = xmalloc (col - desc + 1); @@ -3340,29 +3468,19 @@ monodroid_profiler_load (const char *libmono_path, const char *desc, const char mname = monodroid_strdup_printf ("%s", desc); } + int dlopen_flags = RTLD_LAZY; char *libname = monodroid_strdup_printf ("libmono-profiler-%s.so", mname); - mono_bool found = 0; - - for (oi = 0; oi < MAX_OVERRIDES; ++oi) { - if (!directory_exists (override_dirs [oi])) - continue; - if ((found = load_profiler_from_directory (override_dirs [oi], libname, desc, mname))) - break; + void *handle = load_dso_from_any_directories (libname, dlopen_flags); + found = load_profiler_from_handle (handle, desc, mname); + + if (!found && libmono_path != NULL) { + char *full_path = path_combine (libmono_path, libname); + handle = load_dso (full_path, dlopen_flags, FALSE); + free (full_path); + found = load_profiler_from_handle (handle, desc, mname); } - do { - if (found) - break; - if ((found = load_profiler_from_directory (app_libdir, libname, desc, mname))) - break; - if ((found = load_embedded_profiler (desc, mname))) - break; - if (libmono_path != NULL && (found = load_profiler_from_directory (libmono_path, libname, desc, mname))) - break; - } while (0); - - if (found && logfile != NULL) set_world_accessable (logfile); @@ -3468,7 +3586,7 @@ setup_environment_from_line (const char *line) } static void -setup_environment_from_file (const char *apk, int index, int apk_count) +setup_environment_from_file (const char *apk, int index, int apk_count, void *user_data) { unzFile file; if ((file = unzOpen (apk)) == NULL) @@ -3510,7 +3628,7 @@ setup_environment_from_file (const char *apk, int index, int apk_count) } static void -for_each_apk (JNIEnv *env, jobjectArray runtimeApks, void (*handler) (const char *apk, int index, int apk_count)) +for_each_apk (JNIEnv *env, jobjectArray runtimeApks, void (*handler) (const char *apk, int index, int apk_count, void *user_data), void *user_data) { int i; jsize apksLength = (*env)->GetArrayLength (env, runtimeApks); @@ -3518,7 +3636,7 @@ for_each_apk (JNIEnv *env, jobjectArray runtimeApks, void (*handler) (const char jstring e = (*env)->GetObjectArrayElement (env, runtimeApks, i); const char *apk = (*env)->GetStringUTFChars (env, e, NULL); - handler (apk, i, apksLength); + handler (apk, i, apksLength, user_data); (*env)->ReleaseStringUTFChars (env, e, apk); } } @@ -3526,11 +3644,11 @@ for_each_apk (JNIEnv *env, jobjectArray runtimeApks, void (*handler) (const char static void setup_environment (JNIEnv *env, jobjectArray runtimeApks) { - for_each_apk (env, runtimeApks, setup_environment_from_file); + for_each_apk (env, runtimeApks, setup_environment_from_file, NULL); } static void -setup_process_args_apk (const char *apk, int index, int apk_count) +setup_process_args_apk (const char *apk, int index, int apk_count, void *user_data) { if (!apk || index != apk_count - 1) return; @@ -3542,7 +3660,7 @@ setup_process_args_apk (const char *apk, int index, int apk_count) static void setup_process_args (JNIEnv *env, jobjectArray runtimeApks) { - for_each_apk (env, runtimeApks, setup_process_args_apk); + for_each_apk (env, runtimeApks, setup_process_args_apk, NULL); } /* @@ -3688,7 +3806,7 @@ This is a hack to set llvm::DisablePrettyStackTrace to true and avoid this sourc static void disable_external_signal_handlers (void) { - void *llvm = dlopen ("libLLVM.so", RTLD_LAZY); + void *llvm = load_dso ("libLLVM.so", RTLD_LAZY, TRUE); if (llvm) { _Bool *disable_signals = dlsym (llvm, "_ZN4llvm23DisablePrettyStackTraceE"); if (disable_signals) { @@ -3742,6 +3860,14 @@ create_and_initialize_domain (JNIEnv* env, jobjectArray runtimeApks, jobjectArra return domain; } +static void +add_apk_libdir (const char *apk, int index, int apk_count, void *user_data) +{ + assert (user_data); + assert (index >= 0 && index < app_lib_directories_size); + app_lib_directories [index] = monodroid_strdup_printf ("%s!/lib/%s", apk, (const char*)user_data); +} + JNIEXPORT void JNICALL Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobjectArray runtimeApks, jstring runtimeNativeLibDir, jobjectArray appDirs, jobject loader, jobjectArray externalStorageDirs, jobjectArray assemblies, jstring packageName) { @@ -3749,13 +3875,13 @@ Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobject char *connect_args; jstring libdir_s; const char *libdir, *esd; - char *libmonosgen_path; - char *libmonodroid_bundle_app_path; char *counters_path; const char *pkgName; char *aotMode; int i; + android_api_level = GetAndroidSdkVersion (env, loader); + pkgName = (*env)->GetStringUTFChars (env, packageName, NULL); monodroid_store_package_name (pkgName); /* Will make a copy of the string */ (*env)->ReleaseStringUTFChars (env, packageName, pkgName); @@ -3772,6 +3898,26 @@ Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobject setup_environment (env, runtimeApks); + if (android_api_level < 23 || getenv ("__XA_DSO_IN_APK") == NULL) { + log_info (LOG_DEFAULT, "Setting up for DSO lookup in app data directories"); + libdir_s = (*env)->GetObjectArrayElement (env, appDirs, 2); + libdir = (*env)->GetStringUTFChars (env, libdir_s, NULL); + app_lib_directories_size = 1; + app_lib_directories = (const char**) xcalloc (app_lib_directories_size, sizeof(char*)); + app_lib_directories [0] = monodroid_strdup_printf ("%s", libdir); + (*env)->ReleaseStringUTFChars (env, libdir_s, libdir); + } else { + log_info (LOG_DEFAULT, "Setting up for DSO lookup directly in the APK"); + embedded_dso_mode = 1; + app_lib_directories_size = (*env)->GetArrayLength (env, runtimeApks); + app_lib_directories = (const char**) xcalloc (app_lib_directories_size, sizeof(char*)); + + unsigned short built_for_cpu = 0, running_on_cpu = 0; + unsigned char is64bit = 0; + _monodroid_detect_cpu_and_architecture (&built_for_cpu, &running_on_cpu, &is64bit); + for_each_apk (env, runtimeApks, add_apk_libdir, android_abi_names [running_on_cpu]); + } + primary_override_dir = get_primary_override_dir (env, (*env)->GetObjectArrayElement (env, appDirs, 0)); esd = (*env)->GetStringUTFChars (env, (*env)->GetObjectArrayElement (env, externalStorageDirs, 0), NULL); external_override_dir = monodroid_strdup_printf ("%s", esd); @@ -3799,23 +3945,7 @@ Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobject log_warn (LOG_DEFAULT, "Using override path: %s", p); } #endif - - jsize appDirsLength = (*env)->GetArrayLength (env, appDirs); - - for (i = 0; i < appDirsLength; ++i) { - jstring appDir = (*env)->GetObjectArrayElement (env, appDirs, i); - libmonodroid_bundle_app_path = get_bundled_app (env, appDir); - if (libmonodroid_bundle_app_path) { - setup_bundled_app (libmonodroid_bundle_app_path); - free (libmonodroid_bundle_app_path); - break; - } - } - - libdir_s = (*env)->GetObjectArrayElement (env, appDirs, 2); - libdir = (*env)->GetStringUTFChars (env, libdir_s, NULL); - app_libdir = monodroid_strdup_printf ("%s", libdir); - (*env)->ReleaseStringUTFChars (env, libdir_s, libdir); + setup_bundled_app ("libmonodroid_bundle_app.so"); if (runtimeNativeLibDir != NULL) { const char *rd; @@ -3824,14 +3954,25 @@ Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobject (*env)->ReleaseStringUTFChars (env, runtimeNativeLibDir, rd); } - libmonosgen_path = get_libmonosgen_path (); - if (!monodroid_dylib_mono_init (&mono, libmonosgen_path)) { + void *libmonosgen_handle = NULL; + + /* + * We need to use RTLD_GLOBAL so that libmono-profiler-log.so can resolve + * symbols against the Mono library we're loading. + */ + int sgen_dlopen_flags = RTLD_LAZY | RTLD_GLOBAL; + if (embedded_dso_mode) { + libmonosgen_handle = load_dso_from_any_directories (MONO_SGEN_SO, sgen_dlopen_flags); + } + + if (libmonosgen_handle == NULL) + libmonosgen_handle = load_dso (get_libmonosgen_path (), sgen_dlopen_flags, FALSE); + + if (!monodroid_dylib_mono_init (&mono, libmonosgen_handle)) { log_fatal (LOG_DEFAULT, "shared runtime initialization error: %s", dlerror ()); exit (FATAL_EXIT_CANNOT_FIND_MONO); } setup_process_args (env, runtimeApks); - - free (libmonosgen_path); #ifndef WINDOWS _monodroid_getifaddrs_init (); #endif diff --git a/src/monodroid/jni/util.c b/src/monodroid/jni/util.c index 049adcde107..b0a199e2035 100644 --- a/src/monodroid/jni/util.c +++ b/src/monodroid/jni/util.c @@ -14,6 +14,7 @@ #ifdef WINDOWS #include +#include #endif #include "java-interop-util.h" @@ -500,3 +501,18 @@ monodroid_dirent_hasextension (monodroid_dirent_t *e, const char *extension) return result; #endif } + +mono_bool +is_path_rooted (const char *path) +{ + if (path == NULL) + return FALSE; +#ifdef WINDOWS + LPCWSTR wpath = utf8_to_utf16 (path); + BOOL ret = !PathIsRelativeW (wpath); + free (wpath); + return ret; +#else + return path [0] == MONODROID_PATH_SEPARATOR_CHAR; +#endif +} diff --git a/src/monodroid/jni/util.h b/src/monodroid/jni/util.h index aac332abdfa..79b0091d345 100644 --- a/src/monodroid/jni/util.h +++ b/src/monodroid/jni/util.h @@ -10,9 +10,11 @@ #endif #if WINDOWS -#define MONODROID_PATH_SEPARATOR "\\" +#define MONODROID_PATH_SEPARATOR "\\" +#define MONODROID_PATH_SEPARATOR_CHAR '\\' #else -#define MONODROID_PATH_SEPARATOR "/" +#define MONODROID_PATH_SEPARATOR "/" +#define MONODROID_PATH_SEPARATOR_CHAR '/' #endif #if WINDOWS @@ -63,5 +65,6 @@ void *monodroid_runtime_invoke (struct DylibMono *mono, MonoDomain *d MonoClass *monodroid_get_class_from_name (struct DylibMono *mono, MonoDomain *domain, const char* assembly, const char *namespace, const char *type); MonoDomain *monodroid_create_appdomain (struct DylibMono *mono, MonoDomain *parent_domain, const char *friendly_name, int shadow_copy, const char *shadow_directories); MonoClass *monodroid_get_class_from_image (struct DylibMono *mono, MonoDomain *domain, MonoImage* image, const char *namespace, const char *type); +mono_bool is_path_rooted (const char *path); #endif /* __MONODROID_UTIL_H__ */ diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/BuildTests.cs b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/BuildTests.cs new file mode 100644 index 00000000000..19050a11295 --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/BuildTests.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml; +using System.Xml.XPath; + +using Microsoft.Build.Framework; +using NUnit.Framework; +using Xamarin.ProjectTools; +using Xamarin.Tools.Zip; + +using XABuildPaths = global::Xamarin.Android.Build.Paths; + +namespace EmbeddedDSOUnitTests +{ + sealed class LocalBuilder : Builder + { + public bool Build (string projectOrSolution, string target, string[] parameters = null, Dictionary environmentVariables = null) + { + return BuildInternal (projectOrSolution, target, parameters, environmentVariables); + } + } + + [Parallelizable (ParallelScope.Children)] + public class BuildTests_EmbeddedDSOBuildTests + { + const string ProjectName = "EmbeddedDSO"; + const string ProjectAssemblyName = "Xamarin.Android.EmbeddedDSO_Test"; + + static readonly string TestProjectRootDirectory; + static readonly string TestOutputDir; + + static readonly List produced_binaries = new List { + $"{ProjectAssemblyName}.dll", + $"{ProjectAssemblyName}-Signed.apk", + $"{ProjectAssemblyName}.apk", + }; + + static readonly List log_files = new List { + "process.log", + "msbuild.binlog", + }; + + string testProjectPath; + + static BuildTests_EmbeddedDSOBuildTests () + { + TestProjectRootDirectory = Path.GetFullPath (Path.Combine (XABuildPaths.TopDirectory, "tests", "EmbeddedDSOs", "EmbeddedDSO")); + TestOutputDir = Path.Combine (XABuildPaths.TestOutputDirectory, "EmbeddedDSO"); + + produced_binaries = new List { + $"{ProjectAssemblyName}.dll", + $"{ProjectAssemblyName}-Signed.apk", + $"{ProjectAssemblyName}.apk", + }; + } + + [TestFixtureSetUp] + public void BuildProject () + { + testProjectPath = PrepareProject (ProjectName); + string projectPath = Path.Combine (testProjectPath, $"{ProjectName}.csproj"); + LocalBuilder builder = GetBuilder ("EmbeddedDSO"); + bool success = builder.Build (projectPath, "SignAndroidPackage", new [] { "UnitTestsMode=true" }); + + Assert.That (success, Is.True, "Should have been built"); + } + + [Test] + public void BinariesExist () + { + foreach (string binary in produced_binaries) { + string fp = Path.Combine (testProjectPath, "bin", XABuildPaths.Configuration, binary); + + Assert.That (new FileInfo (fp), Does.Exist, $"File {fp} should exist"); + } + } + + [Test] + public void DSOPageAlignment () + { + Assert.That (new FileInfo (Config.ZipAlignPath), Does.Exist, $"ZipAlign not found at ${Config.ZipAlignPath}"); + + string apk = Path.Combine (testProjectPath, "bin", XABuildPaths.Configuration, $"{ProjectAssemblyName}-Signed.apk"); + Assert.That (new FileInfo (apk), Does.Exist, $"File {apk} should exist"); + Assert.That (RunCommand (Config.ZipAlignPath, $"-c -v -p 4 {apk}"), Is.True, $"{ProjectAssemblyName}-Signed.apk does not contain page-aligned .so files"); + } + + [Test] + public void EnvironmentFileContents () + { + string apk = Path.Combine (testProjectPath, "bin", XABuildPaths.Configuration, $"{ProjectAssemblyName}-Signed.apk"); + Assert.That (new FileInfo (apk), Does.Exist, $"File {apk} should exist"); + + using (ZipArchive zip = ZipArchive.Open (apk, FileMode.Open)) { + Assert.That (zip, Is.Not.Null, $"{apk} couldn't be opened as a zip archive"); + Assert.That (zip.ContainsEntry ("environment"), Is.True, $"`environment` file not found in {apk}"); + + ZipEntry entry = zip.FirstOrDefault (e => String.Compare (e.FullName, "environment", StringComparison.Ordinal) == 0); + Assert.That (entry, Is.Not.Null, $"Unable to open the `environment` entry from {apk}"); + + string environment = null; + using (var ms = new MemoryStream ()) { + entry.Extract (ms); + environment = Encoding.UTF8.GetString (ms.ToArray ()); + } + + Assert.That (String.IsNullOrEmpty (environment), Is.False, $"Environment file from {apk} must not be empty"); + + string[] envLines = environment.Split (new [] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That (envLines.Length > 0, Is.True, $"Environment file from {apk} must contain at least one non-empty line"); + + bool found = false; + foreach (string line in envLines) { + string[] ev = line.Split ('='); + if (ev.Length != 2) + continue; + + if (String.Compare ("__XA_DSO_IN_APK", ev [0].Trim (), StringComparison.Ordinal) == 0) { + found = true; + break; + } + } + + Assert.That (found, Is.True, $"The `__XA_DSO_IN_APK` variable wasn't found in the environment file from {apk}"); + } + } + + [Test] + public void DSOCompressionMode () + { + string apk = Path.Combine (testProjectPath, "bin", XABuildPaths.Configuration, $"{ProjectAssemblyName}-Signed.apk"); + Assert.That (new FileInfo (apk), Does.Exist, $"File {apk} should exist"); + + var bad_dsos = new List (); + using (ZipArchive zip = ZipArchive.Open (apk, FileMode.Open)) { + Assert.That (zip, Is.Not.Null, $"{apk} couldn't be opened as a zip archive"); + + foreach (ZipEntry entry in zip) { + if (!entry.FullName.EndsWith (".so", StringComparison.Ordinal)) + continue; + + if (entry.CompressionMethod == CompressionMethod.Store) + continue; + + bad_dsos.Add (entry.FullName); + } + } + + Assert.That (bad_dsos.Count == 0, Is.True, $"Some DSO entries in {apk} are compressed ({BadDsosString ()})"); + + string BadDsosString () + { + return String.Join ("; ", bad_dsos); + } + } + + [Test] + public void AndroidManifestHasFlag () + { + const string AndroidNS = "http://schemas.android.com/apk/res/android"; + + string manifest = Path.Combine (testProjectPath, "obj", XABuildPaths.Configuration, "android", "manifest", "AndroidManifest.xml"); + Assert.That (new FileInfo (manifest), Does.Exist, $"File {manifest} should exist"); + + var doc = new XPathDocument (manifest); + XPathNavigator nav = doc.CreateNavigator (); + + var manager = new XmlNamespaceManager (nav.NameTable); + manager.AddNamespace ("android", AndroidNS); + + XPathNavigator application = nav.SelectSingleNode ("//manifest/application"); + Assert.That (application, Is.Not.Null, $"Manifest {manifest} does not contain the `application` node"); + + string attr = application.GetAttribute ("extractNativeLibs", AndroidNS)?.Trim (); + Assert.That (String.IsNullOrEmpty (attr), Is.False, $"Manifest {manifest} `application` node does not contain the `extractNativeLibs` attribute"); + Assert.That (String.Compare ("false", attr, StringComparison.OrdinalIgnoreCase), Is.EqualTo (0), $"Manifest {manifest} `application` node's `extractNativeLibs` attribute is not set to `false`"); + } + + bool RunCommand (string command, string arguments) + { + var psi = new ProcessStartInfo () { + FileName = command, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + + var stderr_completed = new ManualResetEvent (false); + var stdout_completed = new ManualResetEvent (false); + + var p = new Process () { + StartInfo = psi, + }; + + p.ErrorDataReceived += (sender, e) => { + if (e.Data == null) + stderr_completed.Set (); + else + Console.WriteLine (e.Data); + }; + + p.OutputDataReceived += (sender, e) => { + if (e.Data == null) + stdout_completed.Set (); + else + Console.WriteLine (e.Data); + }; + + using (p) { + p.StartInfo = psi; + p.Start (); + p.BeginOutputReadLine (); + p.BeginErrorReadLine (); + + bool success = p.WaitForExit (60000); + + // We need to call the parameter-less WaitForExit only if any of the standard + // streams have been redirected (see + // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=netframework-4.7.2#System_Diagnostics_Process_WaitForExit) + // + p.WaitForExit (); + stderr_completed.WaitOne (TimeSpan.FromSeconds (60)); + stdout_completed.WaitOne (TimeSpan.FromSeconds (60)); + + if (!success || p.ExitCode != 0) { + Console.Error.WriteLine ($"Process `{command} {arguments}` exited with value {p.ExitCode}."); + return false; + } + + return true; + } + } + + string PrepareProject (string testName) + { + string tempRoot = Path.Combine (TestOutputDir, $"{testName}.build", XABuildPaths.Configuration); + string temporaryProjectPath = Path.Combine (tempRoot, "project"); + + var ignore = new HashSet { + Path.Combine (TestProjectRootDirectory, "bin"), + Path.Combine (TestProjectRootDirectory, "obj"), + }; + + CopyRecursively (TestProjectRootDirectory, temporaryProjectPath, ignore); + return temporaryProjectPath; + } + + void CopyRecursively (string fromDir, string toDir, HashSet ignoreDirs) + { + if (String.IsNullOrEmpty (fromDir)) + throw new ArgumentException ($"{nameof (fromDir)} is must have a non-empty value"); + if (String.IsNullOrEmpty (toDir)) + throw new ArgumentException ($"{nameof (toDir)} is must have a non-empty value"); + + if (ignoreDirs.Contains (fromDir)) + return; + + var fdi = new DirectoryInfo (fromDir); + if (!fdi.Exists) + throw new InvalidOperationException ($"Source directory '{fromDir}' does not exist"); + + if (Directory.Exists (toDir)) + Directory.Delete (toDir, true); + + foreach (FileSystemInfo fsi in fdi.EnumerateFileSystemInfos ("*", SearchOption.TopDirectoryOnly)) { + if (fsi is FileInfo finfo) + CopyFile (fsi.FullName, Path.Combine (toDir, finfo.Name)); + else + CopyRecursively (fsi.FullName, Path.Combine (toDir, fsi.Name), ignoreDirs); + } + } + + void CopyFile (string from, string to) + { + string dir = Path.GetDirectoryName (to); + if (!Directory.Exists (dir)) + Directory.CreateDirectory (dir); + File.Copy (from, to, true); + } + + LocalBuilder GetBuilder (string baseLogFileName) + { + return new LocalBuilder { + Verbosity = LoggerVerbosity.Diagnostic, + BuildLogFile = $"{baseLogFileName}.log" + }; + } + } +} diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Config.cs.in b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Config.cs.in new file mode 100644 index 00000000000..a2b23e66cd3 --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Config.cs.in @@ -0,0 +1,10 @@ +using System; +using System.IO; + +namespace EmbeddedDSOUnitTests +{ + public static class Config + { + public static readonly string ZipAlignPath = Path.Combine ("@ANDROID_SDK_DIRECTORY@", "build-tools", "@BUILD_TOOLS_FOLDER@", "zipalign@EXECUTABLE_EXTENSION@"); + } +} diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/EmbeddedDSO-UnitTests.csproj b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/EmbeddedDSO-UnitTests.csproj new file mode 100644 index 00000000000..fef6dacb7f1 --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/EmbeddedDSO-UnitTests.csproj @@ -0,0 +1,73 @@ + + + + Debug + AnyCPU + {8B5E63B7-8C18-4BA7-BAAB-A1955B257F5E} + Library + EmbeddedDSOUnitTests + EmbeddedDSOUnitTests + v4.7 + + + true + full + false + ..\..\..\bin\TestDebug\EmbeddedDSO + DEBUG; + prompt + 4 + + + true + ..\..\..\bin\TestRelease\EmbeddedDSO + prompt + 4 + + + + + + + + + + + ..\..\..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + + + + + + + XABuildPaths.cs + + + + + + + + + {2DD1EE75-6D8D-4653-A800-0A24367F7F38} + Xamarin.ProjectTools + + + {E248B2CA-303B-4645-ADDC-9D4459D550FD} + libZipSharp + + + + + + .exe + + + + + diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Properties/AssemblyInfo.cs b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..f95d796870d --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle ("EmbeddedDSO-UnitTests")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("Marek Habersack")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion ("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/packages.config b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/packages.config new file mode 100644 index 00000000000..d9c5868ddbf --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/run.sh b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/run.sh new file mode 100755 index 00000000000..7f928c5f9f8 --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO-UnitTests/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash -e +export MONO_ENV_OPTIONS="--debug" +export USE_MSBUILD=1 +export MSBUILD=msbuild +msbuild EmbeddedDSO-UnitTests.csproj +cd ../../../ +exec mono --debug packages/NUnit.ConsoleRunner.3.7.0/tools/nunit3-console.exe bin/TestDebug/EmbeddedDSO/EmbeddedDSOUnitTests.dll diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/Assets/AboutAssets.txt b/tests/EmbeddedDSOs/EmbeddedDSO/Assets/AboutAssets.txt new file mode 100644 index 00000000000..a9b0638eb1b --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with your package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj new file mode 100644 index 00000000000..dbcadd5f71e --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.csproj @@ -0,0 +1,118 @@ + + + + Debug + AnyCPU + {056ED976-618F-4A3E-910E-AA25230C2296} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + EmbeddedDSO + Xamarin.Android.EmbeddedDSO_Test + v8.1 + True + Resources\Resource.designer.cs + Resource + Properties\AndroidManifest.xml + Resources + Assets + true + false + armeabi-v7a;x86 + + + .so + + + + ..\..\..\..\..\.. + + + + ..\..\.. + + + + + true + full + false + $(RelativeRootPath)\bin\TestDebug + bin\Debug + DEBUG; + prompt + 4 + None + + + + true + pdbonly + true + $(RelativeRootPath)\bin\TestRelease + bin\Release + prompt + 4 + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3CC4E384-4985-4D93-A34C-73F69A379FA7} + TestRunner.Core + + + {CB2335CB-0050-4020-8A05-E9614EDAA05E} + TestRunner.NUnit + + + + + + + + + + + + + + + + + $(InstallDependsOnTargets); + _GrantPermissions + + + diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.projitems b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.projitems new file mode 100644 index 00000000000..f8c3205813f --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO/EmbeddedDSO.projitems @@ -0,0 +1,34 @@ + + + + <_PackageName>Xamarin.Android.EmbeddedDSO_Test + + + + + Xamarin.Android.EmbeddedDSO_Test + xamarin.android.embeddeddso_test.NUnitInstrumentation + $(MSBuildThisFileDirectory)..\..\..\build-tools\scripts\TimingDefinitions.txt + $(MSBuildThisFileDirectory)..\..\..\TestResult-Xamarin.Android.EmbeddedDSO_Test-times.csv + + + + + + Xamarin.Android.EmbeddedDSO_Test + $(OutputPath)TestResult-Xamarin.Android.EmbeddedDSO_Test.nunit.xml + + + + Xamarin.Android.EmbeddedDSO_Test + + + + Xamarin.Android.EmbeddedDSO_Test + + + + Xamarin.Android.EmbeddedDSO_Test + + + diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt b/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt new file mode 100644 index 00000000000..966b2ab27fb --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO/Environment.txt @@ -0,0 +1,5 @@ +debug.mono.debug=1 +MONO_LOG_LEVEL=debug +MONO_LOG_MASK=asm +MONO_XDEBUG=1 +__XA_DSO_IN_APK=1 diff --git a/tests/EmbeddedDSOs/EmbeddedDSO/MainActivity.cs b/tests/EmbeddedDSOs/EmbeddedDSO/MainActivity.cs new file mode 100644 index 00000000000..74538f5fa87 --- /dev/null +++ b/tests/EmbeddedDSOs/EmbeddedDSO/MainActivity.cs @@ -0,0 +1,25 @@ +using Android.App; +using Android.Widget; +using Android.OS; + +namespace EmbeddedDSO { + [Activity (Label = "EmbeddedDSO", MainLauncher = true, Icon = "@mipmap/icon")] + public class MainActivity : Activity { + int count = 1; + + protected override void OnCreate (Bundle savedInstanceState) + { + base.OnCreate (savedInstanceState); + + // Set our view from the "main" layout resource + SetContentView (Resource.Layout.Main); + + // Get our button from the layout resource, + // and attach an event to it + Button button = FindViewById