diff --git a/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidApkFileReplacerTask.cs b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidApkFileReplacerTask.cs new file mode 100644 index 0000000000000..e1d83132f0292 --- /dev/null +++ b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidApkFileReplacerTask.cs @@ -0,0 +1,38 @@ +// 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.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class AndroidApkFileReplacerTask : Task +{ + [Required] + public string FilePath { get; set; } = ""!; + + [Required] + public string OutputDir { get; set; } = ""!; + + public string? AndroidSdk { get; set; } + + public string? MinApiLevel { get; set; } + + public string? BuildToolsVersion { get; set; } + + public string? KeyStorePath { get; set; } + + + public override bool Execute() + { + var apkBuilder = new ApkBuilder(); + apkBuilder.OutputDir = OutputDir; + apkBuilder.AndroidSdk = AndroidSdk; + apkBuilder.MinApiLevel = MinApiLevel; + apkBuilder.BuildToolsVersion = BuildToolsVersion; + apkBuilder.KeyStorePath = KeyStorePath; + apkBuilder.ReplaceFileInApk(FilePath); + return true; + } +} diff --git a/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidAppBuilder.cs b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidAppBuilder.cs index 40f1615022b76..f9c88cb5a1d95 100644 --- a/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidAppBuilder.cs +++ b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/AndroidAppBuilder.cs @@ -23,9 +23,10 @@ public class AndroidAppBuilderTask : Task [Required] public string RuntimeIdentifier { get; set; } = ""!; - public string? ProjectName { get; set; } + [Required] + public string OutputDir { get; set; } = ""!; - public string? OutputDir { get; set; } + public string? ProjectName { get; set; } public string? AndroidSdk { get; set; } @@ -78,20 +79,13 @@ public override bool Execute() return true; } - private string DetermineAbi() - { - switch (RuntimeIdentifier) + private string DetermineAbi() => + RuntimeIdentifier switch { - case "android-x86": - return "x86"; - case "android-x64": - return "x86_64"; - case "android-arm": - return "armeabi-v7a"; - case "android-arm64": - return "arm64-v8a"; - default: - throw new ArgumentException(RuntimeIdentifier + " is not supported for Android"); - } - } + "android-x86" => "x86", + "android-x64" => "x86_64", + "android-arm" => "armeabi-v7a", + "android-arm64" => "arm64-v8a", + _ => throw new ArgumentException($"{RuntimeIdentifier} is not supported for Android"), + }; } diff --git a/tools-local/tasks/mobile.tasks/AndroidAppBuilder/ApkBuilder.cs b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/ApkBuilder.cs index d71f3ff889b79..6d69fe5486438 100644 --- a/tools-local/tasks/mobile.tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/tools-local/tasks/mobile.tasks/AndroidAppBuilder/ApkBuilder.cs @@ -16,7 +16,7 @@ public class ApkBuilder public string? MinApiLevel { get; set; } public string? BuildApiLevel { get; set; } public string? BuildToolsVersion { get; set; } - public string? OutputDir { get; set; } + public string OutputDir { get; set; } = ""!; public bool StripDebugSymbols { get; set; } public string? NativeMainSource { get; set; } public string? KeyStorePath { get; set; } @@ -50,9 +50,6 @@ public class ApkBuilder 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."); @@ -134,7 +131,6 @@ public class ApkBuilder 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"; string zip = "zip"; @@ -244,30 +240,66 @@ public class ApkBuilder // we don't need the unaligned one any more File.Delete(apkFile); - // 5. Generate key + // 5. Generate key (if needed) & sign the apk + SignApk(alignedApk, apksigner); + + Utils.LogInfo($"\nAPK size: {(new FileInfo(alignedApk).Length / 1000_000.0):0.#} Mb.\n"); + + return (alignedApk, packageId); + } + + 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"); - string signingKey = Path.Combine(OutputDir, "debug.keystore"); - if (!string.IsNullOrEmpty(KeyStorePath)) - signingKey = Path.Combine(KeyStorePath, "debug.keystore"); if (!File.Exists(signingKey)) { - Utils.RunProcess(keytool, "-genkey -v -keystore debug.keystore -storepass android -alias " + + 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); } - else + else if (Path.GetFullPath(signingKey) != Path.GetFullPath(defaultKey)) { File.Copy(signingKey, Path.Combine(OutputDir, "debug.keystore")); } + Utils.RunProcess(apksigner, $"sign --min-sdk-version {MinApiLevel} --ks debug.keystore " + + $"--ks-pass pass:android --key-pass pass:android {apkPath}", workingDir: OutputDir); + } - // 6. Sign APK + public void ReplaceFileInApk(string file) + { + if (string.IsNullOrEmpty(AndroidSdk)) + AndroidSdk = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT"); - Utils.RunProcess(apksigner, $"sign --min-sdk-version {MinApiLevel} --ks debug.keystore " + - $"--ks-pass pass:android --key-pass pass:android {alignedApk}", workingDir: OutputDir); + 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)."); - Utils.LogInfo($"\nAPK size: {(new FileInfo(alignedApk).Length / 1000_000.0):0.#} Mb.\n"); + if (string.IsNullOrEmpty(BuildToolsVersion)) + BuildToolsVersion = GetLatestBuildTools(AndroidSdk); - return (alignedApk, packageId); + 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(aapt, $"remove -v bin/{Path.GetFileName(apkPath)} {file}", workingDir: OutputDir); + Utils.RunProcess(aapt, $"add -v bin/{Path.GetFileName(apkPath)} {file}", workingDir: OutputDir); + + // we need to re-sign the apk + SignApk(apkPath, apksigner); } ///