Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions eng/testing/tests.mobile.targets
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

<UsingTask Condition="'$(RunAOTCompilation)' == 'true'" TaskName="MonoAOTCompiler" AssemblyFile="$(MonoAOTCompilerTasksAssemblyPath)" />
<Import Condition="'$(RunAOTCompilation)' == 'true'" Project="$(MonoAOTCompilerDir)MonoAOTCompiler.props" />

<PropertyGroup Condition="'$(RunAOTCompilation)' == 'true'">
<_MobileIntermediateOutputPath>$(IntermediateOutputPath)mobile</_MobileIntermediateOutputPath>
</PropertyGroup>

<!-- Generate a self-contained app bundle for Android with tests. -->
<UsingTask Condition="'$(TargetOS)' == 'Android'"
TaskName="AndroidAppBuilderTask"
Expand All @@ -42,6 +49,12 @@

<MainLibraryFileName Condition="'$(MainLibraryFileName)' == ''">AndroidTestRunner.dll</MainLibraryFileName>
</PropertyGroup>
<ItemGroup Condition="'$(RunAOTCompilation)' == 'true'">
<AotInputAssemblies Include="$(PublishDir)\*.dll">
<AotArguments>@(MonoAOTCompilerDefaultAotArguments, ';')</AotArguments>
<ProcessArguments>@(MonoAOTCompilerDefaultProcessArguments, ';')</ProcessArguments>
</AotInputAssemblies>
</ItemGroup>

<Copy Condition="'$(ANDROID_OPENSSL_AAR)' != ''"
SourceFiles="$(ANDROID_OPENSSL_AAR)\prefab\modules\crypto\libs\android.$(AndroidAbi)\libcrypto.so"
Expand All @@ -51,17 +64,34 @@
DestinationFolder="$(PublishDir)" SkipUnchangedFiles="true"/>

<WriteLinesToFile File="$(PublishDir)xunit-excludes.txt" Lines="$(XunitExcludesTxtFileContent)" />


<MakeDir Directories="$(_MobileIntermediateOutputPath)"
Condition="'$(RunAOTCompilation)' == 'true'"/>
<RemoveDir Directories="$(BundleDir)" />

<MonoAOTCompiler Condition="'$(RunAOTCompilation)' == 'true'"
CompilerBinaryPath="$(MicrosoftNetCoreAppRuntimePackNativeDir)cross\$(PackageRID)\mono-aot-cross"
OutputDir="$(_MobileIntermediateOutputPath)"
Mode="Full"
OutputType="AsmOnly"
Assemblies="@(AotInputAssemblies)"
AotModulesTablePath="$(BundleDir)\modules.c"
UseLLVM="$(MonoEnableLLVM)"
LLVMPath="$(MicrosoftNetCoreAppRuntimePackNativeDir)cross\$(PackageRID)">
<Output TaskParameter="CompiledAssemblies" ItemName="BundleAssemblies" />
Comment thread
MaximLipnin marked this conversation as resolved.
Outdated
</MonoAOTCompiler>

<AndroidAppBuilderTask
RuntimeIdentifier="$(RuntimeIdentifier)"
ProjectName="$(AssemblyName)"
MonoRuntimeHeaders="$(MicrosoftNetCoreAppRuntimePackNativeDir)include\mono-2.0"
Assemblies="@(BundleAssemblies)"
MainLibraryFileName="$(MainLibraryFileName)"
ForceAOT="$(RunAOTCompilation)"
ForceInterpreter="$(MonoForceInterpreter)"
StripDebugSymbols="False"
OutputDir="$(BundleDir)"
SourceDir="$(PublishDir)">
AppDir="$(PublishDir)">
<Output TaskParameter="ApkPackageId" PropertyName="ApkPackageId" />
<Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
</AndroidAppBuilderTask>
Expand All @@ -80,10 +110,6 @@
<UsingTask Condition="'$(TargetOS)' == 'MacCatalyst' or '$(TargetOS)' == 'iOS' or '$(TargetOS)' == 'tvOS'"
TaskName="AppleAppBuilderTask"
AssemblyFile="$(AppleAppBuilderTasksAssemblyPath)" />
<UsingTask TaskName="MonoAOTCompiler"
AssemblyFile="$(MonoAOTCompilerTasksAssemblyPath)" />

<Import Project="$(MonoAOTCompilerDir)MonoAOTCompiler.props" />

