1
0
Fork 0
mirror of https://github.com/VSadov/Satori.git synced 2025-06-09 17:44:48 +09:00

[mono] Add Android sample and AndroidAppBuilder task (#35483)

This commit is contained in:
Egor Bogatov 2020-04-29 19:34:29 +03:00 committed by GitHub
parent 11c2e4c3b8
commit 0fa0b905f6
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1042 additions and 2 deletions

View file

@ -118,6 +118,57 @@
<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"

View 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. -->

View file

@ -936,9 +936,18 @@
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>

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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)

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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 ();
}

View file

@ -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);
}
}

View file

@ -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");
}

View file

@ -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>

View file

@ -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

View file

@ -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;
}
}

View file

@ -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>