From a7788172c98d988e5330016c1294a5ed50d04d6e Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Mon, 12 Jul 2021 15:47:09 +0200 Subject: [PATCH] [build] Reimplement NdkUtils Context: https://github.com/xamarin/xamarin-android/issues/5996 Context: https://github.com/xamarin/xamarin-android/issues/5964 Context: https://github.com/xamarin/xamarin-android/issues/5964#issuecomment-854273002 The `NdkUtils` class used by Xamarin.Andrid.Build.Tasks to find tooling shipped with the Android NDK, has grown increasingly complicated over the years due to a number of incompatibilities between various versions of the NDK. The code became hard to follow and untidy. This commit attempts to address the issue by replacing the single static `NdkUtils` class with a hierarchy of dynamically instantiated classes rooted in a new base class, `NdkTools`. `NdkUtils` had to be initialized for each thread that needed to access its methods, which led to various issues with concurrency and lack of proper initialization since the initialization had to be done wherever `NdkUtils` was first accessed, meaning that any task using it had to do it. `NdkTools` doesn't require such initialization, instead it provides a factory method called `Create` which takes path to the NDK as its parameter and returns an instance of `NdkTools` child class (or `null` if an error occurs) which the can be safely used by the caller. Callers need not concern themselves with what is the actual type of the returned instance, they access only methods and properties defined in the `NdkTools` base abstract class. The hierarchy of `NdkTools` derivatives is structured and named after the breaking changes in the NDK. For instance, NDK versions before 16 used the GNU compilers, while release 16 and above use the clang compilers - this is reflected in existence of two classes derived from `NdkTools`, `NoClang` for NDKs older than r16 and `WithClang` for the newer ones. The other breaking changes are the addition of unified headers in r19, removal of the `platforms` directory in r22 and removal of GNU Binutils in r23. NDK r23 is recognized in this commit but it is NOT supported. Support for r23 is being worked on in PR #6073 which will be merged once r23 is out of beta. --- src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs | 54 +- .../Tasks/BuildApk.cs | 8 +- .../Tasks/LinkApplicationSharedLibraries.cs | 2 +- .../Tasks/MakeBundleNativeCodeExternal.cs | 34 +- .../Tasks/NdkUtils.cs | 474 ------------------ .../Tasks/NdkUtilsOld.cs | 314 ------------ .../Xamarin.Android.Build.Tests/AotTests.cs | 6 +- .../Tasks/NdkUtilTests.cs | 39 +- .../Utilities/EnvironmentHelper.cs | 4 +- .../Utilities/NdkTools/NdkToolKind.cs | 11 + .../Utilities/NdkTools/NdkToolchainDir.cs | 9 + .../Utilities/NdkTools/NdkTools.cs | 401 +++++++++++++++ .../Utilities/NdkTools/NdkVersion.cs | 39 ++ .../Utilities/NdkTools/NoClang.cs | 99 ++++ .../NdkTools/NoClangNoUnifiedHeaders.cs | 21 + .../NdkTools/NoClangWithUnifiedHeaders.cs | 24 + .../Utilities/NdkTools/WithClang.cs | 128 +++++ .../Utilities/NdkTools/WithClangNoBinutils.cs | 42 ++ .../NdkTools/WithClangNoPlatforms.cs | 87 ++++ .../NdkTools/WithClangWithPlatforms.cs | 70 +++ 20 files changed, 1003 insertions(+), 863 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/NdkUtils.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/NdkUtilsOld.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolKind.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolchainDir.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkTools.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkVersion.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClang.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangNoUnifiedHeaders.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangWithUnifiedHeaders.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClang.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoBinutils.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoPlatforms.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangWithPlatforms.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs index acc5fef88ca..246fc8bd50a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aot.cs @@ -84,15 +84,6 @@ public class Aot : AndroidAsyncTask AotMode AotMode; SequencePointsMode sequencePointsMode; - public override bool RunTask () - { - // NdkUtil must always be initialized - once per thread - if (!NdkUtil.Init (LogCodedError, AndroidNdkDirectory)) - return false; - - return base.RunTask (); - } - public static bool GetAndroidAotMode(string androidAotMode, out AotMode aotMode) { aotMode = AotMode.Normal; @@ -138,7 +129,7 @@ public static bool TryGetSequencePointsMode (string value, out SequencePointsMod return false; } - static string GetNdkToolchainLibraryDir(string binDir, string archDir = null) + static string GetNdkToolchainLibraryDir (NdkTools ndk, string binDir, string archDir = null) { var baseDir = Path.GetFullPath(Path.Combine(binDir, "..")); @@ -154,7 +145,7 @@ static string GetNdkToolchainLibraryDir(string binDir, string archDir = null) goto no_toolchain_error; } - if (NdkUtil.UsingClangNDK) + if (ndk.UsesClang) return libPath; gccLibDir = Directory.EnumerateDirectories(libPath).ToList(); @@ -171,9 +162,9 @@ static string GetNdkToolchainLibraryDir(string binDir, string archDir = null) throw new Exception("Could not find a valid NDK compiler toolchain library path"); } - static string GetNdkToolchainLibraryDir (string binDir, AndroidTargetArch arch) + static string GetNdkToolchainLibraryDir (NdkTools ndk, string binDir, AndroidTargetArch arch) { - return GetNdkToolchainLibraryDir (binDir, NdkUtil.GetArchDirName (arch)); + return GetNdkToolchainLibraryDir (ndk, binDir, ndk.GetArchDirName (arch)); } static string QuoteFileName(string fileName) @@ -183,7 +174,7 @@ static string QuoteFileName(string fileName) return builder.ToString(); } - int GetNdkApiLevel(string androidNdkPath, string androidApiLevel, AndroidTargetArch arch) + int GetNdkApiLevel (NdkTools ndk, string androidApiLevel, AndroidTargetArch arch) { var manifest = AndroidAppManifest.Load (ManifestFile.ItemSpec, MonoAndroidHelper.SupportedVersions); @@ -209,7 +200,7 @@ int GetNdkApiLevel(string androidNdkPath, string androidApiLevel, AndroidTargetA else if (level == 23) level = 21; // API levels below level 21 do not provide support for 64-bit architectures. - if (NdkUtil.IsNdk64BitArch(arch) && level < 21) { + if (ndk.IsNdk64BitArch (arch) && level < 21) { level = 21; } @@ -217,7 +208,7 @@ int GetNdkApiLevel(string androidNdkPath, string androidApiLevel, AndroidTargetA // mapping above and we do not want to crash needlessly. for (; level >= 5; level--) { try { - NdkUtil.GetNdkPlatformLibPath (androidNdkPath, arch, level); + ndk.GetDirectoryPath (NdkToolchainDir.PlatformLib, arch, level); break; } catch (InvalidOperationException ex) { // Path not found, continue searching... @@ -230,10 +221,9 @@ int GetNdkApiLevel(string androidNdkPath, string androidApiLevel, AndroidTargetA public async override System.Threading.Tasks.Task RunTaskAsync () { - // NdkUtil must always be initialized - once per thread - if (!NdkUtil.Init (LogCodedError, AndroidNdkDirectory)) { - LogDebugMessage ("Failed to initialize NdkUtil"); - return; + NdkTools? ndk = NdkTools.Create (AndroidNdkDirectory, Log); + if (ndk == null) { + return; // NdkTools.Create will log appropriate error } bool hasValidAotMode = GetAndroidAotMode (AndroidAotMode, out AotMode); @@ -251,7 +241,7 @@ public async override System.Threading.Tasks.Task RunTaskAsync () var nativeLibs = new List (); - await this.WhenAllWithLock (GetAotConfigs (), + await this.WhenAllWithLock (GetAotConfigs (ndk), (config, lockObject) => { if (!config.Valid) { Cancel (); @@ -277,7 +267,7 @@ await this.WhenAllWithLock (GetAotConfigs (), LogDebugTaskItems (" NativeLibrariesReferences: ", NativeLibrariesReferences); } - IEnumerable GetAotConfigs () + IEnumerable GetAotConfigs (NdkTools ndk) { if (!Directory.Exists (AotOutputDirectory)) Directory.CreateDirectory (AotOutputDirectory); @@ -325,7 +315,7 @@ IEnumerable GetAotConfigs () throw new Exception ("Unsupported Android target architecture ABI: " + abi); } - if (EnableLLVM && !NdkUtil.ValidateNdkPlatform (LogMessage, LogCodedError, AndroidNdkDirectory, arch, enableLLVM:EnableLLVM)) { + if (EnableLLVM && !ndk.ValidateNdkPlatform (LogMessage, LogCodedError, arch, enableLLVM:EnableLLVM)) { yield return Config.Invalid; yield break; } @@ -341,8 +331,8 @@ IEnumerable GetAotConfigs () int level = 0; string toolPrefix = EnableLLVM - ? NdkUtil.GetNdkToolPrefix (AndroidNdkDirectory, arch, level = GetNdkApiLevel (AndroidNdkDirectory, AndroidApiLevel, arch)) - : Path.Combine (AndroidBinUtilsDirectory, $"{NdkUtil.GetArchDirName (arch)}-"); + ? ndk.GetNdkToolPrefixForAOT (arch, level = GetNdkApiLevel (ndk, AndroidApiLevel, arch)) + : Path.Combine (AndroidBinUtilsDirectory, $"{ndk.GetArchDirName (arch)}-"); var toolchainPath = toolPrefix.Substring(0, toolPrefix.LastIndexOf(Path.DirectorySeparatorChar)); var ldFlags = string.Empty; if (EnableLLVM) { @@ -353,25 +343,25 @@ IEnumerable GetAotConfigs () string androidLibPath = string.Empty; try { - androidLibPath = NdkUtil.GetNdkPlatformLibPath(AndroidNdkDirectory, arch, level); + androidLibPath = ndk.GetDirectoryPath (NdkToolchainDir.PlatformLib, arch, level); } catch (InvalidOperationException ex) { Diagnostic.Error (5101, ex.Message); } string toolchainLibDir; - if (NdkUtil.UsingClangNDK) - toolchainLibDir = GetNdkToolchainLibraryDir (toolchainPath, arch); + if (ndk.UsesClang) + toolchainLibDir = GetNdkToolchainLibraryDir (ndk, toolchainPath, arch); else - toolchainLibDir = GetNdkToolchainLibraryDir (toolchainPath); + toolchainLibDir = GetNdkToolchainLibraryDir (ndk, toolchainPath); var libs = new List(); - if (NdkUtil.UsingClangNDK) { + if (ndk.UsesClang) { libs.Add ($"-L{toolchainLibDir.TrimEnd ('\\')}"); libs.Add ($"-L{androidLibPath.TrimEnd ('\\')}"); if (arch == AndroidTargetArch.Arm) { // Needed for -lunwind to work - string compilerLibDir = Path.Combine (toolchainPath, "..", "sysroot", "usr", "lib", NdkUtil.GetArchDirName (arch)); + string compilerLibDir = Path.Combine (toolchainPath, "..", "sysroot", "usr", "lib", ndk.GetArchDirName (arch)); libs.Add ($"-L{compilerLibDir.TrimEnd ('\\')}"); } } @@ -385,7 +375,7 @@ IEnumerable GetAotConfigs () string ldName = String.Empty; if (EnableLLVM) { - ldName = NdkUtil.GetNdkTool (AndroidNdkDirectory, arch, "ld", level); + ldName = ndk.GetToolPath (NdkToolKind.Linker, arch, level); if (!String.IsNullOrEmpty (ldName)) { ldName = Path.GetFileName (ldName); if (ldName.IndexOf ('-') >= 0) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs index ff354a468d9..36763a00717 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs @@ -635,8 +635,12 @@ private void AddNativeLibraries (ArchiveFileList files, string [] supportedAbis) return; } - NdkUtil.Init (AndroidNdkDirectory); - string clangDir = NdkUtil.GetClangDeviceLibraryPath (AndroidNdkDirectory); + NdkTools? ndk = NdkTools.Create (AndroidNdkDirectory, Log); + if (ndk == null) { + return; // NdkTools.Create will log appropriate error + } + + string clangDir = ndk.GetClangDeviceLibraryPath (); if (String.IsNullOrEmpty (clangDir)) { LogSanitizerError ($"Unable to find the clang compiler directory. Is NDK installed?"); return; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs index 8b8ff883a20..f7f592e6ce4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkApplicationSharedLibraries.cs @@ -168,7 +168,7 @@ IEnumerable GetLinkerConfigs () linkerArgs.Add (QuoteFileName (file)); } - string ld = MonoAndroidHelper.GetExecutablePath (AndroidBinUtilsDirectory, $"{NdkUtil.GetNdkToolchainPrefix (arch, false)}ld"); + string ld = MonoAndroidHelper.GetExecutablePath (AndroidBinUtilsDirectory, $"{NdkTools.GetBinutilsToolchainPrefix (arch)}ld"); yield return new Config { LinkerPath = Path.Combine (AndroidBinUtilsDirectory, ld), LinkerOptions = String.Join (" ", linkerArgs), diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/MakeBundleNativeCodeExternal.cs b/src/Xamarin.Android.Build.Tasks/Tasks/MakeBundleNativeCodeExternal.cs index 36cb78917ae..4d95d17ebb3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/MakeBundleNativeCodeExternal.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/MakeBundleNativeCodeExternal.cs @@ -28,11 +28,11 @@ public class MakeBundleNativeCodeExternal : AndroidTask [Required] public ITaskItem[] Assemblies { get; set; } - + // Which ABIs to include native libs for [Required] public string [] SupportedAbis { get; set; } - + [Required] public string TempOutputPath { get; set; } @@ -57,11 +57,13 @@ public MakeBundleNativeCodeExternal () public override bool RunTask () { - if (!NdkUtil.Init (Log, AndroidNdkDirectory)) - return false; + NdkTools? ndk = NdkTools.Create (AndroidNdkDirectory, Log); + if (ndk == null) { + return false; // NdkTools.Create will log appropriate error + } try { - return DoExecute (); + return DoExecute (ndk); } catch (XamarinAndroidException e) { Log.LogCodedError (string.Format ("XA{0:0000}", e.Code), e.MessageWithoutCode); if (MonoAndroidHelper.LogInternalExceptions) @@ -72,7 +74,7 @@ public override bool RunTask () return !Log.HasLoggedErrors; } - bool DoExecute () + bool DoExecute (NdkTools ndk) { var results = new List (); string bundlepath = Path.Combine (TempOutputPath, "bundles"); @@ -102,11 +104,11 @@ bool DoExecute () break; } - if (!NdkUtil.ValidateNdkPlatform (Log, AndroidNdkDirectory, arch, enableLLVM: false)) { + if (!ndk.ValidateNdkPlatform (arch, enableLLVM: false)) { return false; } - int level = NdkUtil.GetMinimumApiLevelFor (arch, AndroidNdkDirectory); + int level = ndk.GetMinimumApiLevelFor (arch); var outpath = Path.Combine (bundlepath, abi); if (!Directory.Exists (outpath)) Directory.CreateDirectory (outpath); @@ -140,10 +142,10 @@ bool DoExecute () CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, }; - string windowsCompilerSwitches = NdkUtil.GetCompilerTargetParameters (AndroidNdkDirectory, arch, level); - var compilerNoQuotes = NdkUtil.GetNdkTool (AndroidNdkDirectory, arch, "gcc", level); + string windowsCompilerSwitches = ndk.GetCompilerTargetParameters (arch, level); + var compilerNoQuotes = ndk.GetToolPath (NdkToolKind.CompilerC, arch, level); var compiler = $"\"{compilerNoQuotes}\" {windowsCompilerSwitches}".Trim (); - var gas = '"' + NdkUtil.GetNdkTool (AndroidNdkDirectory, arch, "as", level) + '"'; + var gas = '"' + ndk.GetToolPath (NdkToolKind.Assembler, arch, level) + '"'; psi.EnvironmentVariables ["CC"] = compiler; psi.EnvironmentVariables ["AS"] = gas; Log.LogDebugMessage ("CC=" + compiler); @@ -167,7 +169,7 @@ bool DoExecute () clb = new CommandLineBuilder (); - // See NdkUtils.GetNdkTool for reasons why + // See NdkToolsWithClangWithPlatforms.ctor for reasons why if (!String.IsNullOrEmpty (windowsCompilerSwitches)) clb.AppendTextUnquoted (windowsCompilerSwitches); @@ -188,14 +190,14 @@ bool DoExecute () clb.AppendFileNameIfNotNull (IncludePath); } - string asmIncludePath = NdkUtil.GetNdkAsmIncludePath (AndroidNdkDirectory, arch, level); + string asmIncludePath = ndk.GetDirectoryPath (NdkToolchainDir.AsmInclude, arch, level); if (!String.IsNullOrEmpty (asmIncludePath)) { clb.AppendSwitch ("-I"); clb.AppendFileNameIfNotNull (asmIncludePath); } clb.AppendSwitch ("-I"); - clb.AppendFileNameIfNotNull (NdkUtil.GetNdkPlatformIncludePath (AndroidNdkDirectory, arch, level)); + clb.AppendFileNameIfNotNull (ndk.GetDirectoryPath (NdkToolchainDir.PlatformInclude, arch, level)); clb.AppendFileNameIfNotNull (Path.Combine (outpath, "temp.c")); Log.LogDebugMessage ("[CC] " + compiler + " " + clb); if (MonoAndroidHelper.RunProcess (compilerNoQuotes, clb.ToString (), OnCcOutputData, OnCcErrorData) != 0) { @@ -216,13 +218,13 @@ bool DoExecute () clb.AppendSwitch ("-o"); clb.AppendFileNameIfNotNull (Path.Combine (outpath, BundleSharedLibraryName)); clb.AppendSwitch ("-L"); - clb.AppendFileNameIfNotNull (NdkUtil.GetNdkPlatformLibPath (AndroidNdkDirectory, arch, level)); + clb.AppendFileNameIfNotNull (ndk.GetDirectoryPath (NdkToolchainDir.PlatformLib, arch, level)); clb.AppendSwitch ("-lc"); clb.AppendSwitch ("-lm"); clb.AppendSwitch ("-ldl"); clb.AppendSwitch ("-llog"); clb.AppendSwitch ("-lz"); // Compress - string ld = NdkUtil.GetNdkTool (AndroidNdkDirectory, arch, "ld", level); + string ld = ndk.GetToolPath (NdkToolKind.Linker, arch, level); Log.LogMessage (MessageImportance.Normal, "[LD] " + ld + " " + clb); if (MonoAndroidHelper.RunProcess (ld, clb.ToString (), OnLdOutputData, OnLdErrorData) != 0) { Log.LogCodedError ("XA5201", Properties.Resources.XA5201, proc.ExitCode); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtils.cs b/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtils.cs deleted file mode 100644 index 851e4fb83ad..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtils.cs +++ /dev/null @@ -1,474 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -using Java.Interop.Tools.Diagnostics; -using Xamarin.Android.Tools; -using Microsoft.Android.Build.Tasks; - -namespace Xamarin.Android.Tasks -{ - public static class NdkUtil - { - // We need it to work fine during our tests which are executed at the same time in various threads. - [ThreadStatic] - static bool usingClangNDK; - - public static bool UsingClangNDK => usingClangNDK; - - public static bool Init (string ndkPath) - { - return Init (delegate { }, ndkPath); // For tests which don't have access to a TaskLoggingHelper - } - - public static bool Init (TaskLoggingHelper log, string ndkPath) => - Init ((c, m) => log.LogCodedError (c, m), ndkPath); - - public static bool Init (Action logError, string ndkPath) - { - Version ndkVersion; - bool hasNdkVersion = GetNdkToolchainRelease (ndkPath ?? "", out ndkVersion); - if (!hasNdkVersion) { - logError ("XA5104", Properties.Resources.XA5104); - return false; - } - - usingClangNDK = ndkVersion.Major >= 19; - - return true; - } - - public static bool ValidateNdkPlatform (TaskLoggingHelper log, string ndkPath, AndroidTargetArch arch, bool enableLLVM) - { - return ValidateNdkPlatform ((m) => log.LogMessage (m), (c, m) => log.LogCodedError (c, m), ndkPath, arch, enableLLVM); - } - - public static bool ValidateNdkPlatform (Action logMessage, Action logError, string ndkPath, AndroidTargetArch arch, bool enableLLVM) - { - if (!UsingClangNDK) - return NdkUtilOld.ValidateNdkPlatform (logMessage, logError, ndkPath, arch, enableLLVM); - - // Check that we have a compatible NDK version for the targeted ABIs. - Version ndkVersion; - bool hasNdkVersion = GetNdkToolchainRelease (ndkPath, out ndkVersion); - - if (hasNdkVersion && ndkVersion.Major < 19) { - logMessage ( - "The detected Android NDK version is incompatible with this version of Xamarin.Android, " + - "please upgrade to NDK r19 or newer."); - } - - return true; - } - - public static string GetNdkToolPrefix (string androidNdkPath, AndroidTargetArch arch, int apiLevel) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkToolPrefix (androidNdkPath, arch); - - var path = GetNdkTool (androidNdkPath, arch, "as", apiLevel); - if (path != null) - path = path.Substring (0, path.LastIndexOf ("-") + 1); - return path; - } - - // See the "NDK r19 bug" comment in the "GetNdkTool" method below for explanation of the issue - // this method fixes. - public static string GetCompilerTargetParameters (string androidNdkPath, AndroidTargetArch arch, int apiLevel, bool forCPlusPlus = false) - { - if (!UsingClangNDK || !OS.IsWindows) - return String.Empty; - - string targetPrefix; - string otherParams = String.Empty; - switch (arch) { - case AndroidTargetArch.Arm: - targetPrefix = "--target=armv7a-linux-androideabi"; - break; - - case AndroidTargetArch.Arm64: - targetPrefix = "--target=aarch64-linux-android"; - break; - - case AndroidTargetArch.X86: - targetPrefix = "--target=i686-linux-android"; - otherParams = "-mstackrealign"; - break; - - case AndroidTargetArch.X86_64: - targetPrefix = "--target=x86_64-linux-android"; - break; - - default: - throw new InvalidOperationException ($"Unsupported target architecture {arch}"); - } - - string stdlib = String.Empty; - if (forCPlusPlus) - stdlib = "-stdlib=libc++"; - - return $"{targetPrefix}{apiLevel} {otherParams} -fno-addrsig {stdlib}"; - } - - static string GetToolchainDir (string androidNdkPath) - { - return Path.Combine (androidNdkPath, "toolchains", "llvm", "prebuilt", MonoAndroidHelper.AndroidSdk.AndroidNdkHostPlatform); - } - - public static string GetClangDeviceLibraryPath (string androidNdkPath) - { - if (!UsingClangNDK) - throw new InvalidOperationException ("NDK version with the clang compiler must be used"); - - string toolchainDir = GetToolchainDir (androidNdkPath); - string clangBaseDir = Path.Combine (toolchainDir, "lib64", "clang"); - - if (!Directory.Exists (clangBaseDir)) { - return null; - } - - // There should be just one subdir - clang version - but it's better to be safe than sorry... - foreach (string dir in Directory.EnumerateDirectories (clangBaseDir)) { - if (dir[0] == '.') { - continue; - } - - string libDir = Path.Combine (dir, "lib", "linux"); - if (Directory.Exists (libDir)) { - return libDir; - } - } - - return null; - } - - public static string GetNdkTool (string androidNdkPath, AndroidTargetArch arch, string tool, int apiLevel) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkTool (androidNdkPath, arch, tool); - - string toolchainDir = GetToolchainDir (androidNdkPath); - string toolName; - bool forCompiler = false; - - if (String.Compare (tool, "gcc", StringComparison.Ordinal) == 0 || - String.Compare (tool, "clang", StringComparison.Ordinal) == 0) { - forCompiler = true; - toolName = "clang"; - } else if (String.Compare (tool, "g++", StringComparison.Ordinal) == 0 || - String.Compare (tool, "clang++", StringComparison.Ordinal) == 0) { - forCompiler = true; - toolName = "clang++"; - } else - toolName = tool; - - // - // NDK r19 bug. - // - // The llvm toolchain directory contains a selection of shell scripts (both Unix and Windows) - // which call `clang/clang++` with different `-target` parameters depending on both the target - // architecture and API level. For instance, the clang/clang++ compilers targetting aarch64 on API level - // 28 will have the following Unix shell scripts present in the toolchain `bin` directory: - // - // aarch64-linux-android28-clang - // aarch64-linux-android28-clang++ - // - // However, the Windows version of the NDK has a bug where there is only one Windows - // counterpart to the above Unix scripts: - // - // aarch64-linux-android28-clang.cmd - // - // This script, despite its name suggesting that it calls `clang.exe` in fact calls - // `clang++.exe` which breaks compilation of some C programs (including the code generated by - // Mono's mkbundle utility) because `clang++` treats the input as C++. There is no corresponding - // `aarch64-linux-android28-clang++.cmd` and so invocation of `clang.exe` becomes harder and, - // most certainly, non-standard as far as cross-platform NDK compatibility is concerned. - // - // The code below tries to rectify the situation by special-casing the compiler tool handling to - // return path to the actual .exe instead of the CMD. Unfortunately, the caller of this code - // will need to provide the correct parameters for the compilers. - // - string toolchainPrefix; - if (forCompiler) { - if (!OS.IsWindows) - toolchainPrefix = $"{GetNdkToolchainPrefix (arch, true)}{apiLevel}"; - else - toolchainPrefix = String.Empty; - } else - toolchainPrefix = GetNdkToolchainPrefix (arch, false); - - string extension = OS.IsWindows ? ".exe" : String.Empty; - if (forCompiler && OS.IsWindows) - toolName = $"{toolName}{extension}"; - else - toolName = GetPrefixedName (toolName); - - string toolPath = GetToolPath (toolName); - if (String.IsNullOrEmpty (toolPath) && String.Compare ("ld", tool, StringComparison.OrdinalIgnoreCase) == 0) { - // NDK r22 removed arch-prefixed `ld` binary. There exists the unprefixed `ld` binary, from the LLVM - // toolchain, and two binutils linkers - `ld.bfd` and `ld.gold`. Since we will need to keep using - // binutils once NDK removes them, let's use one of the latter. `ld.gold` is the better choice, so we'll - // use it if found - toolPath = GetToolPath (GetPrefixedName ("ld.gold")); - } - - if (!String.IsNullOrEmpty (toolPath)) { - return toolPath; - } - - Diagnostic.Error (5105, Properties.Resources.XA5105, toolName, arch, toolchainDir); - return null; - - string GetPrefixedName (string name) - { - return $"{toolchainPrefix}-{name}{extension}"; - } - - string GetToolPath (string name) - { - string binDir = Path.Combine (toolchainDir, "bin"); - string toolExe = MonoAndroidHelper.GetExecutablePath (binDir, name); - string toolPath = Path.Combine (binDir, toolExe); - if (File.Exists (toolPath)) - return toolPath; - return null; - } - } - - static string GetUnifiedHeadersPath (string androidNdkPath) - { - string preNdk22SysrootIncludeDir = Path.Combine (androidNdkPath, "sysroot", "usr", "include"); - if (Directory.Exists (preNdk22SysrootIncludeDir)) { - return preNdk22SysrootIncludeDir; - } - - return Path.Combine (GetToolchainDir (androidNdkPath), "sysroot", "usr", "include"); - } - - public static string GetArchDirName (AndroidTargetArch arch) - { - switch (arch) { - case AndroidTargetArch.Arm: - return "arm-linux-androideabi"; - - case AndroidTargetArch.Arm64: - return "aarch64-linux-android"; - - case AndroidTargetArch.X86: - return "i686-linux-android"; - - case AndroidTargetArch.X86_64: - return "x86_64-linux-android"; - - default: - throw new InvalidOperationException ($"Unsupported architecture {arch}"); - } - } - - public static string GetNdkAsmIncludePath (string androidNdkPath, AndroidTargetArch arch, int apiLevel) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkAsmIncludePath (androidNdkPath, arch, apiLevel); - - string path = GetUnifiedHeadersPath (androidNdkPath); - string archDir = GetArchDirName (arch); - - return Path.Combine (path, archDir); - } - - public static string GetNdkPlatformIncludePath (string androidNdkPath, AndroidTargetArch arch, int apiLevel) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkPlatformIncludePath (androidNdkPath, arch, apiLevel); - - string path = GetUnifiedHeadersPath (androidNdkPath); - if (Directory.Exists (path)) - return path; - - throw new InvalidOperationException ($"Android include path not found. Tried: {path}"); - } - - public static string GetNdkPlatformLibPath (string androidNdkPath, AndroidTargetArch arch, int apiLevel) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkPlatformLibPath (androidNdkPath, arch, apiLevel); - - var checkedPaths = new List (); - string lib = arch == AndroidTargetArch.X86_64 ? "lib64" : "lib"; - string path = Path.Combine (androidNdkPath, "platforms", $"android-{apiLevel}", $"arch-{GetPlatformArch (arch)}", "usr", lib); - if (!Directory.Exists (path)) { - checkedPaths.Add (path); - path = Path.Combine (GetNdk22OrNewerSysrootDir (androidNdkPath), GetArchDirName (arch), apiLevel.ToString ()); - } - - if (!Directory.Exists (path)) { - checkedPaths.Add (path); - string paths = String.Join ("; ", checkedPaths); - throw new InvalidOperationException ($"Platform library directory for target {arch} and API Level {apiLevel} was not found. Checked paths: {paths}"); - } - return path; - } - - static string GetPlatformArch (AndroidTargetArch arch) - { - if (!UsingClangNDK) - return NdkUtilOld.GetPlatformArch (arch); - - switch (arch) { - case AndroidTargetArch.Arm: - return "arm"; - case AndroidTargetArch.Arm64: - return "arm64"; - case AndroidTargetArch.X86: - return "x86"; - case AndroidTargetArch.X86_64: - return "x86_64"; - } - return null; - } - - public static string GetNdkToolchainPrefix (AndroidTargetArch arch, bool forCompiler) - { - if (!UsingClangNDK) - return NdkUtilOld.GetNdkToolchainPrefix (arch); - - switch (arch) { - case AndroidTargetArch.Arm: - return forCompiler ? "armv7a-linux-androideabi" : "arm-linux-androideabi"; - case AndroidTargetArch.Arm64: - return "aarch64-linux-android"; - case AndroidTargetArch.X86: - return "i686-linux-android"; - case AndroidTargetArch.X86_64: - return "x86_64-linux-android"; - default: - // return empty. Since this method returns the "prefix", the resulting - // tool path just becomes the tool name i.e. "gcc" becomes "gcc". - // This should work for any custom arbitrary platform. - return String.Empty; - } - } - - public static bool GetNdkToolchainRelease (string androidNdkPath, out NdkUtilOld.NdkVersion ndkVersion) - { - return NdkUtilOld.GetNdkToolchainRelease (androidNdkPath, out ndkVersion); - } - - public static bool GetNdkToolchainRelease (string androidNdkPath, out Version ndkVersion) - { - ndkVersion = new Version (); - string sourcePropertiesPath = Path.Combine (androidNdkPath, "source.properties"); - if (!File.Exists (sourcePropertiesPath)) { - return false; - } - - foreach (string l in File.ReadAllLines (sourcePropertiesPath)) { - string line = l.Trim (); - if (!line.StartsWith ("Pkg.Revision", StringComparison.Ordinal)) - continue; - string[] parts = line.Split (new char[] {'='}, 2); - if (parts.Length != 2) - return false; - - if (Version.TryParse (parts [1].Trim (), out ndkVersion)) - return true; - break; - } - - return false; - } - - public static bool IsNdk64BitArch (AndroidTargetArch arch) - { - return arch == AndroidTargetArch.Arm64 || arch == AndroidTargetArch.X86_64; - } - - static string GetNdk22OrNewerSysrootDir (string androidNdkPath) - { - return Path.Combine (GetToolchainDir (androidNdkPath), "sysroot", "usr", "lib"); - } - - public static IEnumerable GetSupportedPlatforms (TaskLoggingHelper log, string androidNdkPath) - { - string preNdk22PlatformsDir = Path.Combine (androidNdkPath, "platforms"); - - if (Directory.Exists (preNdk22PlatformsDir)) { - return GetSupportedPlatformsPreNdk22 (preNdk22PlatformsDir); - } - - // NDK r22 no longer has a single platforms dir. The API level directories are now found in per-arch - // subdirectories under the toolchain directory. We need to examine all of them and compose a list of unique - // API levels (since they are repeated in each per-arch subdirectory, but not all architectures have the - // same set of API levels) - var apiLevels = new HashSet (); - string sysrootLibDir = GetNdk22OrNewerSysrootDir (androidNdkPath); - foreach (AndroidTargetArch targetArch in Enum.GetValues (typeof (AndroidTargetArch))) { - if (targetArch == AndroidTargetArch.None || - targetArch == AndroidTargetArch.Other || - targetArch == AndroidTargetArch.Mips) { - continue; - } - - string archDirName = GetArchDirName (targetArch); - if (String.IsNullOrEmpty (archDirName)) { - log.LogWarning ($"NDK architecture {targetArch} unknown?"); - continue; - } - - string archDir = Path.Combine (sysrootLibDir, archDirName); - if (!Directory.Exists (archDir)) { - log.LogWarning ($"Architecture {targetArch} toolchain directory '{archDir}' not found"); - continue; - } - - foreach (string platform in Directory.EnumerateDirectories (archDir, "*", SearchOption.TopDirectoryOnly)) { - string plibc = Path.Combine (platform, "libc.so"); - if (!File.Exists (plibc)) { - continue; - } - - string pdir = Path.GetFileName (platform); - int api; - if (!Int32.TryParse (pdir, out api) || apiLevels.Contains (api)) { - continue; - } - apiLevels.Add (api); - } - } - - return apiLevels; - } - - static IEnumerable GetSupportedPlatformsPreNdk22 (string platformsDir) - { - foreach (var platform in Directory.EnumerateDirectories (platformsDir)) { - var androidApi = Path.GetFileName (platform); - int api = -1; - if (int.TryParse (androidApi.Replace ("android-", String.Empty), out api)) { - yield return api; - } - } - } - - public static int GetMinimumApiLevelFor (AndroidTargetArch arch, string androidNdkPath) - { - if (!UsingClangNDK) - return NdkUtilOld.GetMinimumApiLevelFor (arch, androidNdkPath); - - int minValue = 0; - string archName = GetPlatformArch (arch); - if (!XABuildConfig.ArchAPILevels.TryGetValue (archName, out minValue)) - throw new InvalidOperationException ($"Unable to determine minimum API level for architecture {arch}"); - - return minValue; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtilsOld.cs b/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtilsOld.cs deleted file mode 100644 index 8469371e9f1..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/NdkUtilsOld.cs +++ /dev/null @@ -1,314 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -using Java.Interop.Tools.Diagnostics; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks -{ - public static class NdkUtilOld { - - public static bool ValidateNdkPlatform (Action logMessage, Action logError, string ndkPath, AndroidTargetArch arch, bool enableLLVM) - { - // Check that we have a compatible NDK version for the targeted ABIs. - NdkVersion ndkVersion; - bool hasNdkVersion = GetNdkToolchainRelease (ndkPath, out ndkVersion); - - if (IsNdk64BitArch(arch) && hasNdkVersion && ndkVersion.Version < 10) { - logMessage ( - "The detected Android NDK version is incompatible with the targeted 64-bit architecture, " + - "please upgrade to NDK r10 or newer."); - } - - // NDK r10d is buggy and cannot link x86_64 ABI shared libraries because they are 32-bits. - // See https://code.google.com/p/android/issues/detail?id=161421 - if (enableLLVM && ndkVersion.Version == 10 && ndkVersion.Revision == "d" && arch == AndroidTargetArch.X86_64) { - logError ("XA3004", Properties.Resources.XA3004); - return false; - } - - if (enableLLVM && (ndkVersion.Version < 10 || (ndkVersion.Version == 10 && ndkVersion.Revision[0] < 'd'))) { - logError ("XA3005", Properties.Resources.XA3005); - } - - return true; - } - - public static string GetNdkToolPrefix (string androidNdkPath, AndroidTargetArch arch) - { - var path = GetNdkTool (androidNdkPath, arch, "as"); - if (path != null) - path = path.Substring (0, path.LastIndexOf ("-") + 1); - return path; - } - - public static List GetNdkToolchainPath(string androidNdkPath, AndroidTargetArch arch) - { - var toolchains = GetNdkToolchainDirectories (Path.Combine (androidNdkPath, "toolchains"), arch); - if (!toolchains.Any ()) - Diagnostic.Error (5101, Properties.Resources.XA5101_Toolchain, arch); - // Sort the toolchains paths in reverse so that we prefer the latest versions. - Array.Sort(toolchains); - Array.Reverse(toolchains); - - return new List(toolchains); - } - - public static string GetNdkTool (string androidNdkPath, AndroidTargetArch arch, string tool) - { - var toolchains = GetNdkToolchainPath(androidNdkPath, arch); - string extension = OS.IsWindows ? ".exe" : string.Empty; - List toolPaths = null; - foreach (var platbase in toolchains) { - string path = Path.Combine (platbase, "prebuilt", MonoAndroidHelper.AndroidSdk.AndroidNdkHostPlatform, "bin", GetNdkToolchainPrefix (arch) + tool + extension); - if (File.Exists (path)) - return path; - if (toolPaths == null) - toolPaths = new List(); - toolPaths.Add (path); - } - { - string path = Path.Combine (androidNdkPath, "prebuilt", MonoAndroidHelper.AndroidSdk.AndroidNdkHostPlatform, "bin", tool); - if (File.Exists (path)) - return path; - if (toolPaths == null) - toolPaths = new List(); - toolPaths.Add (path); - } - - Diagnostic.Error (5101, Properties.Resources.XA5101_C_Compiler, arch, string.Join ("; ", toolPaths)); - return null; - } - - static string GetUnifiedHeadersPath (string androidNdkPath) - { - return Path.Combine (androidNdkPath, "sysroot", "usr", "include"); - } - - static string GetPerPlatformHeadersPath (string androidNdkPath, AndroidTargetArch arch, int level) - { - return Path.Combine (androidNdkPath, "platforms", "android-" + level, "arch-" + GetPlatformArch (arch), "usr", "include"); - } - - public static string GetNdkAsmIncludePath (string androidNdkPath, AndroidTargetArch arch, int level) - { - string path = GetPerPlatformHeadersPath (androidNdkPath, arch, level); - if (Directory.Exists (path)) - return null; - - path = GetUnifiedHeadersPath (androidNdkPath); - if (!Directory.Exists (path)) - return null; - - string archDir = null; - switch (arch) { - case AndroidTargetArch.Arm: - archDir = "arm-linux-androideabi"; - break; - - case AndroidTargetArch.Arm64: - archDir = "aarch64-linux-android"; - break; - - case AndroidTargetArch.Mips: - archDir = "mipsel-linux-android"; - break; - - case AndroidTargetArch.X86: - archDir = "i686-linux-android"; - break; - - case AndroidTargetArch.X86_64: - archDir = "x86_64-linux-android"; - break; - } - - if (archDir == null) - return null; - - return Path.Combine (path, archDir); - } - - public static string GetNdkPlatformIncludePath (string androidNdkPath, AndroidTargetArch arch, int level) - { - // This is for NDK older than r16 which isn't configured to use unified headers. We - string path = GetPerPlatformHeadersPath (androidNdkPath, arch, level); - if (!Directory.Exists (path)) { - // This is for NDK r15 (if configured to use unified headers) or NDK r16+ (which doesn't have - // the per-platform includes anymore) - path = GetUnifiedHeadersPath (androidNdkPath); - if (Directory.Exists (path)) - return path; - - throw new InvalidOperationException (String.Format ("Platform header files for target {0} and API Level {1} was not found. Expected path is \"{2}\"", arch, level, path)); - } - - return path; - } - - public static string GetNdkPlatformLibPath (string androidNdkPath, AndroidTargetArch arch, int level) - { - string lib = arch == AndroidTargetArch.X86_64 ? "lib64" : "lib"; - string path = Path.Combine (androidNdkPath, "platforms", "android-" + level, "arch-" + GetPlatformArch (arch), "usr", lib); - if (!Directory.Exists (path)) - throw new InvalidOperationException (String.Format ("Platform library directory for target {0} and API Level {1} was not found. Expected path is \"{2}\"", arch, level, path)); - return path; - } - - public static string GetPlatformArch (AndroidTargetArch arch) - { - switch (arch) { - case AndroidTargetArch.Arm: - return "arm"; - case AndroidTargetArch.Arm64: - return "arm64"; - case AndroidTargetArch.Mips: - return "mips"; - case AndroidTargetArch.X86: - return "x86"; - case AndroidTargetArch.X86_64: - return "x86_64"; - } - return null; - } - - static string[] GetNdkToolchainDirectories (string toolchainsPath, AndroidTargetArch arch) - { - if (!Directory.Exists (toolchainsPath)) - Diagnostic.Error (5101, Properties.Resources.XA5101, toolchainsPath); - switch (arch) { - case AndroidTargetArch.Arm: - return Directory.GetDirectories (toolchainsPath, "arm-linux-androideabi-*"); - case AndroidTargetArch.Arm64: - return Directory.GetDirectories (toolchainsPath, "aarch64-linux-android-*"); - case AndroidTargetArch.X86: - return Directory.GetDirectories (toolchainsPath, "x86-*"); - case AndroidTargetArch.X86_64: - return Directory.GetDirectories (toolchainsPath, "x86_64-*"); - case AndroidTargetArch.Mips: - return Directory.GetDirectories (toolchainsPath, "mipsel-linux-android-*"); - default: // match any directory that contains the arch name. - return Directory.GetDirectories (toolchainsPath, "*" + arch + "*"); - } - } - - public static string GetNdkToolchainPrefix (AndroidTargetArch arch) - { - switch (arch) { - case AndroidTargetArch.Arm: - return "arm-linux-androideabi-"; - case AndroidTargetArch.Arm64: - return "aarch64-linux-android-"; - case AndroidTargetArch.X86: - return "i686-linux-android-"; - case AndroidTargetArch.X86_64: - return "x86_64-linux-android-"; - case AndroidTargetArch.Mips: - return "mipsel-linux-android-"; - default: - // return empty. Since this method returns the "prefix", the resulting - // tool path just becomes the tool name i.e. "gcc" becomes "gcc". - // This should work for any custom arbitrary platform. - return String.Empty; - } - } - - static bool GetNdkToolchainRelease (string androidNdkPath, out string version) - { - var releaseVersionPath = Path.Combine (androidNdkPath, "RELEASE.txt"); - if (!File.Exists (releaseVersionPath)) - { - version = string.Empty; - return false; - } - - version = File.ReadAllText (releaseVersionPath).Trim(); - return true; - } - - static bool GetNdkToolchainSourceProperties (string androidNdkPath, out NdkVersion version) - { - version = new NdkVersion (); - var sourcePropertiesPath = Path.Combine (androidNdkPath, "source.properties"); - if (!File.Exists (sourcePropertiesPath)) { - return false; - } - var match = Regex.Match (File.ReadAllText (sourcePropertiesPath).Trim (), "^Pkg.Revision\\s*=\\s*([.0-9]+)$", RegexOptions.Multiline); - if (!match.Success) { - return false; - } - var numbers = match.Groups[1].Value.Trim().Split ('.'); - version.Version = int.Parse (numbers [0]); - version.Revision = Convert.ToChar (int.Parse (numbers [1]) + (int)'a').ToString (); - return true; - } - - public struct NdkVersion - { - public int Version; - public string Revision; - } - - public static bool GetNdkToolchainRelease (string androidNdkPath, out NdkVersion ndkVersion) - { - ndkVersion = new NdkVersion (); - - string version; - if (!GetNdkToolchainRelease (androidNdkPath, out version)) { - if (GetNdkToolchainSourceProperties (androidNdkPath, out ndkVersion)) - return true; - return false; - } - - var match = Regex.Match(version, @"r(\d+)\s*(.*)\s+.*"); - if( !match.Success) - return false; - - ndkVersion.Version = int.Parse (match.Groups[1].Value.Trim()); - ndkVersion.Revision = match.Groups[2].Value.Trim().ToLowerInvariant(); - - return true; - } - - public static bool IsNdk64BitArch (AndroidTargetArch arch) - { - return arch == AndroidTargetArch.Arm64 || arch == AndroidTargetArch.X86_64; - } - - public static IEnumerable GetSupportedPlatforms (string androidNdkPath) - { - foreach (var platform in Directory.EnumerateDirectories (Path.Combine (androidNdkPath, "platforms"))) { - var androidApi = Path.GetFileName (platform); - int api = -1; - if (int.TryParse (androidApi.Replace ("android-", String.Empty), out api)) { - yield return api; - } - } - } - - static readonly Dictionary archPathMap = new Dictionary () { - { AndroidTargetArch.Arm, "arm"}, - { AndroidTargetArch.Arm64, "arm64"}, - { AndroidTargetArch.Mips, "mips"}, - { AndroidTargetArch.None, "none"}, - { AndroidTargetArch.Other, "other"}, - { AndroidTargetArch.X86, "x86"}, - { AndroidTargetArch.X86_64, "x86_64"}, - }; - - public static int GetMinimumApiLevelFor (AndroidTargetArch arch, string androidNdkPath) - { - var minValue = IsNdk64BitArch (arch) ? 21 : 14; - var platforms = GetSupportedPlatforms (androidNdkPath).OrderBy (x => x).Where (x => x >= minValue); - return platforms.First (x => Directory.Exists (Path.Combine (androidNdkPath, "platforms", $"android-{x}", $"arch-{archPathMap[arch]}"))); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs index bb8e58a32d1..ffb7b48000e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs @@ -180,10 +180,8 @@ public void BuildAotApplicationAndÜmläüts (string supportedAbis, bool enableL return; //NOTE: Windows has shortened paths such as: C:\Users\myuser\ANDROI~3\ndk\PLATFO~1\AN3971~1\arch-x86\usr\lib\libc.so if (checkMinLlvmPath && !IsWindows) { - bool ndk22OrNewer = false; - if (Xamarin.Android.Tasks.NdkUtil.GetNdkToolchainRelease (AndroidNdkPath, out Xamarin.Android.Tasks.NdkUtilOld.NdkVersion ndkVersion)) { - ndk22OrNewer = ndkVersion.Version >= 22; - } + Xamarin.Android.Tasks.NdkTools ndk = Xamarin.Android.Tasks.NdkTools.Create (AndroidNdkPath); + bool ndk22OrNewer = ndk.Version.Main.Major >= 22; // LLVM passes a direct path to libc.so, and we need to use the libc.so // which corresponds to the *minimum* SDK version specified in AndroidManifest.xml diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/NdkUtilTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/NdkUtilTests.cs index 2028ab07cdd..c1947f59a15 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/NdkUtilTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/NdkUtilTests.cs @@ -33,29 +33,32 @@ public void TestNdkUtil () using (var builder = new Builder ()) { var ndkDir = AndroidNdkPath; var sdkDir = AndroidSdkPath; + NdkTools ndk = NdkTools.Create (ndkDir, log); MonoAndroidHelper.AndroidSdk = new AndroidSdkInfo ((arg1, arg2) => { }, sdkDir, ndkDir, AndroidSdkResolver.GetJavaSdkPath ()); - NdkUtil.Init (log, ndkDir); - var platforms = NdkUtil.GetSupportedPlatforms (log, ndkDir); + var platforms = ndk.GetSupportedPlatforms (); Assert.AreNotEqual (0, platforms.Count (), "No platforms found"); var arch = AndroidTargetArch.X86; - Assert.IsTrue (NdkUtil.ValidateNdkPlatform (log, ndkDir, arch, enableLLVM: false)); - Assert.AreEqual (0, errors.Count, "NdkUtil.ValidateNdkPlatform should not have returned false."); - int level = NdkUtil.GetMinimumApiLevelFor (arch, ndkDir); + Assert.IsTrue (ndk.ValidateNdkPlatform (arch, enableLLVM: false)); + Assert.AreEqual (0, errors.Count, "NdkTools.ValidateNdkPlatform should not have returned false."); + int level = ndk.GetMinimumApiLevelFor (arch); int expected = 16; Assert.AreEqual (expected, level, $"Min Api Level for {arch} should be {expected}."); - var compilerNoQuotes = NdkUtil.GetNdkTool (ndkDir, arch, "gcc", level); - Assert.AreEqual (0, errors.Count, "NdkUtil.GetNdkTool should not have errored."); - Assert.NotNull (compilerNoQuotes, "NdkUtil.GetNdkTool returned null."); - var gas = NdkUtil.GetNdkTool (ndkDir, arch, "as", level); - Assert.AreEqual (0, errors.Count, "NdkUtil.GetNdkTool should not have errored."); - Assert.NotNull (gas, "NdkUtil.GetNdkTool returned null."); - var inc = NdkUtil.GetNdkPlatformIncludePath (ndkDir, arch, level); - Assert.NotNull (inc, " NdkUtil.GetNdkPlatformIncludePath should not return null"); - var libPath = NdkUtil.GetNdkPlatformLibPath (ndkDir, arch, level); - Assert.NotNull (libPath, "NdkUtil.GetNdkPlatformLibPath should not return null"); - string ld = NdkUtil.GetNdkTool (ndkDir, arch, "ld", level); - Assert.AreEqual (0, errors.Count, "NdkUtil.GetNdkTool should not have errored."); - Assert.NotNull (ld, "NdkUtil.GetNdkTool returned null."); + var compilerNoQuotes = ndk.GetToolPath (NdkToolKind.CompilerC, arch, level); + Assert.AreEqual (0, errors.Count, "NdkTools.GetToolPath should not have errored."); + Assert.NotNull (compilerNoQuotes, "NdkTools.GetToolPath returned null for NdkToolKind.CompilerC."); + compilerNoQuotes = ndk.GetToolPath (NdkToolKind.CompilerCPlusPlus, arch, level); + Assert.AreEqual (0, errors.Count, "NdkTools.GetToolPath should not have errored."); + Assert.NotNull (compilerNoQuotes, "NdkTools.GetToolPath returned null for NdkToolKind.CompilerCPlusPlus."); + var gas = ndk.GetToolPath (NdkToolKind.Assembler, arch, level); + Assert.AreEqual (0, errors.Count, "NdkTools.GetToolPath should not have errored."); + Assert.NotNull (gas, "NdkTools.GetToolPath returned null for NdkToolKind.Assembler."); + var inc = ndk.GetDirectoryPath (NdkToolchainDir.PlatformInclude, arch, level); + Assert.NotNull (inc, " NdkTools.GetToolPath should not return null for NdkToolchainDir.PlatformInclude"); + var libPath = ndk.GetDirectoryPath (NdkToolchainDir.PlatformLib, arch, level); + Assert.NotNull (libPath, "NdkTools.GetDirectoryPath should not return null for NdkToolchainDir.PlatformLib"); + string ld = ndk.GetToolPath (NdkToolKind.Linker, arch, level); + Assert.AreEqual (0, errors.Count, "NdkTools.GetToolPath should not have errored."); + Assert.NotNull (ld, "NdkTools.GetToolPath returned null for NdkToolKind.Linker."); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index 478aade9493..1e00bd36d28 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -345,7 +345,7 @@ public static List GatherEnvironmentFiles (string outputDirectoryRoot, s public static void AssertValidEnvironmentSharedLibrary (string outputDirectoryRoot, string sdkDirectory, string ndkDirectory, string supportedAbis) { - NdkUtil.Init (ndkDirectory); + NdkTools ndk = NdkTools.Create (ndkDirectory); MonoAndroidHelper.AndroidSdk = new AndroidSdkInfo ((arg1, arg2) => {}, sdkDirectory, ndkDirectory, AndroidSdkResolver.GetJavaSdkPath ()); AndroidTargetArch arch; @@ -378,7 +378,7 @@ public static void AssertValidEnvironmentSharedLibrary (string outputDirectoryRo Assert.IsTrue (File.Exists (envSharedLibrary), $"Application environment SharedLibrary '{envSharedLibrary}' must exist"); // API level doesn't matter in this case - AssertSharedLibraryHasRequiredSymbols (envSharedLibrary, NdkUtil.GetNdkTool (ndkDirectory, arch, "readelf", 0)); + AssertSharedLibraryHasRequiredSymbols (envSharedLibrary, ndk.GetToolPath ("readelf", arch, 0)); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolKind.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolKind.cs new file mode 100644 index 00000000000..bd3b80b3645 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolKind.cs @@ -0,0 +1,11 @@ +namespace Xamarin.Android.Tasks +{ + public enum NdkToolKind + { + Assembler, + CompilerC, + CompilerCPlusPlus, + Linker, + Strip, + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolchainDir.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolchainDir.cs new file mode 100644 index 00000000000..44c9edb8777 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkToolchainDir.cs @@ -0,0 +1,9 @@ +namespace Xamarin.Android.Tasks +{ + public enum NdkToolchainDir + { + AsmInclude, + PlatformInclude, + PlatformLib, + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkTools.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkTools.cs new file mode 100644 index 00000000000..9342030a358 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkTools.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + public abstract class NdkTools + { + // Target triples used in various places (tools prefix, include directory naming etc) + static readonly Dictionary archTriples = new Dictionary { + { AndroidTargetArch.Arm, "arm-linux-androideabi" }, + { AndroidTargetArch.Arm64, "aarch64-linux-android" }, + { AndroidTargetArch.X86, "i686-linux-android" }, + { AndroidTargetArch.X86_64, "x86_64-linux-android" }, + }; + + // Architecture names as used by the `platforms/*` directories pre NDK r22 + static readonly Dictionary archPlatforms = new Dictionary { + { AndroidTargetArch.Arm, "arm" }, + { AndroidTargetArch.Arm64, "arm64" }, + { AndroidTargetArch.X86, "x86" }, + { AndroidTargetArch.X86_64, "x86_64" }, + }; + + protected Dictionary NdkToolNames = new Dictionary { + { NdkToolKind.Assembler, "as" }, + { NdkToolKind.Linker, "ld" }, + { NdkToolKind.Strip, "strip" }, + }; + + public NdkVersion Version { get; } + public string NdkRootDirectory { get; } + public bool UsesClang { get; protected set; } + public bool NoBinutils { get; protected set; } + + // We can't use MonoAndroidHelper.AndroidSdk.AndroidNdkHostPlatform here since it's + // not initialized while running tests and attempts to use cause a NREX + // We could call `MonoAndroidHelper.RefreshSupportedVersions` but since this NdkTools + // instance already knows the location of NDK, it would be just a waste of time. + protected string HostPlatform => GetNdkHostPlatform (); + protected bool IsWindows => OS.IsWindows; + + protected TaskLoggingHelper? Log { get; } + + protected NdkTools (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log = null) + { + if (String.IsNullOrEmpty (androidNdkPath)) { + throw new ArgumentException ("must be a non-empty string", nameof (androidNdkPath)); + } + + NdkRootDirectory = androidNdkPath; + Version = version; + } + + public static NdkTools? Create (string androidNdkPath, TaskLoggingHelper? log = null) + { + if (String.IsNullOrEmpty (androidNdkPath)) { + log?.LogCodedError ("XA5104", Properties.Resources.XA5104); + return null; + } + + if (!Directory.Exists (androidNdkPath)) { + log?.LogCodedError ("XA5104", Properties.Resources.XA5104); + return null; + } + + NdkVersion? version = ReadVersion (androidNdkPath, log); + if (version == null) { + return null; + } + + if (version.Main.Major < 14) { + if (log != null) { + log.LogCodedError ("XA5104", Properties.Resources.XA5104); + log.LogDebugMessage ($"Unsupported NDK version {version}"); + } + } else if (version.Main.Major < 16) { + // old, non-clang, no unified headers + return new NdkToolsNoClangNoUnifiedHeaders (androidNdkPath, version, log); + } else if (version.Main.Major < 19) { + // old, non-clang, with unified headers + return new NdkToolsNoClangWithUnifiedHeaders (androidNdkPath, version, log); + } else if (version.Main.Major < 22) { + return new NdkToolsWithClangWithPlatforms (androidNdkPath, version, log); + } else if (version.Main.Major == 22) { + return new NdkToolsWithClangNoPlatforms (androidNdkPath, version, log); + } + + return new NdkToolsWithClangNoBinutils (androidNdkPath, version, log); + } + + public abstract string GetToolPath (NdkToolKind kind, AndroidTargetArch arch, int apiLevel); + public abstract string GetToolPath (string name, AndroidTargetArch arch, int apiLevel); + public abstract int GetMinimumApiLevelFor (AndroidTargetArch arch); + public abstract bool ValidateNdkPlatform (Action logMessage, Action logError, AndroidTargetArch arch, bool enableLLVM); + + public bool ValidateNdkPlatform (AndroidTargetArch arch, bool enableLLVM) + { + return ValidateNdkPlatform ((m) => Log?.LogMessage (m), (c, m) => Log?.LogCodedError (c, m), arch, enableLLVM); + } + + public string GetArchDirName (AndroidTargetArch arch) + { + return GetArchTriple (arch);; + } + + public virtual IEnumerable GetSupportedPlatforms () + { + // This works until NDK r22 + foreach (var platform in Directory.EnumerateDirectories (Path.Combine (NdkRootDirectory, "platforms"))) { + var androidApi = Path.GetFileName (platform); + int api = -1; + if (int.TryParse (androidApi.Replace ("android-", String.Empty), out api)) { + yield return api; + } + } + } + + // This call is very specific as it needs to return full path to the location where the arch-prefixed tools + // reside, but WITHOUT the actual tool name. This is required by Mono's AOT LLVM backend. + public string GetNdkToolPrefixForAOT (AndroidTargetArch arch, int apiLevel) + { + string path = GetToolPath (NdkToolKind.Assembler, arch, apiLevel); + return path.Substring (0, path.LastIndexOf ("-") + 1);; + } + + // Work around for a bug in NDK r19 before its 'c' release. See NdkToolsWithClangWithPlatforms.ctor + public virtual string GetCompilerTargetParameters (AndroidTargetArch arch, int apiLevel, bool forCPlusPlus = false) + { + return String.Empty; + } + + public virtual string GetClangDeviceLibraryPath () + { + throw new NotSupportedException (); + } + + public static string GetBinutilsToolchainPrefix (AndroidTargetArch arch) + { + return $"{GetArchTriple (arch)}-"; + } + + public virtual string GetNdkToolchainPrefix (AndroidTargetArch arch) + { + string triple; + switch (arch) { + case AndroidTargetArch.Arm: + case AndroidTargetArch.Arm64: + case AndroidTargetArch.X86: + case AndroidTargetArch.X86_64: + triple = GetArchTriple (arch); + break; + + default: + // return empty. Since this method returns the "prefix", the resulting + // tool path just becomes the tool name i.e. "gcc" becomes "gcc". + // This should work for any custom arbitrary platform. + return String.Empty; + } + + return $"{triple}-"; + } + + public bool IsNdk64BitArch (AndroidTargetArch arch) + { + return arch == AndroidTargetArch.Arm64 || arch == AndroidTargetArch.X86_64; + } + + public string GetDirectoryPath (NdkToolchainDir dir, AndroidTargetArch arch, int apiLevel) + { + string? path = null; + switch (dir) { + case NdkToolchainDir.AsmInclude: // optional + path = GetAsmIncludeDirPath (arch, apiLevel); + if (String.IsNullOrEmpty (path)) { + return String.Empty; + } + break; + + case NdkToolchainDir.PlatformInclude: + path = GetPlatformIncludeDirPath (arch, apiLevel); + break; + + case NdkToolchainDir.PlatformLib: + path = GetPlatformLibPath (arch, apiLevel); + break; + + default: + throw new InvalidOperationException ($"Unsupported NDK toolchain directory {dir}"); + } + + if (String.IsNullOrEmpty (path)) { + throw new InvalidOperationException ($"NDK toolchain directory {dir} is required"); + } + + return EnsureDirectoryExists (path); + } + + protected virtual string GetAsmIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return String.Empty; + } + + protected virtual string GetPlatformLibPath (AndroidTargetArch arch, int apiLevel) + { + // This works until NDK r22 + string libDir = arch == AndroidTargetArch.X86_64 ? "lib64" : "lib"; + return Path.Combine (NdkRootDirectory, "platforms", $"android-{apiLevel}", $"arch-{GetPlatformArch (arch)}", "usr", libDir); + } + + protected abstract string GetPlatformIncludeDirPath (AndroidTargetArch arch, int apiLevel); + + protected string? GetExecutablePath (string toolPath, bool mustExist) + { + string? executablePath = null; + if (IsWindows) { + // We can't just use `File.Exists (toolPath)` since the Windows NDK contains extension-less + // Unix shell scripts which would fail to work if an attempt to execute them would be made. + // + // Also, the NDK r19+ workaround (see NdkToolsWithClangWithPlatforms.ctor) will cause `toolPath` + // here to end with .exe when looking for the compiler and we can save some time by not letting + // `MonoAndroidHelper.GetExecutablePath` iterate over all %PATHEXT% extensions only to return the + // original tool name + if (Path.HasExtension (toolPath) && File.Exists (toolPath)) { + executablePath = toolPath; + } else { + string toolDir = Path.GetDirectoryName (toolPath); + executablePath = Path.Combine (toolDir, MonoAndroidHelper.GetExecutablePath (toolDir, Path.GetFileName (toolPath))); + } + } else if (File.Exists (toolPath)) { + executablePath = toolPath; + } + + if (mustExist && String.IsNullOrEmpty (executablePath)) { + throw new InvalidOperationException ($"Required tool '{toolPath}' not found"); + } + + return executablePath; + } + + protected virtual string GetToolName (NdkToolKind kind) + { + if (!NdkToolNames.TryGetValue (kind, out string? toolName) || String.IsNullOrEmpty (toolName)) { + throw new InvalidOperationException ($"Unsupported NDK tool '{kind}'"); + } + + return toolName; + } + + protected static string GetArchTriple (AndroidTargetArch arch) + { + if (archTriples.TryGetValue (arch, out string? triple) && !String.IsNullOrEmpty (triple)) { + return triple; + } + + throw new InvalidOperationException ($"Unsupported NDK architecture '{arch}'"); + } + + protected static string GetPlatformArch (AndroidTargetArch arch) + { + if (archPlatforms.TryGetValue (arch, out string? name) && !String.IsNullOrEmpty (name)) { + return name; + } + + throw new InvalidOperationException ($"Unsupported NDK architecture '{arch}'"); + } + + protected string EnsureDirectoryExists (string path) + { + if (Directory.Exists (path)) { + return path; + } + + throw new InvalidOperationException ($"Required directory '{path}' not found"); + } + + // This is an ugly compromise to support unified headers with minimum code duplication, because C# doesn't + // support multiple inheritance :( + // + // Unified headers are supported by both clang and non-clang NDKs, so the clang+unified headers NDK class + // (NdkToolsNoClangWithUnifiedHeaders) would have to derive from two classes - one to implement the "with clang" + // option and another to implement the "with unified headers" option. + protected string GetUnifiedHeadersDirPath (string androidNdkPath) + { + return EnsureDirectoryExists (MakeUnifiedHeadersDirPath (androidNdkPath)); + } + + protected virtual string MakeUnifiedHeadersDirPath (string androidNdkPath) + { + return Path.Combine (androidNdkPath, "sysroot", "usr", "include"); + } + + protected string UnifiedHeaders_GetAsmIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return Path.Combine (GetUnifiedHeadersDirPath (NdkRootDirectory), GetArchTriple (arch)); + } + + protected string UnifiedHeaders_GetPlatformIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return GetUnifiedHeadersDirPath (NdkRootDirectory); + } + + const string platformLinux64 = "linux-x86_64"; + const string platformLinux32 = "linux-x86"; + const string platformMac64 = "darwin-x86_64"; + const string platformMac32 = "darwin-x86"; + const string platformWindows64 = "windows-x86_64"; + const string platformWindows32 = "windows-x86"; + + string GetNdkHostPlatform () + { + bool cannotDeterminePlatform = false; + + if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + if (HasPrebuiltDir (platformLinux64)) { + return platformLinux64; + } + + if (HasPrebuiltDir (platformLinux32)) { + return platformLinux32; + } + + cannotDeterminePlatform = true; + } + + if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + if (HasPrebuiltDir (platformMac64)) { + return platformMac64; + } + + if (HasPrebuiltDir (platformMac32)) { + return platformMac32; + } + + cannotDeterminePlatform = true; + } + + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + if (HasPrebuiltDir (platformWindows64)) { + return platformWindows64; + } + + if (HasPrebuiltDir (platformWindows32)) { + return platformWindows32; + } + + cannotDeterminePlatform = true; + } + + if (cannotDeterminePlatform) { + throw new InvalidOperationException ($"Unable to determine host NDK platform"); + } + + throw new InvalidOperationException ($"Unsupported OS"); + + bool HasPrebuiltDir (string name) + { + return Directory.Exists (Path.Combine (NdkRootDirectory, "prebuilt", name)); + } + } + + static NdkVersion? ReadVersion (string androidNdkPath, TaskLoggingHelper? log = null) + { + string sourcePropertiesPath = Path.Combine (androidNdkPath, "source.properties"); + if (!File.Exists (sourcePropertiesPath)) { + if (log != null) { + log.LogCodedError ("XA5104", Properties.Resources.XA5104); + log.LogDebugMessage ("Could not read NDK version information, '{sourcePropertiesPath}' not found."); + } + return null; + } + + var splitChars = new char[] {'='}; + string? ver = null; + foreach (string l in File.ReadAllLines (sourcePropertiesPath)) { + string line = l.Trim (); + if (!line.StartsWith ("Pkg.Revision", StringComparison.Ordinal)) { + continue; + } + + string[] parts = line.Split (splitChars, 2); + if (parts.Length != 2) { + if (log != null) { + log.LogCodedError ("XA5104", Properties.Resources.XA5104); + log.LogDebugMessage ($"Invalid NDK version format in '{sourcePropertiesPath}'."); + } + return null; + } + + ver = parts [1].Trim (); + } + + return new NdkVersion (ver); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkVersion.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkVersion.cs new file mode 100644 index 00000000000..c704d2cceda --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NdkVersion.cs @@ -0,0 +1,39 @@ +using System; + +namespace Xamarin.Android.Tasks +{ + public class NdkVersion + { + public Version Main { get; } + public string Tag { get; } = String.Empty; + + public NdkVersion (string? version) + { + string? ver = version?.Trim (); + if (String.IsNullOrEmpty (ver)) { + throw new ArgumentException ("must be a non-empty string", nameof (version)); + } + + int tagIdx = ver.IndexOf ('-'); + if (tagIdx >= 0) { + Tag = ver.Substring (tagIdx + 1); + ver = ver.Substring (0, tagIdx - 1); + } + + if (!Version.TryParse (ver, out Version? ndkVersion) || ndkVersion == null) { + throw new InvalidOperationException ($"Failed to parse '{ver}' as a valid NDK version."); + } + + Main = ndkVersion; + } + + public override string ToString () + { + if (!String.IsNullOrEmpty (Tag)) { + return $"{Main}-{Tag}"; + } + + return Main.ToString (); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClang.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClang.cs new file mode 100644 index 00000000000..ed915a6614b --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClang.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + abstract class NdkToolsNoClang : NdkTools + { + protected NdkToolsNoClang (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + { + NdkToolNames[NdkToolKind.CompilerC] = "gcc"; + NdkToolNames[NdkToolKind.CompilerCPlusPlus] = "g++"; + } + + public override bool ValidateNdkPlatform (Action logMessage, Action logError, AndroidTargetArch arch, bool enableLLVM) + { + // Check that we have a compatible NDK version for the targeted ABIs. + if (IsNdk64BitArch (arch) && Version.Main.Major < 10) { + logMessage ( + "The detected Android NDK version is incompatible with the targeted 64-bit architecture, " + + "please upgrade to NDK r14 or newer."); + } + + // NDK r10d is buggy and cannot link x86_64 ABI shared libraries because they are 32-bits. + // See https://code.google.com/p/android/issues/detail?id=161421 + if (enableLLVM && Version.Main.Major == 10 && Version.Main.Minor == 4 && arch == AndroidTargetArch.X86_64) { + logError ("XA3004", Properties.Resources.XA3004); + return false; + } + + if (enableLLVM && (Version.Main.Major < 10 || (Version.Main.Major == 10 && Version.Main.Minor < 4))) { + logError ("XA3005", Properties.Resources.XA3005); + } + + return true; + } + + public override int GetMinimumApiLevelFor (AndroidTargetArch arch) + { + var minValue = IsNdk64BitArch (arch) ? 21 : 14; + var platforms = GetSupportedPlatforms ().OrderBy (x => x).Where (x => x >= minValue); + return platforms.First (x => Directory.Exists (Path.Combine (NdkRootDirectory, "platforms", $"android-{x}", $"arch-{GetPlatformArch (arch)}"))); + } + + public override string GetToolPath (NdkToolKind kind, AndroidTargetArch arch, int apiLevel) + { + return GetToolPath (GetToolName (kind), arch, apiLevel); + } + + public override string GetToolPath (string name, AndroidTargetArch arch, int apiLevel) + { + string triple = GetArchTriple (arch); + string toolPath = Path.Combine (NdkRootDirectory, "toolchains", GetArchDirectoryName (arch), "prebuilt", HostPlatform, "bin", $"{triple}-{name}"); + return GetExecutablePath (toolPath, mustExist: true)!; + } + + protected string GetArchDirectoryName (AndroidTargetArch arch) + { + // All toolchains before clang were version 4.9 + string archDir; + + switch (arch) { + case AndroidTargetArch.X86: + archDir = "x86"; + break; + + case AndroidTargetArch.X86_64: + archDir = "x86_64"; + break; + + case AndroidTargetArch.Arm: + case AndroidTargetArch.Arm64: + archDir = GetArchTriple (arch); + break; + + default: + throw new InvalidOperationException ($"Unsupported architecture {arch}"); + } + + return $"{archDir}-4.9"; + } + + public override IEnumerable GetSupportedPlatforms () + { + foreach (var platform in Directory.EnumerateDirectories (Path.Combine (NdkRootDirectory, "platforms"))) { + var androidApi = Path.GetFileName (platform); + int api = -1; + if (int.TryParse (androidApi.Replace ("android-", String.Empty), out api)) { + yield return api; + } + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangNoUnifiedHeaders.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangNoUnifiedHeaders.cs new file mode 100644 index 00000000000..150bbbfe14e --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangNoUnifiedHeaders.cs @@ -0,0 +1,21 @@ +using System.IO; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + // No unified headers + // No clang + class NdkToolsNoClangNoUnifiedHeaders : NdkToolsNoClang + { + public NdkToolsNoClangNoUnifiedHeaders (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + {} + + protected override string GetPlatformIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return Path.Combine (NdkRootDirectory, "platforms", $"android-{apiLevel}", $"arch-{GetPlatformArch (arch)}", "usr", "include"); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangWithUnifiedHeaders.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangWithUnifiedHeaders.cs new file mode 100644 index 00000000000..a3ea01bf6e3 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/NoClangWithUnifiedHeaders.cs @@ -0,0 +1,24 @@ +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + // Unified headers + // No clang + class NdkToolsNoClangWithUnifiedHeaders : NdkToolsNoClang + { + public NdkToolsNoClangWithUnifiedHeaders (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + {} + + protected override string GetAsmIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return UnifiedHeaders_GetAsmIncludeDirPath (arch, apiLevel); + } + + protected override string GetPlatformIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return UnifiedHeaders_GetPlatformIncludeDirPath (arch, apiLevel); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClang.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClang.cs new file mode 100644 index 00000000000..a428902f083 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClang.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + abstract class NdkToolsWithClang : NdkTools + { + protected string UnifiedHeadersDirPath { get; } + + // See NdkToolsWithClangWithPlatforms.ctor for explanation + protected bool NeedClangWorkaround { get; set; } + + protected NdkToolsWithClang (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + { + NdkToolNames[NdkToolKind.CompilerC] = "clang"; + NdkToolNames[NdkToolKind.CompilerCPlusPlus] = "clang++"; + UnifiedHeadersDirPath = GetUnifiedHeadersDirPath (androidNdkPath); + UsesClang = true; + } + + public override bool ValidateNdkPlatform (Action logMessage, Action logError, AndroidTargetArch arch, bool enableLLVM) + { + // Check that we have a compatible NDK version for the targeted ABIs. + if (Version.Main.Major < 19) { + logMessage ( + "The detected Android NDK version is incompatible with this version of Xamarin.Android, " + + "please upgrade to NDK r19 or newer."); + } + + return true; + } + + public override int GetMinimumApiLevelFor (AndroidTargetArch arch) + { + int minValue = 0; + string archName = GetPlatformArch (arch); + if (!XABuildConfig.ArchAPILevels.TryGetValue (archName, out minValue)) + throw new InvalidOperationException ($"Unable to determine minimum API level for architecture {arch}"); + + return minValue; + } + + public override string GetToolPath (NdkToolKind kind, AndroidTargetArch arch, int apiLevel) + { + string toolName = GetToolName (kind); + + if (kind == NdkToolKind.CompilerC || kind == NdkToolKind.CompilerCPlusPlus) { + if (!NeedClangWorkaround) { + // See NdkToolsWithClangWithPlatforms.ctor for explanation + toolName = $"{GetCompilerTriple (arch)}{apiLevel}-{toolName}"; + } + } else { + toolName = $"{GetArchTriple (arch)}-{toolName}"; + } + + return MakeToolPath (toolName); + } + + public override string GetToolPath (string name, AndroidTargetArch arch, int apiLevel) + { + return MakeToolPath ($"{GetArchTriple (arch)}-{name}"); + } + + public override string GetClangDeviceLibraryPath () + { + string toolchainDir = GetToolchainDir (); + string clangBaseDir = Path.Combine (toolchainDir, "lib64", "clang"); + + if (!Directory.Exists (clangBaseDir)) { + throw new InvalidOperationException ($"Clang toolchain directory '{clangBaseDir}' not found"); + } + + // There should be just one subdir - clang version - but it's better to be safe than sorry... + foreach (string dir in Directory.EnumerateDirectories (clangBaseDir)) { + if (dir[0] == '.') { + continue; + } + + string libDir = Path.Combine (dir, "lib", "linux"); + if (Directory.Exists (libDir)) { + return libDir; + } + } + + throw new InvalidOperationException ("Unable to locate clang device library path"); + } + + public override string GetNdkToolchainPrefix (AndroidTargetArch arch) + { + if (arch == AndroidTargetArch.Arm) { + return "armv7a-linux-androideabi-"; + } + + return base.GetNdkToolchainPrefix (arch); + } + + protected string GetCompilerTriple (AndroidTargetArch arch) + { + return arch == AndroidTargetArch.Arm ? "armv7a-linux-androideabi" : GetArchTriple (arch); + } + + protected string MakeToolPath (string toolName) + { + string toolPath = Path.Combine (GetToolchainDir (), "bin", toolName); + + return GetExecutablePath (toolPath, mustExist: true)!; + } + + protected string GetToolchainDir () + { + return Path.Combine (NdkRootDirectory, "toolchains", "llvm", "prebuilt", HostPlatform); + } + + protected override string GetAsmIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return UnifiedHeaders_GetAsmIncludeDirPath (arch, apiLevel); + } + + protected override string GetPlatformIncludeDirPath (AndroidTargetArch arch, int apiLevel) + { + return UnifiedHeaders_GetPlatformIncludeDirPath (arch, apiLevel); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoBinutils.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoBinutils.cs new file mode 100644 index 00000000000..c4b0bb98607 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoBinutils.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Collections.Generic; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + class NdkToolsWithClangNoBinutils : NdkToolsWithClangNoPlatforms + { + public NdkToolsWithClangNoBinutils (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + { + NdkToolNames[NdkToolKind.Linker] = "ld"; + NoBinutils = true; + + throw new NotSupportedException ($"NDK {Version} is not supported by this version of Xamarin.Android"); + } + + public override string GetToolPath (NdkToolKind kind, AndroidTargetArch arch, int apiLevel) + { + switch (kind) { + case NdkToolKind.Assembler: + case NdkToolKind.Linker: + case NdkToolKind.Strip: + return GetEmbeddedToolPath (kind, arch); + + default: + return base.GetToolPath (kind, arch, apiLevel); + } + } + + string GetEmbeddedToolPath (NdkToolKind kind, AndroidTargetArch arch) + { + string toolName = GetToolName (kind); + string triple = GetArchTriple (arch); + + return $"[TODO]/{triple}-{toolName}"; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoPlatforms.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoPlatforms.cs new file mode 100644 index 00000000000..48a5f58a81b --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangNoPlatforms.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + class NdkToolsWithClangNoPlatforms : NdkToolsWithClang + { + public NdkToolsWithClangNoPlatforms (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + { + // NDK r22 removed the arch-prefixed `ld` executable. It provides instead `ld.gold` and `ld.bfd` + // We're going to use the former + + NdkToolNames[NdkToolKind.Linker] = "ld.gold"; + } + + public override IEnumerable GetSupportedPlatforms () + { + // NDK r22 and newer no longer have a single platforms dir. The API level directories are now found in per-arch + // subdirectories under the toolchain directory. We need to examine all of them and compose a list of unique + // API levels (since they are repeated in each per-arch subdirectory, but not all architectures have the + // same set of API levels) + var apiLevels = new HashSet (); + string sysrootLibDir = GetToolchainLibDir (); + var supportedArchitectures = new []{ + AndroidTargetArch.Arm, + AndroidTargetArch.Arm64, + AndroidTargetArch.X86, + AndroidTargetArch.X86_64, + }; + + foreach (AndroidTargetArch targetArch in supportedArchitectures) { + string archDirName = GetArchDirName (targetArch); + if (String.IsNullOrEmpty (archDirName)) { + Log?.LogWarning ($"NDK architecture {targetArch} unknown?"); + continue; + } + + string archDir = Path.Combine (sysrootLibDir, archDirName); + if (!Directory.Exists (archDir)) { + Log?.LogWarning ($"Architecture {targetArch} toolchain directory '{archDir}' not found"); + continue; + } + + foreach (string platform in Directory.EnumerateDirectories (archDir, "*", SearchOption.TopDirectoryOnly)) { + string plibc = Path.Combine (platform, "libc.so"); + if (!File.Exists (plibc)) { + continue; + } + + string pdir = Path.GetFileName (platform); + int api; + if (!Int32.TryParse (pdir, out api) || apiLevels.Contains (api)) { + continue; + } + apiLevels.Add (api); + } + } + + return apiLevels; + } + + protected override string MakeUnifiedHeadersDirPath (string androidNdkPath) + { + return Path.Combine (GetSysrootDir (androidNdkPath), "usr", "include"); + } + + protected override string GetPlatformLibPath (AndroidTargetArch arch, int apiLevel) + { + return Path.Combine (GetToolchainLibDir (), GetArchTriple (arch), apiLevel.ToString ()); + } + + protected string GetSysrootDir (string androidNdkPath) + { + return Path.Combine (androidNdkPath, "toolchains", "llvm", "prebuilt", HostPlatform, "sysroot"); + } + + protected string GetToolchainLibDir () + { + return Path.Combine (GetSysrootDir (NdkRootDirectory), "usr", "lib"); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangWithPlatforms.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangWithPlatforms.cs new file mode 100644 index 00000000000..7c56dc2ebd7 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NdkTools/WithClangWithPlatforms.cs @@ -0,0 +1,70 @@ +using System.Text; + +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + class NdkToolsWithClangWithPlatforms : NdkToolsWithClang + { + public NdkToolsWithClangWithPlatforms (string androidNdkPath, NdkVersion version, TaskLoggingHelper? log) + : base (androidNdkPath, version, log) + { + NeedClangWorkaround = IsWindows && Version.Main.Major >= 19; + if (NeedClangWorkaround) { + // + // NDK r19 bug (fixed in r19c). + // + // The llvm toolchain directory contains a selection of shell scripts (both Unix and Windows) + // which call `clang/clang++` with different `-target` parameters depending on both the target + // architecture and API level. For instance, the clang/clang++ compilers targetting aarch64 on API level + // 28 will have the following Unix shell scripts present in the toolchain `bin` directory: + // + // aarch64-linux-android28-clang + // aarch64-linux-android28-clang++ + // + // However, the Windows version of the NDK has a bug where there is only one Windows + // counterpart to the above Unix scripts: + // + // aarch64-linux-android28-clang.cmd + // + // This script, despite its name suggesting that it calls `clang.exe` in fact calls + // `clang++.exe` which breaks compilation of some C programs (including the code generated by + // Mono's mkbundle utility) because `clang++` treats the input as C++. There is no corresponding + // `aarch64-linux-android28-clang++.cmd` and so invocation of `clang.exe` becomes harder and, + // most certainly, non-standard as far as cross-platform NDK compatibility is concerned. + // + // The code below tries to rectify the situation by special-casing the compiler tool handling to + // return path to the actual .exe instead of the CMD. + // + // Despite the above issue having been fixed in NDK r19c, the CMD scripts in question have another issue - + // they don't work well with paths that have spaces in them. That means we either need to quote the paths + // ourselves, or invoke the compilers directly. The latter option seems to be the better one. + // + NdkToolNames[NdkToolKind.CompilerC] = "clang.exe"; + NdkToolNames[NdkToolKind.CompilerCPlusPlus] = "clang++.exe"; + } + } + + public override string GetCompilerTargetParameters (AndroidTargetArch arch, int apiLevel, bool forCPlusPlus = false) + { + if (!NeedClangWorkaround) { + return base.GetCompilerTargetParameters (arch, apiLevel, forCPlusPlus); + } + + var sb = new StringBuilder (); + sb.Append ("--target=").Append (GetCompilerTriple (arch)).Append (apiLevel); + sb.Append (" -fno-addrsig"); + + if (arch == AndroidTargetArch.X86) { + sb.Append (" -mstackrealign"); + } + + if (forCPlusPlus) { + sb.Append (" -stdlib=libc++"); + } + + return sb.ToString (); + } + } +}