<PropertyGroup>
<DevTeamProvisioning Condition="'$(TargetOS)' == 'MacCatalyst' and '$(DevTeamProvisioning)' == ''">-</DevTeamProvisioning>
Expand Down
18 changes: 14 additions & 4 deletions src/mono/sample/Android/AndroidSampleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.$(RuntimeIdentifier)\$(Configuration)\runtimes\android-$(TargetArchitecture)\</MicrosoftNetCoreAppRuntimePackDir>
<ForceAOT Condition="'$(ForceAOT)' == ''">false</ForceAOT>
</PropertyGroup>

<!-- Redirect 'dotnet publish' to in-tree runtime pack -->
Expand All @@ -23,6 +24,10 @@
<Import Project="$(RepoTasksDir)AotCompilerTask\MonoAOTCompiler.props" />
<UsingTask TaskName="AndroidAppBuilderTask" AssemblyFile="$(AndroidAppBuilderTasksAssemblyPath)"/>
<UsingTask TaskName="MonoAOTCompiler" AssemblyFile="$(MonoAOTCompilerTasksAssemblyPath)" />

<PropertyGroup Condition="'$(RunAOTCompilation)' == 'true'">
<_MobileIntermediateOutputPath>$(IntermediateOutputPath)mobile</_MobileIntermediateOutputPath>
</PropertyGroup>

<Target Name="BuildApp" AfterTargets="CopyFilesToPublishDirectory">
<PropertyGroup>
Expand All @@ -39,6 +44,8 @@
</AotInputAssemblies>
</ItemGroup>

<MakeDir Directories="$(_MobileIntermediateOutputPath)"
Condition="'$(ForceAOT)' == 'true'"/>
<RemoveDir Directories="$(ApkDir)" />

<PropertyGroup>
Expand All @@ -59,26 +66,29 @@
<Message Importance="High" Text="Path: $(PublishDir)" />
<Message Importance="High" Text="SourceDir: $(OutputPath)" />

<MonoAOTCompiler
Condition="'$(ForceAOT)' == 'true'"
<MonoAOTCompiler Condition="'$(ForceAOT)' == 'true'"
CompilerBinaryPath="$(MicrosoftNetCoreAppRuntimePackDir)native\cross\android-$(TargetArchitecture)\mono-aot-cross"
OutputDir="$(_MobileIntermediateOutputPath)"
Mode="Full"
OutputType="AsmOnly"
Assemblies="@(AotInputAssemblies)"
AotModulesTablePath="$(BundleDir)\modules.c"
UseLLVM="$(UseLLVM)"
LLVMPath="$(MicrosoftNetCoreAppRuntimePackDir)native\cross\android-$(TargetArchitecture)">
<Output TaskParameter="CompiledAssemblies" ItemName="BundleAssemblies" />
</MonoAOTCompiler>

<AndroidAppBuilderTask
RuntimeIdentifier="$(RuntimeIdentifier)"
SourceDir="$(PublishDir)"
ProjectName="HelloAndroid"
ForceInterpreter="$(MonoForceInterpreter)"
ForceAOT="$(ForceAOT)"
MonoRuntimeHeaders="$(MicrosoftNetCoreAppRuntimePackDir)\native\include\mono-2.0"
Assemblies="@(BundleAssemblies)"
MainLibraryFileName="$(AssemblyName).dll"
StripDebugSymbols="$(StripDebugSymbols)"
OutputDir="$(ApkDir)">
OutputDir="$(ApkDir)"
AppDir="$(PublishDir)">
<Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
<Output TaskParameter="ApkPackageId" PropertyName="ApkPackageId" />
</AndroidAppBuilderTask>
Expand Down
23 changes: 20 additions & 3 deletions src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,36 @@
public class AndroidAppBuilderTask : Task
{
[Required]
public string SourceDir { get; set; } = ""!;
public string MonoRuntimeHeaders { get; set; } = ""!;

/// <summary>
/// Target directory with *dll and other content to be AOT'd and/or bundled
/// </summary>
[Required]
public string MonoRuntimeHeaders { get; set; } = ""!;
public string AppDir { get; set; } = ""!;

/// <summary>
/// This library will be used as an entry-point (e.g. TestRunner.dll)
/// </summary>
public string MainLibraryFileName { get; set; } = ""!;

/// <summary>
/// List of paths to assemblies to be included in the app. For AOT builds the 'ObjectFile' metadata key needs to point to the object file.
/// </summary>
public ITaskItem[] Assemblies { get; set; } = Array.Empty<ITaskItem>();

/// <summary>
/// Prefer FullAOT mode for Emulator over JIT
/// </summary>
public bool ForceAOT { get; set; }

[Required]
public string RuntimeIdentifier { get; set; } = ""!;

[Required]
public string OutputDir { get; set; } = ""!;

[Required]
public string? ProjectName { get; set; }

public string? AndroidSdk { get; set; }
Expand Down Expand Up @@ -64,6 +78,7 @@ public override bool Execute()

var apkBuilder = new ApkBuilder();
apkBuilder.ProjectName = ProjectName;
apkBuilder.AppDir = AppDir;
apkBuilder.OutputDir = OutputDir;
apkBuilder.AndroidSdk = AndroidSdk;
apkBuilder.AndroidNdk = AndroidNdk;
Expand All @@ -74,7 +89,9 @@ public override bool Execute()
apkBuilder.NativeMainSource = NativeMainSource;
apkBuilder.KeyStorePath = KeyStorePath;
apkBuilder.ForceInterpreter = ForceInterpreter;
(ApkBundlePath, ApkPackageId) = apkBuilder.BuildApk(SourceDir, abi, MainLibraryFileName, MonoRuntimeHeaders);
apkBuilder.ForceAOT = ForceAOT;
apkBuilder.Assemblies = Assemblies;
(ApkBundlePath, ApkPackageId) = apkBuilder.BuildApk(abi, MainLibraryFileName, MonoRuntimeHeaders);

return true;
}
Expand Down
112 changes: 84 additions & 28 deletions src/tasks/AndroidAppBuilder/ApkBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;

