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

NetTraceToMibcConverter - Used in profiled AOT scenarios where a .nettrace file is given as input and is converted to a .mibc file that can be fed into the AOT compiler. This previously was in the AotCompiler task, but for clarity purposes is now separated out. Streamline Android testing targets - The testing targets function the same, but are now structured similarly to iOS and Wasm. - Introduced new testing properties to support profiled AOT: NetTraceFilePath - The path to a .nettrace file that will be converted into a .mibc file and fed into the aot compiler RuntimeComponents - The list of native components to include in the test app build (diagnostics_tracing) DiagnosticsPorts - The ip address:port where the runtime will listen when running diagnostic tooling DiagnosticStartupMode - The mode the runtime will use at startup for diagnostic scenarios. Suspend will halt the app very early and wait, while nosuspend will wait for a connection, but not halt the runtime Co-authored-by: Mitchell Hwang <16830051+mdh1418@users.noreply.github.com>
638 lines
27 KiB
C#
638 lines
27 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Build.Framework;
|
|
using Microsoft.Build.Utilities;
|
|
|
|
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; }
|
|
public string? BuildApiLevel { get; set; }
|
|
public string? BuildToolsVersion { get; set; }
|
|
public string OutputDir { get; set; } = ""!;
|
|
public bool StripDebugSymbols { get; set; }
|
|
public string? NativeMainSource { get; set; }
|
|
public bool IncludeNetworkSecurityConfig { get; set; }
|
|
public string? KeyStorePath { get; set; }
|
|
public bool ForceInterpreter { get; set; }
|
|
public bool ForceAOT { get; set; }
|
|
public bool ForceFullAOT { get; set; }
|
|
public ITaskItem[] EnvironmentVariables { get; set; } = Array.Empty<ITaskItem>();
|
|
public bool InvariantGlobalization { get; set; }
|
|
public bool EnableRuntimeLogging { get; set; }
|
|
public bool StaticLinkedRuntime { get; set; }
|
|
public string? RuntimeComponents { get; set; }
|
|
public string? DiagnosticPorts { get; set; }
|
|
public ITaskItem[] Assemblies { get; set; } = Array.Empty<ITaskItem>();
|
|
|
|
private TaskLoggingHelper logger;
|
|
|
|
public ApkBuilder(TaskLoggingHelper logger)
|
|
{
|
|
this.logger = logger;
|
|
}
|
|
|
|
public (string apk, string packageId) BuildApk(
|
|
string abi,
|
|
string mainLibraryFileName,
|
|
string monoRuntimeHeaders)
|
|
{
|
|
if (string.IsNullOrEmpty(AppDir) || !Directory.Exists(AppDir))
|
|
{
|
|
throw new ArgumentException($"AppDir='{AppDir}' is empty or doesn't exist");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(mainLibraryFileName) && !File.Exists(Path.Combine(AppDir, mainLibraryFileName)))
|
|
{
|
|
throw new ArgumentException($"MainLibraryFileName='{mainLibraryFileName}' was not found in AppDir='{AppDir}'");
|
|
}
|
|
|
|
var networkSecurityConfigFilePath = Path.Combine(AppDir, "res", "xml", "network_security_config.xml");
|
|
if (IncludeNetworkSecurityConfig && !File.Exists(networkSecurityConfigFilePath))
|
|
{
|
|
throw new ArgumentException($"IncludeNetworkSecurityConfig is set but the file '{networkSecurityConfigFilePath}' was not found");
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(abi))
|
|
{
|
|
throw new ArgumentException("abi should not be empty (e.g. x86, x86_64, armeabi-v7a or arm64-v8a");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(ProjectName) && ProjectName.Contains(' '))
|
|
{
|
|
throw new ArgumentException($"ProjectName='{ProjectName}' should not 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).");
|
|
}
|
|
|
|
if (ForceInterpreter && ForceAOT)
|
|
{
|
|
throw new InvalidOperationException("Interpreter and AOT cannot be enabled at the same time");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(DiagnosticPorts))
|
|
{
|
|
bool validDiagnosticsConfig = false;
|
|
|
|
if (string.IsNullOrEmpty(RuntimeComponents))
|
|
validDiagnosticsConfig = false;
|
|
else if (RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
|
|
validDiagnosticsConfig = true;
|
|
else if (RuntimeComponents.Contains("diagnostics_tracing", StringComparison.OrdinalIgnoreCase))
|
|
validDiagnosticsConfig = true;
|
|
|
|
if (!validDiagnosticsConfig)
|
|
throw new ArgumentException("Using DiagnosticPorts require diagnostics_tracing runtime component.");
|
|
}
|
|
|
|
// 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.");
|
|
}
|
|
|
|
var assemblerFiles = new StringBuilder();
|
|
var assemblerFilesToLink = new StringBuilder();
|
|
var aotLibraryFiles = new List<string>();
|
|
foreach (ITaskItem file in Assemblies)
|
|
{
|
|
// use AOT files if available
|
|
var obj = file.GetMetadata("AssemblerFile");
|
|
var llvmObj = file.GetMetadata("LlvmObjectFile");
|
|
var lib = file.GetMetadata("LibraryFile");
|
|
|
|
if (!string.IsNullOrEmpty(obj))
|
|
{
|
|
var name = Path.GetFileNameWithoutExtension(obj);
|
|
assemblerFiles.AppendLine($"add_library({name} OBJECT {obj})");
|
|
assemblerFilesToLink.AppendLine($" {name}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(llvmObj))
|
|
{
|
|
var name = Path.GetFileNameWithoutExtension(llvmObj);
|
|
assemblerFilesToLink.AppendLine($" {llvmObj}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(lib))
|
|
{
|
|
aotLibraryFiles.Add(lib);
|
|
}
|
|
}
|
|
|
|
if (ForceAOT && assemblerFiles.Length == 0 && aotLibraryFiles.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("Need list of AOT files.");
|
|
}
|
|
|
|
Directory.CreateDirectory(OutputDir);
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "bin"));
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "obj"));
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "assets-tozip"));
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "assets"));
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "res"));
|
|
|
|
var extensionsToIgnore = new List<string> { ".so", ".a" };
|
|
if (StripDebugSymbols)
|
|
{
|
|
extensionsToIgnore.Add(".pdb");
|
|
extensionsToIgnore.Add(".dbg");
|
|
}
|
|
|
|
// Copy sourceDir to OutputDir/assets-tozip (ignore native files)
|
|
// these files then will be zipped and copied to apk/assets/assets.zip
|
|
var assetsToZipDirectory = Path.Combine(OutputDir, "assets-tozip");
|
|
|
|
Utils.DirectoryCopy(AppDir, assetsToZipDirectory, file =>
|
|
{
|
|
string fileName = Path.GetFileName(file);
|
|
string extension = Path.GetExtension(file);
|
|
|
|
if (extensionsToIgnore.Contains(extension))
|
|
{
|
|
// ignore native files, those go to lib/%abi%
|
|
// also, aapt is not happy about zip files
|
|
return false;
|
|
}
|
|
if (fileName.StartsWith("."))
|
|
{
|
|
// aapt complains on such files
|
|
return false;
|
|
}
|
|
if (file.Contains("/res/"))
|
|
{
|
|
// exclude everything in the `res` folder
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// copy the res directory as is
|
|
if (Directory.Exists(Path.Combine(AppDir, "res")))
|
|
{
|
|
Utils.DirectoryCopy(Path.Combine(AppDir, "res"), Path.Combine(OutputDir, "res"));
|
|
}
|
|
|
|
// add AOT .so libraries
|
|
foreach (var aotlib in aotLibraryFiles)
|
|
{
|
|
File.Copy(aotlib, Path.Combine(assetsToZipDirectory, Path.GetFileName(aotlib)));
|
|
}
|
|
|
|
// tools:
|
|
string dx = Path.Combine(buildToolsFolder, "dx");
|
|
string d8 = Path.Combine(buildToolsFolder, "d8");
|
|
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 javac = "javac";
|
|
string cmake = "cmake";
|
|
string zip = "zip";
|
|
|
|
Utils.RunProcess(logger, zip, workingDir: assetsToZipDirectory, args: "-q -r ../assets/assets.zip .");
|
|
Directory.Delete(assetsToZipDirectory, true);
|
|
|
|
if (!File.Exists(androidJar))
|
|
throw new ArgumentException($"API level={BuildApiLevel} is not downloaded in Android SDK");
|
|
|
|
// 1. Build libmonodroid.so` via cmake
|
|
|
|
string nativeLibraries = "";
|
|
string monoRuntimeLib = "";
|
|
if (StaticLinkedRuntime)
|
|
{
|
|
monoRuntimeLib = Path.Combine(AppDir, "libmonosgen-2.0.a");
|
|
}
|
|
else
|
|
{
|
|
monoRuntimeLib = Path.Combine(AppDir, "libmonosgen-2.0.so");
|
|
}
|
|
|
|
if (!File.Exists(monoRuntimeLib))
|
|
{
|
|
throw new ArgumentException($"{monoRuntimeLib} was not found");
|
|
}
|
|
else
|
|
{
|
|
nativeLibraries += $"{monoRuntimeLib}{Environment.NewLine}";
|
|
}
|
|
|
|
if (StaticLinkedRuntime)
|
|
{
|
|
string[] staticComponentStubLibs = Directory.GetFiles(AppDir, "libmono-component-*-stub-static.a");
|
|
bool staticLinkAllComponents = false;
|
|
string[] staticLinkedComponents = Array.Empty<string>();
|
|
|
|
if (!string.IsNullOrEmpty(RuntimeComponents) && RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
|
|
staticLinkAllComponents = true;
|
|
else if (!string.IsNullOrEmpty(RuntimeComponents))
|
|
staticLinkedComponents = RuntimeComponents.Split(";");
|
|
|
|
// by default, component stubs will be linked and depending on how mono runtime has been build,
|
|
// stubs can disable or dynamic load components.
|
|
foreach (string staticComponentStubLib in staticComponentStubLibs)
|
|
{
|
|
string componentLibToLink = staticComponentStubLib;
|
|
if (staticLinkAllComponents)
|
|
{
|
|
// static link component.
|
|
componentLibToLink = componentLibToLink.Replace("-stub-static.a", "-static.a", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else
|
|
{
|
|
foreach (string staticLinkedComponent in staticLinkedComponents)
|
|
{
|
|
if (componentLibToLink.Contains(staticLinkedComponent, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// static link component.
|
|
componentLibToLink = componentLibToLink.Replace("-stub-static.a", "-static.a", StringComparison.OrdinalIgnoreCase);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if lib doesn't exist (primarily due to runtime build without static lib support), fallback linking stub lib.
|
|
if (!File.Exists(componentLibToLink))
|
|
{
|
|
logger.LogMessage(MessageImportance.High, $"\nCouldn't find static component library: {componentLibToLink}, linking static component stub library: {staticComponentStubLib}.\n");
|
|
componentLibToLink = staticComponentStubLib;
|
|
}
|
|
|
|
nativeLibraries += $" {componentLibToLink}{Environment.NewLine}";
|
|
}
|
|
|
|
// There's a circular dependency between static mono runtime lib and static component libraries.
|
|
// Adding mono runtime lib before and after component libs will resolve issues with undefined symbols
|
|
// due to circular dependency.
|
|
nativeLibraries += $" {monoRuntimeLib}{Environment.NewLine}";
|
|
}
|
|
|
|
nativeLibraries += assemblerFilesToLink.ToString();
|
|
|
|
string aotSources = assemblerFiles.ToString();
|
|
|
|
string cmakeLists = Utils.GetEmbeddedResource("CMakeLists-android.txt")
|
|
.Replace("%MonoInclude%", monoRuntimeHeaders)
|
|
.Replace("%NativeLibrariesToLink%", nativeLibraries)
|
|
.Replace("%AotSources%", aotSources)
|
|
.Replace("%AotModulesSource%", string.IsNullOrEmpty(aotSources) ? "" : "modules.c");
|
|
|
|
var defines = new StringBuilder();
|
|
if (ForceInterpreter)
|
|
{
|
|
defines.AppendLine("add_definitions(-DFORCE_INTERPRETER=1)");
|
|
}
|
|
else if (ForceAOT)
|
|
{
|
|
defines.AppendLine("add_definitions(-DFORCE_AOT=1)");
|
|
if (aotLibraryFiles.Count == 0)
|
|
{
|
|
defines.AppendLine("add_definitions(-DSTATIC_AOT=1)");
|
|
}
|
|
}
|
|
|
|
if (ForceFullAOT)
|
|
{
|
|
defines.AppendLine("add_definitions(-DFULL_AOT=1)");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(DiagnosticPorts))
|
|
{
|
|
defines.AppendLine("add_definitions(-DDIAGNOSTIC_PORTS=\"" + DiagnosticPorts + "\")");
|
|
}
|
|
|
|
cmakeLists = cmakeLists.Replace("%Defines%", defines.ToString());
|
|
|
|
File.WriteAllText(Path.Combine(OutputDir, "CMakeLists.txt"), cmakeLists);
|
|
|
|
File.WriteAllText(Path.Combine(OutputDir, "monodroid.c"), Utils.GetEmbeddedResource("monodroid.c"));
|
|
|
|
string cmakeGenArgs = $"-DCMAKE_TOOLCHAIN_FILE={androidToolchain} -DANDROID_ABI=\"{abi}\" -DANDROID_STL=none " +
|
|
$"-DANDROID_PLATFORM=android-{MinApiLevel} -B monodroid";
|
|
|
|
string cmakeBuildArgs = "--build monodroid";
|
|
|
|
if (StripDebugSymbols)
|
|
{
|
|
// Use "-s" to strip debug symbols, it complains it's unused but it works
|
|
cmakeGenArgs+= " -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_FLAGS=\"-s -Wno-unused-command-line-argument\"";
|
|
cmakeBuildArgs += " --config MinSizeRel";
|
|
}
|
|
else
|
|
{
|
|
cmakeGenArgs += " -DCMAKE_BUILD_TYPE=Debug";
|
|
cmakeBuildArgs += " --config Debug";
|
|
}
|
|
|
|
Utils.RunProcess(logger, cmake, workingDir: OutputDir, args: cmakeGenArgs);
|
|
Utils.RunProcess(logger, cmake, workingDir: OutputDir, args: cmakeBuildArgs);
|
|
|
|
// 2. Compile Java files
|
|
|
|
string javaSrcFolder = Path.Combine(OutputDir, "src", "net", "dot");
|
|
Directory.CreateDirectory(javaSrcFolder);
|
|
|
|
string javaActivityPath = Path.Combine(javaSrcFolder, "MainActivity.java");
|
|
string monoRunnerPath = Path.Combine(javaSrcFolder, "MonoRunner.java");
|
|
|
|
Regex checkNumerics = new Regex(@"\.(\d)");
|
|
if (!string.IsNullOrEmpty(ProjectName) && checkNumerics.IsMatch(ProjectName))
|
|
ProjectName = checkNumerics.Replace(ProjectName, @"_$1");
|
|
|
|
string packageId = $"net.dot.{ProjectName}";
|
|
|
|
File.WriteAllText(javaActivityPath,
|
|
Utils.GetEmbeddedResource("MainActivity.java")
|
|
.Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName)));
|
|
if (!string.IsNullOrEmpty(NativeMainSource))
|
|
File.Copy(NativeMainSource, javaActivityPath, true);
|
|
|
|
string networkSecurityConfigAttribute =
|
|
IncludeNetworkSecurityConfig
|
|
? "a:networkSecurityConfig=\"@xml/network_security_config\""
|
|
: string.Empty;
|
|
|
|
string envVariables = "";
|
|
foreach (ITaskItem item in EnvironmentVariables)
|
|
{
|
|
string name = item.ItemSpec;
|
|
string value = item.GetMetadata("Value");
|
|
envVariables += $"\t\tsetEnv(\"{name}\", \"{value}\");\n";
|
|
}
|
|
|
|
string monoRunner = Utils.GetEmbeddedResource("MonoRunner.java")
|
|
.Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName))
|
|
.Replace("%EnvVariables%", envVariables);
|
|
|
|
File.WriteAllText(monoRunnerPath, monoRunner);
|
|
|
|
File.WriteAllText(Path.Combine(OutputDir, "AndroidManifest.xml"),
|
|
Utils.GetEmbeddedResource("AndroidManifest.xml")
|
|
.Replace("%PackageName%", packageId)
|
|
.Replace("%NetworkSecurityConfig%", networkSecurityConfigAttribute)
|
|
.Replace("%MinSdkLevel%", MinApiLevel));
|
|
|
|
string javaCompilerArgs = $"-d obj -classpath src -bootclasspath {androidJar} -source 1.8 -target 1.8 ";
|
|
Utils.RunProcess(logger, javac, javaCompilerArgs + javaActivityPath, workingDir: OutputDir);
|
|
Utils.RunProcess(logger, javac, javaCompilerArgs + monoRunnerPath, workingDir: OutputDir);
|
|
|
|
if (File.Exists(d8))
|
|
{
|
|
string[] classFiles = Directory.GetFiles(Path.Combine(OutputDir, "obj"), "*.class", SearchOption.AllDirectories);
|
|
|
|
if (!classFiles.Any())
|
|
throw new InvalidOperationException("Didn't find any .class files");
|
|
|
|
Utils.RunProcess(logger, d8, $"--no-desugaring {string.Join(" ", classFiles)}", workingDir: OutputDir);
|
|
}
|
|
else
|
|
{
|
|
Utils.RunProcess(logger, dx, "--dex --output=classes.dex obj", workingDir: OutputDir);
|
|
}
|
|
|
|
// 3. Generate APK
|
|
|
|
string debugModeArg = StripDebugSymbols ? string.Empty : "--debug-mode";
|
|
string apkFile = Path.Combine(OutputDir, "bin", $"{ProjectName}.unaligned.apk");
|
|
string resources = IncludeNetworkSecurityConfig ? "-S res" : string.Empty;
|
|
Utils.RunProcess(logger, aapt, $"package -f -m -F {apkFile} -A assets {resources} -M AndroidManifest.xml -I {androidJar} {debugModeArg}", workingDir: OutputDir);
|
|
|
|
var dynamicLibs = new List<string>();
|
|
dynamicLibs.Add(Path.Combine(OutputDir, "monodroid", "libmonodroid.so"));
|
|
dynamicLibs.AddRange(Directory.GetFiles(AppDir, "*.so").Where(file => Path.GetFileName(file) != "libmonodroid.so"));
|
|
|
|
// add all *.so files to lib/%abi%/
|
|
|
|
string[] dynamicLinkedComponents = Array.Empty<string>();
|
|
bool dynamicLinkAllComponents = false;
|
|
if (!StaticLinkedRuntime && !string.IsNullOrEmpty(RuntimeComponents) && RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
|
|
dynamicLinkAllComponents = true;
|
|
if (!string.IsNullOrEmpty(RuntimeComponents) && !StaticLinkedRuntime)
|
|
dynamicLinkedComponents = RuntimeComponents.Split(";");
|
|
|
|
Directory.CreateDirectory(Path.Combine(OutputDir, "lib", abi));
|
|
foreach (var dynamicLib in dynamicLibs)
|
|
{
|
|
string dynamicLibName = Path.GetFileName(dynamicLib);
|
|
string destRelative = Path.Combine("lib", abi, dynamicLibName);
|
|
|
|
if (dynamicLibName == "libmonosgen-2.0.so" && StaticLinkedRuntime)
|
|
{
|
|
// we link mono runtime statically into libmonodroid.so
|
|
// make sure dynamic runtime is not included in package.
|
|
if (File.Exists(destRelative))
|
|
File.Delete(destRelative);
|
|
continue;
|
|
}
|
|
|
|
if (dynamicLibName.Contains("libmono-component-", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
bool includeComponent = dynamicLinkAllComponents;
|
|
if (!StaticLinkedRuntime && !includeComponent)
|
|
{
|
|
foreach (string dynamicLinkedComponent in dynamicLinkedComponents)
|
|
{
|
|
if (dynamicLibName.Contains(dynamicLinkedComponent, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
includeComponent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!includeComponent)
|
|
{
|
|
// make sure dynamic component is not included in package.
|
|
if (File.Exists(destRelative))
|
|
File.Delete(destRelative);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// NOTE: we can run android-strip tool from NDK to shrink native binaries here even more.
|
|
|
|
File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
|
|
Utils.RunProcess(logger, aapt, $"add {apkFile} {destRelative}", workingDir: OutputDir);
|
|
}
|
|
Utils.RunProcess(logger, aapt, $"add {apkFile} classes.dex", workingDir: OutputDir);
|
|
|
|
// 4. Align APK
|
|
|
|
string alignedApk = Path.Combine(OutputDir, "bin", $"{ProjectName}.apk");
|
|
AlignApk(apkFile, alignedApk, zipalign);
|
|
// we don't need the unaligned one any more
|
|
File.Delete(apkFile);
|
|
|
|
// 5. Generate key (if needed) & sign the apk
|
|
SignApk(alignedApk, apksigner);
|
|
|
|
logger.LogMessage(MessageImportance.High, $"\nAPK size: {(new FileInfo(alignedApk).Length / 1000_000.0):0.#} Mb.\n");
|
|
|
|
return (alignedApk, packageId);
|
|
}
|
|
|
|
private void AlignApk(string unalignedApkPath, string apkOutPath, string zipalign)
|
|
{
|
|
Utils.RunProcess(logger, zipalign, $"-v 4 {unalignedApkPath} {apkOutPath}", workingDir: OutputDir);
|
|
}
|
|
|
|
private void SignApk(string apkPath, string apksigner)
|
|
{
|
|
string defaultKey = Path.Combine(OutputDir, "debug.keystore");
|
|
string signingKey = string.IsNullOrEmpty(KeyStorePath) ?
|
|
defaultKey : Path.Combine(KeyStorePath, "debug.keystore");
|
|
|
|
if (!File.Exists(signingKey))
|
|
{
|
|
Utils.RunProcess(logger, "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);
|
|
}
|
|
else if (Path.GetFullPath(signingKey) != Path.GetFullPath(defaultKey))
|
|
{
|
|
File.Copy(signingKey, Path.Combine(OutputDir, "debug.keystore"));
|
|
}
|
|
Utils.RunProcess(logger, apksigner, $"sign --min-sdk-version {MinApiLevel} --ks debug.keystore " +
|
|
$"--ks-pass pass:android --key-pass pass:android {apkPath}", workingDir: OutputDir);
|
|
}
|
|
|
|
public void ZipAndSignApk(string apkPath)
|
|
{
|
|
if (string.IsNullOrEmpty(AndroidSdk))
|
|
AndroidSdk = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT");
|
|
|
|
if (string.IsNullOrEmpty(AndroidSdk) || !Directory.Exists(AndroidSdk))
|
|
throw new ArgumentException($"Android SDK='{AndroidSdk}' was not found or incorrect (can be set via ANDROID_SDK_ROOT envvar).");
|
|
|
|
if (string.IsNullOrEmpty(BuildToolsVersion))
|
|
BuildToolsVersion = GetLatestBuildTools(AndroidSdk);
|
|
|
|
if (string.IsNullOrEmpty(MinApiLevel))
|
|
MinApiLevel = DefaultMinApiLevel;
|
|
|
|
string buildToolsFolder = Path.Combine(AndroidSdk, "build-tools", BuildToolsVersion);
|
|
string zipalign = Path.Combine(buildToolsFolder, "zipalign");
|
|
string apksigner = Path.Combine(buildToolsFolder, "apksigner");
|
|
|
|
string alignedApkPath = $"{apkPath}.aligned";
|
|
AlignApk(apkPath, alignedApkPath, zipalign);
|
|
logger.LogMessage(MessageImportance.High, $"\nMoving '{alignedApkPath}' to '{apkPath}'.\n");
|
|
File.Move(alignedApkPath, apkPath, overwrite: true);
|
|
SignApk(apkPath, apksigner);
|
|
}
|
|
|
|
public void ReplaceFileInApk(string file)
|
|
{
|
|
if (string.IsNullOrEmpty(AndroidSdk))
|
|
AndroidSdk = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT");
|
|
|
|
if (string.IsNullOrEmpty(AndroidSdk) || !Directory.Exists(AndroidSdk))
|
|
throw new ArgumentException($"Android SDK='{AndroidSdk}' was not found or incorrect (can be set via ANDROID_SDK_ROOT envvar).");
|
|
|
|
if (string.IsNullOrEmpty(BuildToolsVersion))
|
|
BuildToolsVersion = GetLatestBuildTools(AndroidSdk);
|
|
|
|
if (string.IsNullOrEmpty(MinApiLevel))
|
|
MinApiLevel = DefaultMinApiLevel;
|
|
|
|
string buildToolsFolder = Path.Combine(AndroidSdk, "build-tools", BuildToolsVersion);
|
|
string aapt = Path.Combine(buildToolsFolder, "aapt");
|
|
string apksigner = Path.Combine(buildToolsFolder, "apksigner");
|
|
|
|
string apkPath;
|
|
if (string.IsNullOrEmpty(ProjectName))
|
|
apkPath = Directory.GetFiles(Path.Combine(OutputDir, "bin"), "*.apk").First();
|
|
else
|
|
apkPath = Path.Combine(OutputDir, "bin", $"{ProjectName}.apk");
|
|
|
|
if (!File.Exists(apkPath))
|
|
throw new Exception($"{apkPath} was not found");
|
|
|
|
Utils.RunProcess(logger, aapt, $"remove -v bin/{Path.GetFileName(apkPath)} {file}", workingDir: OutputDir);
|
|
Utils.RunProcess(logger, aapt, $"add -v bin/{Path.GetFileName(apkPath)} {file}", workingDir: OutputDir);
|
|
|
|
// we need to re-sign the apk
|
|
SignApk(apkPath, apksigner);
|
|
}
|
|
|
|
/// <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); return version; })
|
|
.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();
|
|
}
|
|
}
|