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:
parent
11c2e4c3b8
commit
0fa0b905f6
17 changed files with 1042 additions and 2 deletions
|
@ -118,6 +118,57 @@
|
||||||
<Error Condition="'$(TestRunExitCode)' != '0'" Text="$(TestRunErrorMessage)" />
|
<Error Condition="'$(TestRunExitCode)' != '0'" Text="$(TestRunErrorMessage)" />
|
||||||
</Target>
|
</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.
|
<!-- 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) -->
|
This target is executed once build is done for a test lib (after CopyFilesToOutputDirectory target) -->
|
||||||
<UsingTask TaskName="AppleAppBuilderTask"
|
<UsingTask TaskName="AppleAppBuilderTask"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<DefineConstants>$(DefineConstants);XMLSERIALIZERGENERATORTESTS</DefineConstants>
|
<DefineConstants>$(DefineConstants);XMLSERIALIZERGENERATORTESTS</DefineConstants>
|
||||||
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
|
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
|
||||||
<CoverageSupported>false</CoverageSupported>
|
<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>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Reuse the same runtimeconfig used by MSBuild. -->
|
<!-- Reuse the same runtimeconfig used by MSBuild. -->
|
||||||
|
|
|
@ -936,9 +936,18 @@
|
||||||
Targets="Restore;Build" />
|
Targets="Restore;Build" />
|
||||||
</Target>
|
</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. -->
|
<!-- Ordering matters! Overwriting the Build target. -->
|
||||||
<!-- General targets -->
|
<!-- General targets -->
|
||||||
<Target Name="Build" DependsOnTargets="BuildMonoRuntimeUnix;BuildMonoRuntimeWindows;BuildAppleAppBuilder">
|
<Target Name="Build" DependsOnTargets="BuildMonoRuntimeUnix;BuildMonoRuntimeWindows;BuildAppleAppBuilder;BuildAndroidAppBuilder">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x64'">$(MonoObjDir)x64\Bin\$(Configuration)\mono-2.0-sgen.dll</_MonoRuntimeFilePath>
|
<_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>
|
<_MonoRuntimeFilePath Condition="'$(TargetsWindows)' == 'true' and '$(Platform)' == 'x86'">$(MonoObjDir)Win32\Bin\$(Configuration)\mono-2.0-sgen.dll</_MonoRuntimeFilePath>
|
||||||
|
|
66
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs
Normal file
66
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
24
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj
Normal file
24
src/mono/msbuild/AndroidAppBuilder/AndroidAppBuilder.csproj
Normal 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>
|
223
src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs
Normal file
223
src/mono/msbuild/AndroidAppBuilder/ApkBuilder.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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)
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
98
src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java
Normal file
98
src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java
Normal 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);
|
||||||
|
}
|
199
src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c
Normal file
199
src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c
Normal 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 ();
|
||||||
|
}
|
114
src/mono/msbuild/AndroidAppBuilder/Utils.cs
Normal file
114
src/mono/msbuild/AndroidAppBuilder/Utils.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
89
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs
Normal file
89
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs
Normal 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");
|
||||||
|
}
|
11
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj
Normal file
11
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.csproj
Normal 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>
|
21
src/mono/netcore/sample/Android/Makefile
Normal file
21
src/mono/netcore/sample/Android/Makefile
Normal 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
|
14
src/mono/netcore/sample/Android/Program.cs
Normal file
14
src/mono/netcore/sample/Android/Program.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
62
src/mono/netcore/sample/Android/Program.csproj
Normal file
62
src/mono/netcore/sample/Android/Program.csproj
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue