From 0fa0b905f65e57f5eb64dd556a07ead522930c8a Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 29 Apr 2020 19:34:29 +0300 Subject: [PATCH] [mono] Add Android sample and AndroidAppBuilder task (#35483) --- eng/testing/tests.targets | 51 +++++ .../Microsoft.XmlSerializer.Generator.Tests.csproj | 2 +- src/mono/mono.proj | 11 +- .../msbuild/AndroidAppBuilder/AndroidAppBuilder.cs | 66 ++++++ .../AndroidAppBuilder/AndroidAppBuilder.csproj | 24 +++ src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs | 223 +++++++++++++++++++++ .../Templates/AndroidManifest.xml | 21 ++ .../Templates/CMakeLists-android.txt | 16 ++ .../AndroidAppBuilder/Templates/MainActivity.java | 22 ++ .../AndroidAppBuilder/Templates/MonoRunner.java | 98 +++++++++ .../AndroidAppBuilder/Templates/runtime-android.c | 199 ++++++++++++++++++ src/mono/msbuild/AndroidAppBuilder/Utils.cs | 114 +++++++++++ .../msbuild/AndroidTestRunner/AndroidTestRunner.cs | 89 ++++++++ .../AndroidTestRunner/AndroidTestRunner.csproj | 11 + src/mono/netcore/sample/Android/Makefile | 21 ++ src/mono/netcore/sample/Android/Program.cs | 14 ++ src/mono/netcore/sample/Android/Program.csproj | 62 ++++++ 17 files changed, 1042 insertions(+), 2 deletions(-) create mode 100644 src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs create mode 100644 src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj create mode 100644 src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs create mode 100644 src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml create mode 100644 src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt create mode 100644 src/mono/msbuild/AndroidAppBuilder/Templates/MainActivity.java create mode 100644 src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java create mode 100644 src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c create mode 100644 src/mono/msbuild/AndroidAppBuilder/Utils.cs create mode 100644 src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs create mode 100644 src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj create mode 100644 src/mono/netcore/sample/Android/Makefile create mode 100644 src/mono/netcore/sample/Android/Program.cs create mode 100644 src/mono/netcore/sample/Android/Program.csproj diff --git a/eng/testing/tests.targets b/eng/testing/tests.targets index 7c2512a..a2cfbd0 100644 --- a/eng/testing/tests.targets +++ b/eng/testing/tests.targets @@ -118,6 +118,57 @@ + + + + + $(ArtifactsDir)bin\lib-runtime-packs\runtimes\android-$(TargetArchitecture) + $(OutDir)\Bundle + $(RepoRoot)\src\mono\msbuild\AndroidTestRunner\bin + arm64-v8a + armeabi + x86_64 + $(TargetArchitecture) + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(DefineConstants);XMLSERIALIZERGENERATORTESTS $(NetCoreAppCurrent) false - true + true diff --git a/src/mono/mono.proj b/src/mono/mono.proj index c8fdca7..e6849d9 100644 --- a/src/mono/mono.proj +++ b/src/mono/mono.proj @@ -936,9 +936,18 @@ Targets="Restore;Build" /> + + + + + - + <_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x64'">$(MonoObjDir)x64\Bin\$(Configuration)\mono-2.0-sgen.dll <_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x86'">$(MonoObjDir)Win32\Bin\$(Configuration)\mono-2.0-sgen.dll diff --git a/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs new file mode 100644 index 0000000..57e1f90 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class AndroidAppBuilderTask : Task +{ + [Required] + public string SourceDir { get; set; } = ""!; + + [Required] + public string MonoRuntimeHeaders { get; set; } = ""!; + + /// + /// This library will be used as an entry-point (e.g. TestRunner.dll) + /// + [Required] + public string MainLibraryFileName { get; set; } = ""!; + + /// + /// Target arch, can be 'x86', 'x86_64', 'armeabi', 'armeabi-v7a' or 'arm64-v8a' + /// + [Required] + public string Abi { get; set; } = ""!; + + public string? ProjectName { get; set; } + + public string? OutputDir { get; set; } + + public string? AndroidSdk { get; set; } + + public string? AndroidNdk { get; set; } + + public string? MinApiLevel { get; set; } + + public string? BuildApiLevel { get; set; } + + public string? BuildToolsVersion { get; set; } + + [Output] + public string ApkBundlePath { get; set; } = ""!; + + [Output] + public string ApkPackageId { get; set; } = ""!; + + public override bool Execute() + { + Utils.Logger = Log; + + var apkBuilder = new ApkBuilder(); + apkBuilder.ProjectName = ProjectName; + apkBuilder.OutputDir = OutputDir; + apkBuilder.AndroidSdk = AndroidSdk; + apkBuilder.AndroidNdk = AndroidNdk; + apkBuilder.MinApiLevel = MinApiLevel; + apkBuilder.BuildApiLevel = BuildApiLevel; + apkBuilder.BuildToolsVersion = BuildToolsVersion; + (ApkBundlePath, ApkPackageId) = apkBuilder.BuildApk(SourceDir, Abi, MainLibraryFileName, MonoRuntimeHeaders); + + return true; + } +} diff --git a/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj new file mode 100644 index 0000000..d0ced74 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj @@ -0,0 +1,24 @@ + + + Library + bin + $(NetCoreAppCurrent) + enable + true + false + + + + + + + + + + + + + + + + diff --git a/src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs b/src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs new file mode 100644 index 0000000..a7da309 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +public class ApkBuilder +{ + private const string DefaultMinApiLevel = "21"; + + public string? ProjectName { get; set; } + public string? AndroidNdk { get; set; } + public string? AndroidSdk { get; set; } + public string? MinApiLevel { get; set; } + public string? BuildApiLevel { get; set; } + public string? BuildToolsVersion { get; set; } + public string? OutputDir { get; set; } + + public (string apk, string packageId) BuildApk( + string sourceDir, string abi, string entryPointLib, string monoRuntimeHeaders) + { + if (!Directory.Exists(sourceDir)) + throw new ArgumentException($"sourceDir='{sourceDir}' is empty or doesn't exist"); + + if (string.IsNullOrEmpty(abi)) + throw new ArgumentException("abi shoudln't be empty (e.g. x86, x86_64, armeabi, armeabi-v7a or arm64-v8a"); + + if (string.IsNullOrEmpty(entryPointLib)) + throw new ArgumentException("entryPointLib shouldn't be empty"); + + if (!File.Exists(Path.Combine(sourceDir, entryPointLib))) + throw new ArgumentException($"{entryPointLib} was not found in sourceDir='{sourceDir}'"); + + if (string.IsNullOrEmpty(ProjectName)) + ProjectName = Path.GetFileNameWithoutExtension(entryPointLib); + + if (string.IsNullOrEmpty(OutputDir)) + OutputDir = Path.Combine(sourceDir, "bin-" + abi); + + if (ProjectName.Contains(' ')) + throw new ArgumentException($"ProjectName='{ProjectName}' shouldn't not contain spaces."); + + if (string.IsNullOrEmpty(AndroidSdk)) + AndroidSdk = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT"); + + if (string.IsNullOrEmpty(AndroidNdk)) + AndroidNdk = Environment.GetEnvironmentVariable("ANDROID_NDK_ROOT"); + + if (string.IsNullOrEmpty(AndroidSdk) || !Directory.Exists(AndroidSdk)) + throw new ArgumentException($"Android SDK='{AndroidSdk}' was not found or empty (can be set via ANDROID_SDK_ROOT envvar)."); + + if (string.IsNullOrEmpty(AndroidNdk) || !Directory.Exists(AndroidNdk)) + throw new ArgumentException($"Android NDK='{AndroidNdk}' was not found or empty (can be set via ANDROID_NDK_ROOT envvar)."); + + // Try to get the latest build-tools version if not specified + if (string.IsNullOrEmpty(BuildToolsVersion)) + BuildToolsVersion = GetLatestBuildTools(AndroidSdk); + + // Try to get the latest API level if not specified + if (string.IsNullOrEmpty(BuildApiLevel)) + BuildApiLevel = GetLatestApiLevel(AndroidSdk); + + if (string.IsNullOrEmpty(MinApiLevel)) + MinApiLevel = DefaultMinApiLevel; + + // make sure BuildApiLevel >= MinApiLevel + // only if these api levels are not "preview" (not integers) + if (int.TryParse(BuildApiLevel, out int intApi) && + int.TryParse(MinApiLevel, out int intMinApi) && + intApi < intMinApi) + { + throw new ArgumentException($"BuildApiLevel={BuildApiLevel} <= MinApiLevel={MinApiLevel}. " + + "Make sure you've downloaded some recent build-tools in Android SDK"); + } + + string buildToolsFolder = Path.Combine(AndroidSdk, "build-tools", BuildToolsVersion); + if (!Directory.Exists(buildToolsFolder)) + throw new ArgumentException($"{buildToolsFolder} was not found."); + + Directory.CreateDirectory(OutputDir); + Directory.CreateDirectory(Path.Combine(OutputDir, "bin")); + Directory.CreateDirectory(Path.Combine(OutputDir, "obj")); + Directory.CreateDirectory(Path.Combine(OutputDir, "assets")); + + // Copy AppDir to OutputDir/assets (ignore native files) + Utils.DirectoryCopy(sourceDir, Path.Combine(OutputDir, "assets"), file => + { + var extension = Path.GetExtension(file); + // ignore native files, those go to lib/%abi% + if (extension == ".so" || extension == ".a") + { + // ignore ".pdb" and ".dbg" to make APK smaller + return false; + } + return true; + }); + + // tools: + string dx = Path.Combine(buildToolsFolder, "dx"); + string aapt = Path.Combine(buildToolsFolder, "aapt"); + string zipalign = Path.Combine(buildToolsFolder, "zipalign"); + string apksigner = Path.Combine(buildToolsFolder, "apksigner"); + string androidJar = Path.Combine(AndroidSdk, "platforms", "android-" + BuildApiLevel, "android.jar"); + string androidToolchain = Path.Combine(AndroidNdk, "build", "cmake", "android.toolchain.cmake"); + string keytool = "keytool"; + string javac = "javac"; + string cmake = "cmake"; + + if (!File.Exists(androidJar)) + throw new ArgumentException($"API level={BuildApiLevel} is not downloaded in Android SDK"); + + // 1. Build libruntime-android.so` via cmake + + string monoRuntimeLib = Path.Combine(sourceDir, "libmonosgen-2.0.a"); + if (!File.Exists(monoRuntimeLib)) + throw new ArgumentException($"libmonosgen-2.0.a was not found in {sourceDir}"); + + string cmakeLists = Utils.GetEmbeddedResource("CMakeLists-android.txt") + .Replace("%MonoInclude%", monoRuntimeHeaders) + .Replace("%NativeLibrariesToLink%", monoRuntimeLib); + File.WriteAllText(Path.Combine(OutputDir, "CMakeLists.txt"), cmakeLists); + + string runtimeAndroidSrc = Utils.GetEmbeddedResource("runtime-android.c") + .Replace("%EntryPointLibName%", Path.GetFileName(entryPointLib)); + File.WriteAllText(Path.Combine(OutputDir, "runtime-android.c"), runtimeAndroidSrc); + + Utils.RunProcess(cmake, workingDir: OutputDir, + args: $"-DCMAKE_TOOLCHAIN_FILE={androidToolchain} -DANDROID_ABI=\"{abi}\" -DANDROID_STL=none " + + $"-DANDROID_NATIVE_API_LEVEL={MinApiLevel} -B runtime-android"); + Utils.RunProcess("make", workingDir: Path.Combine(OutputDir, "runtime-android")); + + // 2. Compile Java files + + string javaSrcFolder = Path.Combine(OutputDir, "src", "net", "dot"); + Directory.CreateDirectory(javaSrcFolder); + + string packageId = $"net.dot.{ProjectName}"; + + File.WriteAllText(Path.Combine(javaSrcFolder, "MainActivity.java"), + Utils.GetEmbeddedResource("MainActivity.java")); + File.WriteAllText(Path.Combine(javaSrcFolder, "MonoRunner.java"), + Utils.GetEmbeddedResource("MonoRunner.java")); + File.WriteAllText(Path.Combine(OutputDir, "AndroidManifest.xml"), + Utils.GetEmbeddedResource("AndroidManifest.xml") + .Replace("%PackageName%", packageId) + .Replace("%MinSdkLevel%", MinApiLevel)); + + string javaCompilerArgs = $"-d obj -classpath src -bootclasspath {androidJar} -source 1.8 -target 1.8 "; + Utils.RunProcess(javac, javaCompilerArgs + Path.Combine(javaSrcFolder, "MainActivity.java"), workingDir: OutputDir); + Utils.RunProcess(javac, javaCompilerArgs + Path.Combine(javaSrcFolder, "MonoRunner.java"), workingDir: OutputDir); + Utils.RunProcess(dx, "--dex --output=classes.dex obj", workingDir: OutputDir); + + // 3. Generate APK + + string apkFile = Path.Combine(OutputDir, "bin", $"{ProjectName}.unaligned.apk"); + Utils.RunProcess(aapt, $"package -f -m -F {apkFile} -A assets -M AndroidManifest.xml -I {androidJar}", workingDir: OutputDir); + + var dynamicLibs = new List(); + dynamicLibs.Add(Path.Combine(OutputDir, "runtime-android", "libruntime-android.so")); + dynamicLibs.AddRange(Directory.GetFiles(sourceDir, "*.so")); + + // add all *.so files to lib/%abi%/ + Directory.CreateDirectory(Path.Combine(OutputDir, "lib", abi)); + foreach (var dynamicLib in dynamicLibs) + { + string destRelative = Path.Combine("lib", abi, Path.GetFileName(dynamicLib)); + File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true); + Utils.RunProcess(aapt, $"add {apkFile} {destRelative}", workingDir: OutputDir); + } + Utils.RunProcess(aapt, $"add {apkFile} classes.dex", workingDir: OutputDir); + + // 4. Align APK + + string alignedApk = Path.Combine(OutputDir, "bin", $"{ProjectName}.apk"); + Utils.RunProcess(zipalign, $"-v 4 {apkFile} {alignedApk}", workingDir: OutputDir); + + // 5. Generate key + + string signingKey = Path.Combine(OutputDir, "debug.keystore"); + if (!File.Exists(signingKey)) + { + Utils.RunProcess(keytool, "-genkey -v -keystore debug.keystore -storepass android -alias " + + "androiddebugkey -keypass android -keyalg RSA -keysize 2048 -noprompt " + + "-dname \"CN=Android Debug,O=Android,C=US\"", workingDir: OutputDir, silent: true); + } + + // 6. Sign APK + + Utils.RunProcess(apksigner, $"sign --min-sdk-version {MinApiLevel} --ks debug.keystore " + + $"--ks-pass pass:android --key-pass pass:android {alignedApk}", workingDir: OutputDir); + + return (alignedApk, packageId); + } + + /// + /// Scan android SDK for build tools (ignore preview versions) + /// + private static string GetLatestBuildTools(string androidSdkDir) + { + string? buildTools = Directory.GetDirectories(Path.Combine(androidSdkDir, "build-tools")) + .Select(Path.GetFileName) + .Where(file => !file!.Contains("-")) + .Select(file => Version.TryParse(Path.GetFileName(file), out Version? version) ? version : default) + .OrderByDescending(v => v) + .FirstOrDefault()?.ToString(); + + if (string.IsNullOrEmpty(buildTools)) + throw new ArgumentException($"Android SDK ({androidSdkDir}) doesn't contain build-tools."); + + return buildTools; + } + + /// + /// Scan android SDK for api levels (ignore preview versions) + /// + private static string GetLatestApiLevel(string androidSdkDir) + { + return Directory.GetDirectories(Path.Combine(androidSdkDir, "platforms")) + .Select(file => int.TryParse(Path.GetFileName(file).Replace("android-", ""), out int apiLevel) ? apiLevel : -1) + .OrderByDescending(v => v) + .FirstOrDefault() + .ToString(); + } +} diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml b/src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml new file mode 100644 index 0000000..21dc552 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt b/src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt new file mode 100644 index 0000000..619d576 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.10) + +project(runtime-android) + +add_library( + runtime-android + SHARED + runtime-android.c) + +include_directories("%MonoInclude%") + +target_link_libraries( + runtime-android + %NativeLibrariesToLink% + libz.so + log) diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/MainActivity.java b/src/mono/msbuild/AndroidAppBuilder/Templates/MainActivity.java new file mode 100644 index 0000000..2d9114f --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Templates/MainActivity.java @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +package net.dot; + +import android.app.AlertDialog; +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity +{ + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("Use `adb shell am instrument -w " + getApplicationContext().getPackageName() + "net.dot.MonoRunner` to run the tests."); + dlgAlert.create().show(); + } +} diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java b/src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java new file mode 100644 index 0000000..87f75b5 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +package net.dot; + +import android.app.Instrumentation; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.app.Activity; +import android.os.Bundle; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class MonoRunner extends Instrumentation +{ + static MonoRunner inst; + + static { + System.loadLibrary("runtime-android"); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + start(); + } + + @Override + public void onStart() { + super.onStart(); + + MonoRunner.inst = this; + Context context = getContext(); + AssetManager am = context.getAssets(); + String filesDir = context.getFilesDir().getAbsolutePath(); + String cacheDir = context.getCacheDir().getAbsolutePath (); + + copyAssetDir(am, "", filesDir); + + // retcode is what Main() returns in C# + int retcode = initRuntime(filesDir, cacheDir); + WriteLineToInstrumentation("[Mono] Main() returned " + retcode); + runOnMainSync (new Runnable() { + public void run() { + finish (retcode, null); + } + }); + } + + static void WriteLineToInstrumentation(String line) { + Bundle b = new Bundle(); + b.putString(Instrumentation.REPORT_KEY_STREAMRESULT, line + "\n"); + MonoRunner.inst.sendStatus(0, b); + } + + static void copyAssetDir(AssetManager am, String path, String outpath) { + try { + String[] res = am.list(path); + for (int i = 0; i < res.length; ++i) { + String fromFile = res[i]; + String toFile = outpath + "/" + res[i]; + try { + InputStream fromStream = am.open(fromFile); + Log.w("MONO", "\tCOPYING " + fromFile + " to " + toFile); + copy(fromStream, new FileOutputStream(toFile)); + } catch (FileNotFoundException e) { + new File(toFile).mkdirs(); + copyAssetDir(am, fromFile, toFile); + continue; + } + } + } + catch (Exception e) { + Log.w("MONO", "EXCEPTION", e); + } + } + + static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buff = new byte [1024]; + for (int len = in.read(buff); len != -1; len = in.read(buff)) + out.write(buff, 0, len); + in.close(); + out.close(); + } + + native int initRuntime(String libsDir, String cacheDir); +} diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c b/src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c new file mode 100644 index 0000000..a2bc8f1 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char *bundle_path; + +#define LOG_INFO(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "MONO", fmt, ##__VA_ARGS__) +#define LOG_ERROR(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "MONO", fmt, ##__VA_ARGS__) + +static MonoAssembly* +load_assembly (const char *name, const char *culture) +{ + char filename [1024]; + char path [1024]; + int res; + + LOG_INFO ("assembly_preload_hook: %s %s %s\n", name, culture, bundle_path); + + int len = strlen (name); + int has_extension = len > 3 && name [len - 4] == '.' && (!strcmp ("exe", name + (len - 3)) || !strcmp ("dll", name + (len - 3))); + + // add extensions if required. + strlcpy (filename, name, sizeof (filename)); + if (!has_extension) { + strlcat (filename, ".dll", sizeof (filename)); + } + + if (culture && strcmp (culture, "")) + res = snprintf (path, sizeof (path) - 1, "%s/%s/%s", bundle_path, culture, filename); + else + res = snprintf (path, sizeof (path) - 1, "%s/%s", bundle_path, filename); + assert (res > 0); + + struct stat buffer; + if (stat (path, &buffer) == 0) { + MonoAssembly *assembly = mono_assembly_open (path, NULL); + assert (assembly); + return assembly; + } + return NULL; +} + +static MonoAssembly* +assembly_preload_hook (MonoAssemblyName *aname, char **assemblies_path, void* user_data) +{ + const char *name = mono_assembly_name_get_name (aname); + const char *culture = mono_assembly_name_get_culture (aname); + return load_assembly (name, culture); +} + +char * +strdup_printf (const char *msg, ...) +{ + va_list args; + char *formatted = NULL; + va_start (args, msg); + vasprintf (&formatted, msg, args); + va_end (args); + return formatted; +} + +static MonoObject * +fetch_exception_property (MonoObject *obj, const char *name, bool is_virtual) +{ + MonoMethod *get = NULL; + MonoMethod *get_virt = NULL; + MonoObject *exc = NULL; + + get = mono_class_get_method_from_name (mono_get_exception_class (), name, 0); + if (get) { + if (is_virtual) { + get_virt = mono_object_get_virtual_method (obj, get); + if (get_virt) + get = get_virt; + } + + return (MonoObject *) mono_runtime_invoke (get, obj, NULL, &exc); + } else { + printf ("Could not find the property System.Exception.%s", name); + } + + return NULL; +} + +static char * +fetch_exception_property_string (MonoObject *obj, const char *name, bool is_virtual) +{ + MonoString *str = (MonoString *) fetch_exception_property (obj, name, is_virtual); + return str ? mono_string_to_utf8 (str) : NULL; +} + +void +unhandled_exception_handler (MonoObject *exc, void *user_data) +{ + MonoClass *type = mono_object_get_class (exc); + char *type_name = strdup_printf ("%s.%s", mono_class_get_namespace (type), mono_class_get_name (type)); + char *trace = fetch_exception_property_string (exc, "get_StackTrace", true); + char *message = fetch_exception_property_string (exc, "get_Message", true); + + LOG_ERROR("UnhandledException: %s %s %s", type_name, message, trace); + + free (trace); + free (message); + free (type_name); + exit (1); +} + +void +log_callback (const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data) +{ + LOG_INFO ("(%s %s) %s", log_domain, log_level, message); + if (fatal) { + LOG_ERROR ("Exit code: %d.", 1); + exit (1); + } +} + +int +mono_mobile_runtime_init (void) +{ + // uncomment for debug output: + // + // setenv ("MONO_LOG_LEVEL", "debug", TRUE); + // setenv ("MONO_LOG_MASK", "all", TRUE); + + bool wait_for_debugger = false; + chdir (bundle_path); + + // TODO: set TRUSTED_PLATFORM_ASSEMBLIES, APP_PATHS and NATIVE_DLL_SEARCH_DIRECTORIES + monovm_initialize(0, NULL, NULL); + + mono_debug_init (MONO_DEBUG_FORMAT_MONO); + mono_install_assembly_preload_hook (assembly_preload_hook, NULL); + mono_install_unhandled_exception_hook (unhandled_exception_handler, NULL); + mono_trace_set_log_handler (log_callback, NULL); + mono_set_signal_chaining (true); + mono_set_crash_chaining (true); + + if (wait_for_debugger) { + char* options[] = { "--debugger-agent=transport=dt_socket,server=y,address=0.0.0.0:55555" }; + mono_jit_parse_options (1, options); + } + mono_jit_init_version ("dotnet.android", "mobile"); + + const char* executable = "%EntryPointLibName%"; + MonoAssembly *assembly = load_assembly (executable, NULL); + assert (assembly); + LOG_INFO ("Executable: %s", executable); + + char *managed_argv [1]; + managed_argv[0] = bundle_path; + + int res = mono_jit_exec (mono_domain_get (), assembly, 1, managed_argv); + LOG_INFO ("Exit code: %d.", res); + return res; +} + +static void +strncpy_str (JNIEnv *env, char *buff, jstring str, int nbuff) +{ + jboolean isCopy = 0; + const char *copy_buff = (*env)->GetStringUTFChars (env, str, &isCopy); + strncpy (buff, copy_buff, nbuff); + if (isCopy) + (*env)->ReleaseStringUTFChars (env, str, copy_buff); +} + +int +Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir) +{ + char file_dir[2048]; + char cache_dir[2048]; + strncpy_str (env, file_dir, j_files_dir, sizeof(file_dir)); + strncpy_str (env, cache_dir, j_cache_dir, sizeof(cache_dir)); + + bundle_path = file_dir; + setenv ("HOME", bundle_path, true); + setenv ("TMPDIR", cache_dir, true); + return mono_mobile_runtime_init (); +} diff --git a/src/mono/msbuild/AndroidAppBuilder/Utils.cs b/src/mono/msbuild/AndroidAppBuilder/Utils.cs new file mode 100644 index 0000000..a5e1824 --- /dev/null +++ b/src/mono/msbuild/AndroidAppBuilder/Utils.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +internal class Utils +{ + public static string GetEmbeddedResource(string file) + { + using Stream stream = typeof(Utils).Assembly + .GetManifestResourceStream($"{typeof(Utils).Assembly.GetName().Name}.Templates.{file}")!; + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static string RunProcess( + string path, + string args = "", + IDictionary? envVars = null, + string? workingDir = null, + bool ignoreErrors = false, + bool silent = false) + { + LogInfo($"Running: {path} {args}"); + var outputBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + var processStartInfo = new ProcessStartInfo + { + FileName = path, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + Arguments = args, + }; + + if (workingDir != null) + processStartInfo.WorkingDirectory = workingDir; + + if (envVars != null) + { + foreach (KeyValuePair envVar in envVars) + processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value; + } + + Process? process = Process.Start(processStartInfo); + if (process == null) + throw new ArgumentException("Process.Start({path} {args}) returned null process"); + + process.ErrorDataReceived += (sender, e) => + { + if (!silent) + { + LogError(e.Data); + outputBuilder.AppendLine(e.Data); + errorBuilder.AppendLine(e.Data); + } + }; + process.OutputDataReceived += (sender, e) => + { + if (!silent) + { + LogInfo(e.Data); + outputBuilder.AppendLine(e.Data); + } + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + + if (!ignoreErrors && process.ExitCode != 0) + throw new Exception("Error: " + errorBuilder); + + return outputBuilder.ToString().Trim('\r','\n'); + } + + public static void DirectoryCopy(string sourceDir, string destDir, Func predicate) + { + string[] files = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories); + foreach (string file in files) + { + if (!predicate(file)) + continue; + + string relativePath = Path.GetRelativePath(sourceDir, file); + string? relativeDir = Path.GetDirectoryName(relativePath); + if (!string.IsNullOrEmpty(relativeDir)) + Directory.CreateDirectory(Path.Combine(destDir, relativeDir)); + + File.Copy(file, Path.Combine(destDir, relativePath), true); + } + } + + public static TaskLoggingHelper? Logger { get; set; } + + public static void LogInfo(string? msg) + { + if (msg != null) + Logger?.LogMessage(MessageImportance.High, msg); + } + + public static void LogError(string? msg) + { + if (msg != null) + Logger?.LogError(msg); + } +} diff --git a/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs new file mode 100644 index 0000000..9f69be4 --- /dev/null +++ b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Text; +using System.IO; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.XHarness.Tests.Runners; +using Microsoft.DotNet.XHarness.Tests.Runners.Core; + +public class SimpleAndroidTestRunner : AndroidApplicationEntryPoint, IDevice +{ + private static List s_testLibs = new List(); + private static string? s_MainTestName; + + public static async Task Main(string[] args) + { + s_testLibs = Directory.GetFiles(Environment.CurrentDirectory, "*.Tests.dll").ToList(); + if (s_testLibs.Count < 1) + { + Console.WriteLine($"Test libs were not found (*.Tests.dll was not found in {Environment.CurrentDirectory})"); + return -1; + } + s_MainTestName = Path.GetFileNameWithoutExtension(s_testLibs[0]); + var simpleTestRunner = new SimpleAndroidTestRunner(true); + await simpleTestRunner.RunAsync(); + Console.WriteLine("----- Done -----"); + return 0; + } + + public SimpleAndroidTestRunner(bool verbose) + { + if (verbose) + { + MinimumLogLevel = MinimumLogLevel.Verbose; + _maxParallelThreads = 1; + } + else + { + MinimumLogLevel = MinimumLogLevel.Info; + _maxParallelThreads = Environment.ProcessorCount; + } + } + + protected override IEnumerable GetTestAssemblies() + { + foreach (string file in s_testLibs) + { + yield return new TestAssemblyInfo(Assembly.LoadFrom(file), file); + } + } + + protected override void TerminateWithSuccess() + { + Console.WriteLine("[TerminateWithSuccess]"); + } + + private int? _maxParallelThreads; + + protected override int? MaxParallelThreads => _maxParallelThreads; + + protected override IDevice Device => this; + + protected override TestRunnerType TestRunner => TestRunnerType.Xunit; + + protected override string? IgnoreFilesDirectory => null; + + public string BundleIdentifier => "net.dot." + s_MainTestName; + + public string? UniqueIdentifier { get; } + + public string? Name { get; } + + public string? Model { get; } + + public string? SystemName { get; } + + public string? SystemVersion { get; } + + public string? Locale { get; } + + public override TextWriter? Logger => null; + + public override string TestsResultsFinalPath => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "testResults.xml"); +} diff --git a/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj new file mode 100644 index 0000000..8c6e242 --- /dev/null +++ b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj @@ -0,0 +1,11 @@ + + + Exe + bin + $(NetCoreAppCurrent) + enable + + + + + diff --git a/src/mono/netcore/sample/Android/Makefile b/src/mono/netcore/sample/Android/Makefile new file mode 100644 index 0000000..b069bb1 --- /dev/null +++ b/src/mono/netcore/sample/Android/Makefile @@ -0,0 +1,21 @@ +MONO_CONFIG=Debug +MONO_ARCH=arm64 +DOTNET := ../../../../.././dotnet.sh + +#export ANDROID_NDK_ROOT=/path/to/android/ndk +#export ANDROID_SDK_ROOT=/path/to/android/sdk + +all: runtimepack bundle + +bundle: clean + $(DOTNET) build -c $(MONO_CONFIG) Program.csproj + $(DOTNET) msbuild /t:BuildAppBundle /p:Configuration=$(MONO_CONFIG) /p:TargetArchitecture=$(MONO_ARCH) + +deploy-launch: bundle + $(DOTNET) msbuild /t:ReinstallAndLaunch + +runtimepack: + ../../../../.././build.sh -c $(MONO_CONFIG) -os Android -arch $(MONO_ARCH) -subset Mono+Libs /p:DisableCrossgen=true + +clean: + rm -rf bin diff --git a/src/mono/netcore/sample/Android/Program.cs b/src/mono/netcore/sample/Android/Program.cs new file mode 100644 index 0000000..4e61d6f --- /dev/null +++ b/src/mono/netcore/sample/Android/Program.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +public static class Program +{ + public static int Main(string[] args) + { + Console.WriteLine("Hello, Android!"); // logcat + return 42; + } +} diff --git a/src/mono/netcore/sample/Android/Program.csproj b/src/mono/netcore/sample/Android/Program.csproj new file mode 100644 index 0000000..5c22424 --- /dev/null +++ b/src/mono/netcore/sample/Android/Program.csproj @@ -0,0 +1,62 @@ + + + Exe + bin + $(NetCoreAppCurrent) + x64 + $(ArtifactsDir)bin\lib-runtime-packs\runtimes\android-$(TargetArchitecture) + $(MSBuildThisFileDirectory)\bin\bundle + + + + + + + + + + + arm64-v8a + armeabi + x86_64 + $(TargetArchitecture) + + + + + + + + + + + + + + + + + + + + + + + $(ANDROID_SDK_ROOT)\platform-tools\adb + + + + + + + + -- 2.7.4