public class ApkBuilder
{
private const string DefaultMinApiLevel = "21";

public string? ProjectName { get; set; }
public string? AppDir { get; set; }
public string? AndroidNdk { get; set; }
public string? AndroidSdk { get; set; }
public string? MinApiLevel { get; set; }
Expand All @@ -21,49 +23,57 @@ public class ApkBuilder
public string? NativeMainSource { get; set; }
public string? KeyStorePath { get; set; }
public bool ForceInterpreter { get; set; }
public bool ForceAOT { get; set; }
public ITaskItem[] Assemblies { get; set; } = Array.Empty<ITaskItem>();

public (string apk, string packageId) BuildApk(
string sourceDir,
string abi,
string entryPointLib,
string mainLibraryFileName,
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-v7a or arm64-v8a");
if (string.IsNullOrEmpty(AppDir) || !Directory.Exists(AppDir))
{
throw new ArgumentException($"AppDir='{AppDir}' is empty or doesn't exist");
}

string entryPointLibPath = "";
if (!string.IsNullOrEmpty(entryPointLib))
if (!string.IsNullOrEmpty(mainLibraryFileName) && !File.Exists(Path.Combine(AppDir, mainLibraryFileName)))
{
entryPointLibPath = Path.Combine(sourceDir, entryPointLib);
if (!File.Exists(entryPointLibPath))
throw new ArgumentException($"{entryPointLib} was not found in sourceDir='{sourceDir}'");
throw new ArgumentException($"MainLibraryFileName='{mainLibraryFileName}' was not found in AppDir='{AppDir}'");
}

if (string.IsNullOrEmpty(ProjectName))
if (string.IsNullOrEmpty(abi))
{
if (string.IsNullOrEmpty(entryPointLib))
throw new ArgumentException("ProjectName needs to be set if entryPointLib is empty.");
else
ProjectName = Path.GetFileNameWithoutExtension(entryPointLib);
throw new ArgumentException("abi should not be empty (e.g. x86, x86_64, armeabi-v7a or arm64-v8a");
}

if (ProjectName.Contains(' '))
throw new ArgumentException($"ProjectName='{ProjectName}' shouldn't not contain spaces.");
if (!string.IsNullOrEmpty(ProjectName) && ProjectName.Contains(' '))
{
throw new ArgumentException($"ProjectName='{ProjectName}' should not not contain spaces.");
}

if (string.IsNullOrEmpty(AndroidSdk))
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).");
}

if (ForceInterpreter && ForceAOT)
{
throw new InvalidOperationException("Interpreter and AOT cannot be enabled at the same time");
}

// Try to get the latest build-tools version if not specified
if (string.IsNullOrEmpty(BuildToolsVersion))
Expand All @@ -88,7 +98,25 @@ public class ApkBuilder

string buildToolsFolder = Path.Combine(AndroidSdk, "build-tools", BuildToolsVersion);
if (!Directory.Exists(buildToolsFolder))
{
throw new ArgumentException($"{buildToolsFolder} was not found.");
}

