[mono] Add Android sample and AndroidAppBuilder task (#35483)
authorEgor Bogatov <egorbo@gmail.com>
Wed, 29 Apr 2020 16:34:29 +0000 (19:34 +0300)
committerGitHub <noreply@github.com>
Wed, 29 Apr 2020 16:34:29 +0000 (19:34 +0300)
17 files changed:
eng/testing/tests.targets
src/libraries/Microsoft.XmlSerializer.Generator/tests/Microsoft.XmlSerializer.Generator.Tests.csproj
src/mono/mono.proj
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Templates/MainActivity.java [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c [new file with mode: 0644]
src/mono/msbuild/AndroidAppBuilder/Utils.cs [new file with mode: 0644]
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs [new file with mode: 0644]
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj [new file with mode: 0644]
src/mono/netcore/sample/Android/Makefile [new file with mode: 0644]
src/mono/netcore/sample/Android/Program.cs [new file with mode: 0644]
src/mono/netcore/sample/Android/Program.csproj [new file with mode: 0644]

index 7c2512a..a2cfbd0 100644 (file)
     <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" 
index a9678f5..88cd7f6 100644 (file)
@@ -3,7 +3,7 @@
     <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. -->
index c8fdca7..e6849d9 100644 (file)
              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>
diff --git a/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs
new file mode 100644 (file)
index 0000000..57e1f90
--- /dev/null
@@ -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; } = ""!;
+
+    /// <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;
+    }
+}
diff --git a/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj b/src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj
new file mode 100644 (file)
index 0000000..d0ced74
--- /dev/null
@@ -0,0 +1,24 @@
+<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>
diff --git a/src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs b/src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs
new file mode 100644 (file)
index 0000000..a7da309
--- /dev/null
@@ -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<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();
+    }
+}
diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml b/src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..21dc552
--- /dev/null
@@ -0,0 +1,21 @@
+<?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
diff --git a/src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt b/src/mono/msbuild/AndroidAppBuilder/Templates/CMakeLists-android.txt
new file mode 100644 (file)
index 0000000..619d576
--- /dev/null
@@ -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 (file)
index 0000000..2d9114f
--- /dev/null
@@ -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 (file)
index 0000000..87f75b5
--- /dev/null
@@ -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 (file)
index 0000000..a2bc8f1
--- /dev/null
@@ -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 <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 ();
+}
diff --git a/src/mono/msbuild/AndroidAppBuilder/Utils.cs b/src/mono/msbuild/AndroidAppBuilder/Utils.cs
new file mode 100644 (file)
index 0000000..a5e1824
--- /dev/null
@@ -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<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);
+    }
+}
diff --git a/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs
new file mode 100644 (file)
index 0000000..9f69be4
--- /dev/null
@@ -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<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");
+}
diff --git a/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj b/src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj
new file mode 100644 (file)
index 0000000..8c6e242
--- /dev/null
@@ -0,0 +1,11 @@
+<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>
diff --git a/src/mono/netcore/sample/Android/Makefile b/src/mono/netcore/sample/Android/Makefile
new file mode 100644 (file)
index 0000000..b069bb1
--- /dev/null
@@ -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 (file)
index 0000000..4e61d6f
--- /dev/null
@@ -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 (file)
index 0000000..5c22424
--- /dev/null
@@ -0,0 +1,62 @@
+<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>