<Error Condition="'$(TestRunExitCode)' != '0'" Text="$(TestRunErrorMessage)" />
</Target>
+ <!-- Generate a self-contained app bundle for Android with tests.
+ This target is executed once build is done for a test lib (after CopyFilesToOutputDirectory target) -->
+ <UsingTask TaskName="AndroidAppBuilderTask"
+ AssemblyFile="$(ArtifactsObjDir)mono\AndroidAppBuilder\$(TargetArchitecture)\$(Configuration)\AndroidAppBuilder.dll" />
+ <Target Condition="'$(TargetOS)' == 'Android'" Name="BundleTestAndroidApp" AfterTargets="CopyFilesToOutputDirectory">
+ <PropertyGroup>
+ <RuntimePackDir>$(ArtifactsDir)bin\lib-runtime-packs\runtimes\android-$(TargetArchitecture)</RuntimePackDir>
+ <BundleDir>$(OutDir)\Bundle</BundleDir>
+ <AndroidTestRunner>$(RepoRoot)\src\mono\msbuild\AndroidTestRunner\bin</AndroidTestRunner>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='arm64'">arm64-v8a</AndroidAbi>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='arm'">armeabi</AndroidAbi>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='x64'">x86_64</AndroidAbi>
+ <AndroidAbi Condition="'$(AndroidAbi)'==''">$(TargetArchitecture)</AndroidAbi>
+ </PropertyGroup>
+ <!-- TEMP: We need to copy additional stuff into $(OutDir)\Bundle
+ 1) The whole BCL
+ 2) Test Runner (with xharness client-side lib)
+ -->
+ <ItemGroup>
+ <TestBinaries Include="$(OutDir)\*.*"/>
+ <AndroidTestRunnerBinaries Include="$(AndroidTestRunner)\*.*" />
+ <BclBinaries Include="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\*.*"
+ Exclude="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\System.Runtime.WindowsRuntime.dll" />
+ <BclBinaries Include="$(RuntimePackDir)\native\*.*" Exclude="$(RuntimePackDir)\native\libmono.dylib" />
+
+ <!-- remove PDBs and DBGs to save some space until we integrate ILLink -->
+ <BclBinaries Remove="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\*.pdb" />
+ <BclBinaries Remove="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\*.dbg" />
+ </ItemGroup>
+
+ <Error Condition="!Exists('$(AndroidTestRunner)')" Text="AndroidTestRunner=$(AndroidTestRunner) doesn't exist" />
+ <Error Condition="!Exists('$(RuntimePackDir)')" Text="RuntimePackDir=$(RuntimePackDir) doesn't exist" />
+ <RemoveDir Directories="$(BundleDir)" />
+ <Copy SourceFiles="@(TestBinaries)" DestinationFolder="$(BundleDir)" SkipUnchangedFiles="true"/>
+ <Copy SourceFiles="@(AndroidTestRunnerBinaries)" DestinationFolder="$(BundleDir)\%(RecursiveDir)" SkipUnchangedFiles="true"/>
+ <Copy SourceFiles="@(BclBinaries)" DestinationFolder="$(BundleDir)\%(RecursiveDir)" SkipUnchangedFiles="true"/>
+
+ <AndroidAppBuilderTask
+ Abi="$(AndroidAbi)"
+ ProjectName="$(AssemblyName)"
+ MonoRuntimeHeaders="$(RuntimePackDir)\native\include\mono-2.0"
+ MainLibraryFileName="AndroidTestRunner.dll"
+ OutputDir="$(BundleDir)"
+ SourceDir="$(BundleDir)">
+ <Output TaskParameter="ApkPackageId" PropertyName="ApkPackageId" />
+ <Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
+ </AndroidAppBuilderTask>
+ <Message Importance="High" Text="PackageId: $(ApkPackageId)"/>
+ <Message Importance="High" Text="Apk: $(ApkBundlePath)"/>
+ </Target>
+
<!-- Generate a self-contained app bundle for iOS with tests.
This target is executed once build is done for a test lib (after CopyFilesToOutputDirectory target) -->
<UsingTask TaskName="AppleAppBuilderTask"
<DefineConstants>$(DefineConstants);XMLSERIALIZERGENERATORTESTS</DefineConstants>
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
<CoverageSupported>false</CoverageSupported>
- <SkipTestsOnPlatform Condition="'$(TargetOS)' == 'FreeBSD' or '$(TargetOS)' == 'iOS' or '$(TargetOS)' == 'tvOS' or '$(TargetArchitecture)' == 'arm' or '$(TargetArchitecture)' == 'arm64' or '$(TargetArchitecture)' == 'armel' or '$(TargetArchitecture)' == 'wasm'">true</SkipTestsOnPlatform>
+ <SkipTestsOnPlatform Condition="'$(TargetsMobile)' == 'true' or '$(TargetOS)' == 'FreeBSD' or '$(TargetArchitecture)' == 'arm' or '$(TargetArchitecture)' == 'arm64' or '$(TargetArchitecture)' == 'armel' or '$(TargetArchitecture)' == 'wasm'">true</SkipTestsOnPlatform>
</PropertyGroup>
<PropertyGroup>
<!-- Reuse the same runtimeconfig used by MSBuild. -->
Targets="Restore;Build" />
</Target>
+ <Target Name="BuildAndroidAppBuilder">
+ <MSBuild Projects="$(MonoProjectRoot)msbuild\AndroidAppBuilder\AndroidAppBuilder.csproj"
+ Properties="Configuration=$(Configuration)"
+ Targets="Restore;Build" />
+ <MSBuild Projects="$(MonoProjectRoot)msbuild\AndroidTestRunner\AndroidTestRunner.csproj"
+ Properties="Configuration=$(Configuration)"
+ Targets="Restore;Build" />
+ </Target>
+
<!-- Ordering matters! Overwriting the Build target. -->
<!-- General targets -->
- <Target Name="Build" DependsOnTargets="BuildMonoRuntimeUnix;BuildMonoRuntimeWindows;BuildAppleAppBuilder">
+ <Target Name="Build" DependsOnTargets="BuildMonoRuntimeUnix;BuildMonoRuntimeWindows;BuildAppleAppBuilder;BuildAndroidAppBuilder">
<PropertyGroup>
<_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x64'">$(MonoObjDir)x64\Bin\$(Configuration)\mono-2.0-sgen.dll</_MonoRuntimeFilePath>
<_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x86'">$(MonoObjDir)Win32\Bin\$(Configuration)\mono-2.0-sgen.dll</_MonoRuntimeFilePath>
--- /dev/null
+// 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; } = ""!;
+
+ /// <summary>
+ /// This library will be used as an entry-point (e.g. TestRunner.dll)
+ /// </summary>
+ [Required]
+ public string MainLibraryFileName { get; set; } = ""!;
+
+ /// <summary>
+ /// Target arch, can be 'x86', 'x86_64', 'armeabi', 'armeabi-v7a' or 'arm64-v8a'
+ /// </summary>
+ [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;
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <OutputPath>bin</OutputPath>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <Nullable>enable</Nullable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
+ </PropertyGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Templates\*.*" />
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Build" Version="$(RefOnlyMicrosoftBuildVersion)" />
+ <PackageReference Include="Microsoft.Build.Framework" Version="$(RefOnlyMicrosoftBuildFrameworkVersion)" />
+ <PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(RefOnlyMicrosoftBuildTasksCoreVersion)" />
+ <PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(RefOnlyMicrosoftBuildUtilitiesCoreVersion)" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ApkBuilder.cs" />
+ <Compile Include="AndroidAppBuilder.cs" />
+ <Compile Include="Utils.cs" />
+ </ItemGroup>
+</Project>
--- /dev/null
+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<string>();
+ 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);
+ }
+
+ /// <summary>
+ /// Scan android SDK for build tools (ignore preview versions)
+ /// </summary>
+ 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;
+ }
+
+ /// <summary>
+ /// Scan android SDK for api levels (ignore preview versions)
+ /// </summary>
+ 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();
+ }
+}
--- /dev/null
+<?xml version="1.0"?>
+<manifest xmlns:a="http://schemas.android.com/apk/res/android"
+ package="%PackageName%"
+ a:versionCode="1"
+ a:versionName="1.0">
+ <uses-sdk a:minSdkVersion="%MinSdkLevel%" />
+ <uses-permission a:name="android.permission.INTERNET"/>
+ <application a:label="%PackageName%"
+ a:largeHeap="true">
+ <activity a:name="net.dot.MainActivity">
+ <intent-filter>
+ <category a:name="android.intent.category.LAUNCHER"/>
+ <action a:name="android.intent.action.MAIN"/>
+ </intent-filter>
+ </activity>
+ </application>
+
+ <instrumentation
+ a:name="net.dot.MonoRunner"
+ a:targetPackage="%PackageName%" />
+</manifest>
\ No newline at end of file
--- /dev/null
+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)
--- /dev/null
+// 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();
+ }
+}
--- /dev/null
+// 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);
+}
--- /dev/null
+// 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 <mono/utils/mono-publib.h>
+#include <mono/utils/mono-logger.h>
+#include <mono/metadata/assembly.h>
+#include <mono/metadata/mono-debug.h>
+#include <mono/metadata/mono-gc.h>
+#include <mono/metadata/exception.h>
+#include <mono/jit/jit.h>
+#include <mono/jit/mono-private-unstable.h>
+
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <jni.h>
+#include <android/log.h>
+#include <sys/system_properties.h>
+#include <assert.h>
+#include <unistd.h>
+
+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 ();
+}
--- /dev/null
+// 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<string, string>? 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<string, string> 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<string, bool> 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);
+ }
+}
--- /dev/null
+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<string> s_testLibs = new List<string>();
+ private static string? s_MainTestName;
+
+ public static async Task<int> 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<TestAssemblyInfo> 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");
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <OutputPath>bin</OutputPath>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.DotNet.XHarness.Tests.Runners" Version="$(MicrosoftDotNetXHarnessTestsRunnersVersion)" />
+ </ItemGroup>
+</Project>
--- /dev/null
+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
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <OutputPath>bin</OutputPath>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <TargetArchitecture Condition="'$(TargetArchitecture)'==''">x64</TargetArchitecture>
+ <RuntimePackDir>$(ArtifactsDir)bin\lib-runtime-packs\runtimes\android-$(TargetArchitecture)</RuntimePackDir>
+ <BundleDir>$(MSBuildThisFileDirectory)\bin\bundle</BundleDir>
+ </PropertyGroup>
+
+ <Target Name="RebuildAndroidAppBuilder">
+ <MSBuild Projects="$(RepoRoot)src\mono\msbuild\AndroidAppBuilder\AndroidAppBuilder.csproj"
+ Properties="Configuration=$(Configuration)" Targets="Restore;Build" />
+ </Target>
+
+ <UsingTask TaskName="AndroidAppBuilderTask"
+ AssemblyFile="$(ArtifactsObjDir)mono\AndroidAppBuilder\$(TargetArchitecture)\$(Configuration)\AndroidAppBuilder.dll" />
+
+ <Target Name="BuildAppBundle" DependsOnTargets="RebuildAndroidAppBuilder">
+ <PropertyGroup>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='arm64'">arm64-v8a</AndroidAbi>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='arm'">armeabi</AndroidAbi>
+ <AndroidAbi Condition="'$(TargetArchitecture)'=='x64'">x86_64</AndroidAbi>
+ <AndroidAbi Condition="'$(AndroidAbi)'==''">$(TargetArchitecture)</AndroidAbi>
+ </PropertyGroup>
+ <ItemGroup>
+ <AppBinaries Include="bin\*.*"/>
+ <BclBinaries Include="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\*.*"
+ Exclude="$(RuntimePackDir)\lib\$(NetCoreAppCurrent)\System.Runtime.WindowsRuntime.dll" />
+ <BclBinaries Include="$(RuntimePackDir)\native\*.*" />
+ </ItemGroup>
+ <Error Condition="'$(AndroidAbi)'==''" Text="Unknown $(TargetArchitecture)" />
+ <Error Condition="!Exists('$(RuntimePackDir)')" Text="RuntimePackDir=$(RuntimePackDir) doesn't exist" />
+ <RemoveDir Directories="$(BundleDir)" />
+ <Copy SourceFiles="@(AppBinaries)" DestinationFolder="$(BundleDir)" SkipUnchangedFiles="true"/>
+ <Copy SourceFiles="@(BclBinaries)" DestinationFolder="$(BundleDir)\%(RecursiveDir)" SkipUnchangedFiles="true"/>
+ <AndroidAppBuilderTask
+ Abi="$(AndroidAbi)"
+ ProjectName="HelloAndroid"
+ MonoRuntimeHeaders="$(RuntimePackDir)\native\include\mono-2.0"
+ MainLibraryFileName="Program.dll"
+ SourceDir="$(BundleDir)"
+ OutputDir="$(BundleDir)\apk">
+ <Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
+ <Output TaskParameter="ApkPackageId" PropertyName="ApkPackageId" />
+ </AndroidAppBuilderTask>
+ <Message Importance="High" Text="Apk: $(ApkBundlePath)"/>
+ <Message Importance="High" Text="PackageId: $(ApkPackageId)"/>
+ </Target>
+
+ <!-- Deploy and launch on an active emulator or device -->
+ <Target Name="ReinstallAndLaunch">
+ <PropertyGroup>
+ <AdbTool>$(ANDROID_SDK_ROOT)\platform-tools\adb</AdbTool>
+ </PropertyGroup>
+ <Message Importance="High" Text="Uninstalling app if it exists (throws an error if it doesn't but it can be ignored):"/>
+ <Exec Command="$(AdbTool) uninstall net.dot.HelloAndroid" ContinueOnError="WarnAndContinue" />
+ <Exec Command="$(AdbTool) install bin/bundle/apk/bin/HelloAndroid.apk" />
+ <Exec Command="$(AdbTool) shell am instrument -w net.dot.HelloAndroid/net.dot.MonoRunner" />
+ <!--Exec Command="$(AdbTool) logcat" /-->
+ </Target>
+</Project>