var assemblerFiles = new List<string>();
foreach (ITaskItem file in Assemblies)
{
// use AOT files if available
var obj = file.GetMetadata("AssemblerFile");
if (!string.IsNullOrEmpty(obj))
{
assemblerFiles.Add(obj);
}
}

if (ForceAOT && !assemblerFiles.Any())
{
throw new InvalidOperationException("Need list of AOT files.");
}

Directory.CreateDirectory(OutputDir);
Directory.CreateDirectory(Path.Combine(OutputDir, "bin"));
Expand All @@ -105,7 +133,7 @@ public class ApkBuilder

// Copy sourceDir to OutputDir/assets-tozip (ignore native files)
// these files then will be zipped and copied to apk/assets/assets.zip
Utils.DirectoryCopy(sourceDir, Path.Combine(OutputDir, "assets-tozip"), file =>
Utils.DirectoryCopy(AppDir, Path.Combine(OutputDir, "assets-tozip"), file =>
{
string fileName = Path.GetFileName(file);
string extension = Path.GetExtension(file);
Expand Down Expand Up @@ -143,13 +171,41 @@ public class ApkBuilder

// 1. Build libmonodroid.so` via cmake

string monoRuntimeLib = Path.Combine(sourceDir, "libmonosgen-2.0.a");
string monoRuntimeLib = Path.Combine(AppDir, "libmonosgen-2.0.a");
if (!File.Exists(monoRuntimeLib))
throw new ArgumentException($"libmonosgen-2.0.a was not found in {sourceDir}");
{
throw new ArgumentException($"libmonosgen-2.0.a was not found in {AppDir}");
}
else
{
monoRuntimeLib = $" {monoRuntimeLib}{Environment.NewLine}";
}

string aotSources = "";
foreach (string asm in assemblerFiles)
{
// these libraries are linked via modules.c
aotSources += $" {asm}{Environment.NewLine}";
}

string cmakeLists = Utils.GetEmbeddedResource("CMakeLists-android.txt")
.Replace("%MonoInclude%", monoRuntimeHeaders)
.Replace("%NativeLibrariesToLink%", monoRuntimeLib);
.Replace("%NativeLibrariesToLink%", monoRuntimeLib)
.Replace("%AotSources%", aotSources)
.Replace("%AotModulesSource%", string.IsNullOrEmpty(aotSources) ? "" : "modules.c");

string defines = "";
if (ForceInterpreter)
{
defines = "add_definitions(-DFORCE_INTERPRETER=1)";
}
else if (ForceAOT)
{
defines = "add_definitions(-DFORCE_AOT=1)";
}

cmakeLists = cmakeLists.Replace("%Defines%", defines);

File.WriteAllText(Path.Combine(OutputDir, "CMakeLists.txt"), cmakeLists);

File.WriteAllText(Path.Combine(OutputDir, "monodroid.c"), Utils.GetEmbeddedResource("monodroid.c"));
Expand Down Expand Up @@ -186,13 +242,13 @@ public class ApkBuilder

File.WriteAllText(javaActivityPath,
Utils.GetEmbeddedResource("MainActivity.java")
.Replace("%EntryPointLibName%", Path.GetFileName(entryPointLib)));
.Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName)));
if (!string.IsNullOrEmpty(NativeMainSource))
File.Copy(NativeMainSource, javaActivityPath, true);

string monoRunner = Utils.GetEmbeddedResource("MonoRunner.java")
.Replace("%EntryPointLibName%", Path.GetFileName(entryPointLib))
.Replace("%ForceInterpreter%", ForceInterpreter.ToString().ToLower());
.Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName));

File.WriteAllText(monoRunnerPath, monoRunner);

File.WriteAllText(Path.Combine(OutputDir, "AndroidManifest.xml"),
Expand All @@ -213,7 +269,7 @@ public class ApkBuilder

var dynamicLibs = new List<string>();
dynamicLibs.Add(Path.Combine(OutputDir, "monodroid", "libmonodroid.so"));
dynamicLibs.AddRange(Directory.GetFiles(sourceDir, "*.so").Where(file => Path.GetFileName(file) != "libmonodroid.so"));
dynamicLibs.AddRange(Directory.GetFiles(AppDir, "*.so").Where(file => Path.GetFileName(file) != "libmonodroid.so"));

// add all *.so files to lib/%abi%/
Directory.CreateDirectory(Path.Combine(OutputDir, "lib", abi));
Expand Down
Loading