From 0cec16c4bae7b303463d1c8dbe74566b4dc0aabe Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 23 Jul 2018 20:55:53 -0400 Subject: [PATCH] [Xamarin.Android.Tools.AndroidSdk] Add JdkInfo Fixes: https://github.com/xamarin/xamarin-android-tools/issues/26 Context: https://github.com/xamarin/xamarin-android-tools/pull/29#issuecomment-400048306 Refactor out the [underlying functionality][0] of the [`` task][1] so that it is more easily usable. [0]: https://github.com/xamarin/java.interop/commit/4bd9297f311b3e0fb3e07335507a38278b83d255 [1]: https://github.com/xamarin/java.interop/blob/master/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs --- .../JdkInfo.cs | 418 ++++++++++++++++++ src/Xamarin.Android.Tools.AndroidSdk/OS.cs | 9 + .../ProcessUtils.cs | 69 ++- .../Sdks/AndroidSdkBase.cs | 45 +- .../Sdks/AndroidSdkUnix.cs | 24 +- .../Sdks/AndroidSdkWindows.cs | 85 ++-- .../Tests/JdkInfoTests.cs | 159 +++++++ ...arin.Android.Tools.AndroidSdk-Tests.csproj | 1 + .../Xamarin.Android.Tools.AndroidSdk.csproj | 1 + 9 files changed, 720 insertions(+), 91 deletions(-) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Tests/JdkInfoTests.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs new file mode 100644 index 0000000..a30f518 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs @@ -0,0 +1,418 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using System.Xml.Linq; + +namespace Xamarin.Android.Tools +{ + public class JdkInfo { + + public string HomePath {get;} + + public string JarPath {get;} + public string JavaPath {get;} + public string JavacPath {get;} + public string JdkJvmPath {get;} + public ReadOnlyCollection IncludePath {get;} + + public Version Version => javaVersion.Value; + public string Vendor { + get { + if (GetJavaSettingsPropertyValue ("java.vendor", out string vendor)) + return vendor; + return null; + } + } + + public ReadOnlyDictionary ReleaseProperties {get;} + public IEnumerable JavaSettingsPropertyKeys => javaProperties.Value.Keys; + + Lazy>> javaProperties; + Lazy javaVersion; + + public JdkInfo (string homePath) + { + if (homePath == null) + throw new ArgumentNullException (nameof (homePath)); + if (!Directory.Exists (homePath)) + throw new ArgumentException ("Not a directory", nameof (homePath)); + + HomePath = homePath; + + var binPath = Path.Combine (HomePath, "bin"); + JarPath = ProcessUtils.FindExecutablesInDirectory (binPath, "jar").FirstOrDefault (); + JavaPath = ProcessUtils.FindExecutablesInDirectory (binPath, "java").FirstOrDefault (); + JavacPath = ProcessUtils.FindExecutablesInDirectory (binPath, "javac").FirstOrDefault (); + JdkJvmPath = OS.IsMac + ? FindLibrariesInDirectory (HomePath, "jli").FirstOrDefault () + : FindLibrariesInDirectory (Path.Combine (HomePath, "jre"), "jvm").FirstOrDefault (); + + ValidateFile ("jar", JarPath); + ValidateFile ("java", JavaPath); + ValidateFile ("javac", JavacPath); + ValidateFile ("jvm", JdkJvmPath); + + var includes = new List (); + var jdkInclude = Path.Combine (HomePath, "include"); + + if (Directory.Exists (jdkInclude)) { + includes.Add (jdkInclude); + includes.AddRange (Directory.GetDirectories (jdkInclude)); + } + + + ReleaseProperties = GetReleaseProperties(); + + IncludePath = new ReadOnlyCollection (includes); + + javaProperties = new Lazy>> (GetJavaProperties, LazyThreadSafetyMode.ExecutionAndPublication); + javaVersion = new Lazy (GetJavaVersion, LazyThreadSafetyMode.ExecutionAndPublication); + } + + public override string ToString() + { + return $"JdkInfo(Version={Version}, Vendor=\"{Vendor}\", HomePath=\"{HomePath}\")"; + } + + public bool GetJavaSettingsPropertyValues (string key, out IEnumerable value) + { + value = null; + var props = javaProperties.Value; + if (props.TryGetValue (key, out var v)) { + value = v; + return true; + } + return false; + } + + public bool GetJavaSettingsPropertyValue (string key, out string value) + { + value = null; + var props = javaProperties.Value; + if (props.TryGetValue (key, out var v)) { + if (v.Count > 1) + throw new InvalidOperationException ($"Requested to get one string value when property `{key}` contains `{v.Count}` values."); + value = v [0]; + return true; + } + return false; + } + + static IEnumerable FindLibrariesInDirectory (string dir, string libraryName) + { + var library = string.Format (OS.NativeLibraryFormat, libraryName); + return Directory.EnumerateFiles (dir, library, SearchOption.AllDirectories); + } + + void ValidateFile (string name, string path) + { + if (path == null || !File.Exists (path)) + throw new ArgumentException ($"Could not find required file `{name}` within `{HomePath}`; is this a valid JDK?", "homePath"); + } + + static Regex VersionExtractor = new Regex (@"(?[\d]+(\.\d+)+)(_(?\d+))?", RegexOptions.Compiled); + + Version GetJavaVersion () + { + string version = null; + if (!ReleaseProperties.TryGetValue ("JAVA_VERSION", out version)) { + if (GetJavaSettingsPropertyValue ("java.version", out string vs)) + version = vs; + } + if (version == null) + throw new NotSupportedException ("Could not determine Java version"); + var m = VersionExtractor.Match (version); + if (!m.Success) + return null; + version = m.Groups ["version"].Value; + var patch = m.Groups ["patch"].Value; + if (!string.IsNullOrEmpty (patch)) + version += "." + patch; + if (!version.Contains (".")) + version += ".0"; + if (Version.TryParse (version, out Version v)) + return v; + return null; + } + + ReadOnlyDictionary GetReleaseProperties () + { + var releasePath = Path.Combine (HomePath, "release"); + if (!File.Exists (releasePath)) + return new ReadOnlyDictionary (new Dictionary ()); + + var props = new Dictionary (); + using (var release = File.OpenText (releasePath)) { + string line; + while ((line = release.ReadLine ()) != null) { + const string PropertyDelim = "=\""; + int delim = line.IndexOf (PropertyDelim, StringComparison.Ordinal); + if (delim < 0) { + props [line] = ""; + } + string key = line.Substring (0, delim); + string value = line.Substring (delim + PropertyDelim.Length, line.Length - delim - PropertyDelim.Length - 1); + props [key] = value; + } + } + return new ReadOnlyDictionary(props); + } + + Dictionary> GetJavaProperties () + { + return GetJavaProperties (ProcessUtils.FindExecutablesInDirectory (Path.Combine (HomePath, "bin"), "java").First ()); + } + + static Dictionary> GetJavaProperties (string java) + { + var javaProps = new ProcessStartInfo { + FileName = java, + Arguments = "-XshowSettings:properties -version", + }; + + var props = new Dictionary> (); + string curKey = null; + ProcessUtils.Exec (javaProps, (o, e) => { + const string ContinuedValuePrefix = " "; + const string NewValuePrefix = " "; + const string NameValueDelim = " = "; + if (string.IsNullOrEmpty (e.Data)) + return; + if (e.Data.StartsWith (ContinuedValuePrefix, StringComparison.Ordinal)) { + if (curKey == null) + throw new InvalidOperationException ($"Unknown property key for value {e.Data}!"); + props [curKey].Add (e.Data.Substring (ContinuedValuePrefix.Length)); + return; + } + if (e.Data.StartsWith (NewValuePrefix, StringComparison.Ordinal)) { + var delim = e.Data.IndexOf (NameValueDelim, StringComparison.Ordinal); + if (delim <= 0) + return; + curKey = e.Data.Substring (NewValuePrefix.Length, delim - NewValuePrefix.Length); + var value = e.Data.Substring (delim + NameValueDelim.Length); + List values; + if (!props.TryGetValue (curKey, out values)) + props.Add (curKey, values = new List ()); + values.Add (value); + } + }); + + return props; + } + + public static IEnumerable GetKnownSystemJdkInfos (Action logger) + { + return GetWindowsJdks (logger) + .Concat (GetConfiguredJdks (logger)) + .Concat (GetMacOSMicrosoftJdks (logger)) + .Concat (GetJavaHomeEnvironmentJdks (logger)) + .Concat (GetLibexecJdks (logger)) + .Concat (GetPathEnvironmentJdks (logger)) + .Concat (GetJavaAlternativesJdks (logger)) + ; + } + + static IEnumerable GetConfiguredJdks (Action logger) + { + return GetConfiguredJdkPaths (logger) + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetConfiguredJdkPaths (Action logger) + { + var config = AndroidSdkUnix.GetUnixConfigFile (logger); + foreach (var java_sdk in config.Root.Elements ("java-sdk")) { + var path = (string) java_sdk.Attribute ("path"); + yield return path; + } + } + + static IEnumerable GetMacOSMicrosoftJdks (Action logger) + { + return GetMacOSMicrosoftJdkPaths () + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetMacOSMicrosoftJdkPaths () + { + var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal); + var jdks = Path.Combine (home, "Library", "Developer", "Xamarin", "jdk"); + if (!Directory.Exists (jdks)) + return Enumerable.Empty (); + + return Directory.EnumerateDirectories (jdks); + } + + static JdkInfo TryGetJdkInfo (string path, Action logger) + { + JdkInfo jdk = null; + try { + jdk = new JdkInfo (path); + } + catch (Exception e) { + logger (TraceLevel.Warning, $"Not a valid JDK directory: `{path}`"); + logger (TraceLevel.Verbose, e.ToString ()); + } + return jdk; + } + + static IEnumerable GetWindowsJdks (Action logger) + { + if (!OS.IsWindows) + return Enumerable.Empty (); + return AndroidSdkWindows.GetJdkInfos (logger); + } + + static IEnumerable GetJavaHomeEnvironmentJdks (Action logger) + { + var java_home = Environment.GetEnvironmentVariable ("JAVA_HOME"); + if (string.IsNullOrEmpty (java_home)) + yield break; + var jdk = TryGetJdkInfo (java_home, logger); + if (jdk != null) + yield return jdk; + } + + // macOS + static IEnumerable GetLibexecJdks (Action logger) + { + return GetLibexecJdkPaths (logger) + .Distinct () + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetLibexecJdkPaths (Action logger) + { + var java_home = Path.GetFullPath ("/usr/libexec/java_home"); + if (!File.Exists (java_home)) { + yield break; + } + var jhp = new ProcessStartInfo { + FileName = java_home, + Arguments = "-X", + }; + var xml = new StringBuilder (); + ProcessUtils.Exec (jhp, (o, e) => { + if (string.IsNullOrEmpty (e.Data)) + return; + xml.Append (e.Data); + }); + var plist = XElement.Parse (xml.ToString ()); + foreach (var info in plist.Elements ("array").Elements ("dict")) { + var JVMHomePath = (XNode) info.Elements ("key").FirstOrDefault (e => e.Value == "JVMHomePath"); + if (JVMHomePath == null) + continue; + while (JVMHomePath.NextNode.NodeType != XmlNodeType.Element) + JVMHomePath = JVMHomePath.NextNode; + var strElement = (XElement) JVMHomePath.NextNode; + var path = strElement.Value; + yield return path; + } + } + + // Linux; Ubuntu & Derivatives + static IEnumerable GetJavaAlternativesJdks (Action logger) + { + return GetJavaAlternativesJdkPaths () + .Distinct () + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null); + } + + static IEnumerable GetJavaAlternativesJdkPaths () + { + var alternatives = Path.GetFullPath ("/usr/sbin/update-java-alternatives"); + if (!File.Exists (alternatives)) + return Enumerable.Empty (); + + var psi = new ProcessStartInfo { + FileName = alternatives, + Arguments = "-l", + }; + var paths = new List (); + ProcessUtils.Exec (psi, (o, e) => { + if (string.IsNullOrWhiteSpace (e.Data)) + return; + // Example line: + // java-1.8.0-openjdk-amd64 1081 /usr/lib/jvm/java-1.8.0-openjdk-amd64 + var columns = e.Data.Split (new[]{ ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (columns.Length <= 2) + return; + paths.Add (columns [2]); + }); + return paths; + } + + // Linux; Fedora + static IEnumerable GetLibJvmJdks (Action logger) + { + return GetLibJvmJdkPaths () + .Distinct () + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } + + static IEnumerable GetLibJvmJdkPaths () + { + var jvm = "/usr/lib/jvm"; + if (!Directory.Exists (jvm)) + yield break; + + foreach (var jdk in Directory.EnumerateDirectories (jvm)) { + var release = Path.Combine (jdk, "release"); + if (File.Exists (release)) + yield return jdk; + } + } + + // Last-ditch fallback! + static IEnumerable GetPathEnvironmentJdks (Action logger) + { + return GetPathEnvironmentJdkPaths () + .Select (p => TryGetJdkInfo (p, logger)) + .Where (jdk => jdk != null); + } + + static IEnumerable GetPathEnvironmentJdkPaths () + { + foreach (var java in ProcessUtils.FindExecutablesInPath ("java")) { + var props = GetJavaProperties (java); + if (props.TryGetValue ("java.home", out var java_homes)) { + var java_home = java_homes [0]; + // `java -XshowSettings:properties -version 2>&1 | grep java.home` ends with `/jre` on macOS. + // We need the parent dir so we can properly lookup the `include` directories + if (java_home.EndsWith ("jre", StringComparison.OrdinalIgnoreCase)) { + java_home = Path.GetDirectoryName (java_home); + } + yield return java_home; + } + } + } + } + + class JdkInfoVersionComparer : IComparer + { + public static readonly IComparer Default = new JdkInfoVersionComparer (); + + public int Compare (JdkInfo x, JdkInfo y) + { + if (x.Version != null && y.Version != null) + return x.Version.CompareTo (y.Version); + return 0; + } + } +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/OS.cs b/src/Xamarin.Android.Tools.AndroidSdk/OS.cs index 7c04c47..81e7766 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/OS.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/OS.cs @@ -12,6 +12,8 @@ public class OS internal readonly static string ProgramFilesX86; + internal readonly static string NativeLibraryFormat; + static OS () { IsWindows = Path.DirectorySeparatorChar == '\\'; @@ -20,6 +22,13 @@ static OS () if (IsWindows) { ProgramFilesX86 = GetProgramFilesX86 (); } + + if (IsWindows) + NativeLibraryFormat = "{0}.dll"; + if (IsMac) + NativeLibraryFormat = "lib{0}.dylib"; + if (!IsWindows && !IsMac) + NativeLibraryFormat = "lib{0}.so"; } //From Managed.Windows.Forms/XplatUI diff --git a/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs b/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs index 3dc244f..87b02ca 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs @@ -1,13 +1,24 @@ using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Xamarin.Android.Tools { public static class ProcessUtils { + static string[] ExecutableFileExtensions; + + static ProcessUtils () + { + var pathExt = Environment.GetEnvironmentVariable ("PATHEXT"); + var pathExts = pathExt?.Split (new char [] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries) ?? new string [0]; + + ExecutableFileExtensions = pathExts; + } + public static async Task StartProcess (ProcessStartInfo psi, TextWriter stdout, TextWriter stderr, CancellationToken cancellationToken, Action onStarted = null) { cancellationToken.ThrowIfCancellationRequested (); @@ -119,6 +130,62 @@ public static Task ExecuteToolAsync (string exe, Func FindExecutablesInPath (string executable) + { + var path = Environment.GetEnvironmentVariable ("PATH"); + var pathDirs = path.Split (new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var dir in pathDirs) { + foreach (var exe in FindExecutablesInDirectory (dir, executable)) { + yield return exe; + } + } + } + + internal static IEnumerable FindExecutablesInDirectory (string dir, string executable) + { + foreach (var exe in ExecutableFiles (executable)) { + var exePath = Path.Combine (dir, exe); + if (File.Exists (exePath)) + yield return exePath; + } + } + + internal static IEnumerable ExecutableFiles (string executable) + { + if (ExecutableFileExtensions == null || ExecutableFileExtensions.Length == 0) { + yield return executable; + yield break; + } + + foreach (var ext in ExecutableFileExtensions) + yield return Path.ChangeExtension (executable, ext); + yield return executable; + } } } diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs index b317863..1537351 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs @@ -122,7 +122,7 @@ public string NdkHostPlatform { /// public bool ValidateAndroidSdkLocation (string loc) { - return !string.IsNullOrEmpty (loc) && FindExecutableInDirectory (Adb, Path.Combine (loc, "platform-tools")).Any (); + return !string.IsNullOrEmpty (loc) && ProcessUtils.FindExecutablesInDirectory (Path.Combine (loc, "platform-tools"), Adb).Any (); } /// @@ -130,7 +130,7 @@ public bool ValidateAndroidSdkLocation (string loc) /// public virtual bool ValidateJavaSdkLocation (string loc) { - return !string.IsNullOrEmpty (loc) && FindExecutableInDirectory (JarSigner, Path.Combine (loc, "bin")).Any (); + return !string.IsNullOrEmpty (loc) && ProcessUtils.FindExecutablesInDirectory (Path.Combine (loc, "bin"), JarSigner).Any (); } /// @@ -138,42 +138,10 @@ public virtual bool ValidateJavaSdkLocation (string loc) /// public bool ValidateAndroidNdkLocation (string loc) { - return !string.IsNullOrEmpty (loc) && FindExecutableInDirectory (NdkStack, loc).Any (); + return !string.IsNullOrEmpty (loc) && ProcessUtils.FindExecutablesInDirectory (loc, NdkStack).Any (); } - protected IEnumerable FindExecutableInPath (string executable) - { - var path = Environment.GetEnvironmentVariable ("PATH"); - var pathDirs = path.Split (new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var dir in pathDirs) { - foreach (var directory in FindExecutableInDirectory (executable, dir)) { - yield return directory; - } - } - } - - protected IEnumerable FindExecutableInDirectory (string executable, string dir) - { - foreach (var exe in Executables (executable)) - if (File.Exists (Path.Combine (dir, exe))) - yield return dir; - } - - IEnumerable Executables (string executable) - { - yield return executable; - var pathExt = Environment.GetEnvironmentVariable ("PATHEXT"); - var pathExts = pathExt?.Split (new char [] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); - - if (pathExts == null) - yield break; - - foreach (var ext in pathExts) - yield return Path.ChangeExtension (executable, ext); - } - - protected string NullIfEmpty (string s) + protected static string NullIfEmpty (string s) { if (s == null || s.Length != 0) return s; @@ -181,11 +149,12 @@ protected string NullIfEmpty (string s) return null; } - string GetExecutablePath (string dir, string exe) + static string GetExecutablePath (string dir, string exe) { if (string.IsNullOrEmpty (dir)) return exe; - foreach (var e in Executables (exe)) + + foreach (var e in ProcessUtils.ExecutableFiles (exe)) if (File.Exists (Path.Combine (dir, e))) return e; return exe; diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs index 0fb5cfd..db43e89 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs @@ -42,7 +42,7 @@ public override string NdkHostPlatform64Bit { public override string PreferedAndroidSdkPath { get { - var config_file = GetUnixConfigFile (); + var config_file = GetUnixConfigFile (Logger); var androidEl = config_file.Root.Element ("android-sdk"); if (androidEl != null) { @@ -57,7 +57,7 @@ public override string PreferedAndroidSdkPath { public override string PreferedAndroidNdkPath { get { - var config_file = GetUnixConfigFile (); + var config_file = GetUnixConfigFile (Logger); var androidEl = config_file.Root.Element ("android-ndk"); if (androidEl != null) { @@ -72,7 +72,7 @@ public override string PreferedAndroidNdkPath { public override string PreferedJavaSdkPath { get { - var config_file = GetUnixConfigFile (); + var config_file = GetUnixConfigFile (Logger); var javaEl = config_file.Root.Element ("java-sdk"); if (javaEl != null) { @@ -92,7 +92,7 @@ protected override IEnumerable GetAllAvailableAndroidSdks () yield return preferedSdkPath; // Look in PATH - foreach (var path in FindExecutableInPath (Adb)) { + foreach (var path in ProcessUtils.FindExecutablesInPath (Adb)) { // Strip off "platform-tools" var dir = Path.GetDirectoryName (path); @@ -113,7 +113,7 @@ protected override string GetJavaSdkPath () return preferedJavaSdkPath; // Look in PATH - foreach (var path in FindExecutableInPath (JarSigner)) { + foreach (var path in ProcessUtils.FindExecutablesInPath (JarSigner)) { // Strip off "bin" var dir = Path.GetDirectoryName (path); @@ -160,7 +160,7 @@ protected override IEnumerable GetAllAvailableAndroidNdks () yield return preferedNdkPath; // Look in PATH - foreach (var path in FindExecutableInPath (NdkStack)) { + foreach (var path in ProcessUtils.FindExecutablesInPath (NdkStack)) { if (ValidateAndroidNdkLocation (path)) yield return path; } @@ -176,7 +176,7 @@ public override void SetPreferredAndroidSdkPath (string path) { path = NullIfEmpty (path); - var doc = GetUnixConfigFile (); + var doc = GetUnixConfigFile (Logger); var androidEl = doc.Root.Element ("android-sdk"); if (androidEl == null) { @@ -192,7 +192,7 @@ public override void SetPreferredJavaSdkPath (string path) { path = NullIfEmpty (path); - var doc = GetUnixConfigFile (); + var doc = GetUnixConfigFile (Logger); var javaEl = doc.Root.Element ("java-sdk"); if (javaEl == null) { @@ -208,7 +208,7 @@ public override void SetPreferredAndroidNdkPath (string path) { path = NullIfEmpty (path); - var doc = GetUnixConfigFile (); + var doc = GetUnixConfigFile (Logger); var androidEl = doc.Root.Element ("android-ndk"); if (androidEl == null) { @@ -275,7 +275,7 @@ private static string UnixConfigPath { } } - private XDocument GetUnixConfigFile () + internal static XDocument GetUnixConfigFile (Action logger) { var file = UnixConfigPath; XDocument doc = null; @@ -283,8 +283,8 @@ private XDocument GetUnixConfigFile () try { doc = XDocument.Load (file); } catch (Exception ex) { - Logger (TraceLevel.Error, "Could not load monodroid configuration file"); - Logger (TraceLevel.Verbose, ex.ToString ()); + logger (TraceLevel.Error, "Could not load monodroid configuration file"); + logger (TraceLevel.Verbose, ex.ToString ()); // move out of the way and create a new one doc = new XDocument (new XElement ("monodroid")); diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs index 5977ec2..32a0bdc 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs @@ -22,8 +22,10 @@ public AndroidSdkWindows (Action logger) { } + static readonly string _JarSigner = "jarsigner.exe"; + public override string ZipAlign { get; protected set; } = "zipalign.exe"; - public override string JarSigner { get; protected set; } = "jarsigner.exe"; + public override string JarSigner { get; protected set; } = _JarSigner; public override string KeyTool { get; protected set; } = "keytool.exe"; public override string NdkHostPlatform32Bit { get { return "windows"; } } @@ -58,7 +60,7 @@ public override string PreferedJavaSdkPath { } } - string GetMDRegistryKey () + static string GetMDRegistryKey () { var regKey = Environment.GetEnvironmentVariable ("XAMARIN_ANDROID_REGKEY"); return string.IsNullOrWhiteSpace (regKey) ? MDREG_KEY : regKey; @@ -104,18 +106,37 @@ protected override IEnumerable GetAllAvailableAndroidSdks () protected override string GetJavaSdkPath () { - var preferredJdkPath = GetPreferredJdkPath (); - if (!string.IsNullOrEmpty (preferredJdkPath)) - return preferredJdkPath; + var jdk = GetJdkInfos (Logger).FirstOrDefault (); + return jdk?.HomePath; + } + + internal static IEnumerable GetJdkInfos (Action logger) + { + JdkInfo TryGetJdkInfo (string path) + { + JdkInfo jdk = null; + try { + jdk = new JdkInfo (path); + } + catch (Exception e) { + logger (TraceLevel.Warning, e.ToString ()); + } + return jdk; + } - var openJdkPath = GetOpenJdkPath (); - if (!string.IsNullOrEmpty (openJdkPath)) - return openJdkPath; + IEnumerable ToJdkInfos (IEnumerable paths) + { + return paths.Select (TryGetJdkInfo) + .Where (jdk => jdk != null) + .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); + } - return GetOracleJdkPath (); + return ToJdkInfos (GetPreferredJdkPaths ()) + .Concat (ToJdkInfos (GetOpenJdkPaths ())) + .Concat (ToJdkInfos (GetOracleJdkPaths ())); } - private string GetPreferredJdkPath () + private static IEnumerable GetPreferredJdkPaths () { // check the user specified path var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; @@ -123,14 +144,12 @@ private string GetPreferredJdkPath () var regKey = GetMDRegistryKey (); foreach (var root in roots) { - if (CheckRegistryKeyForExecutable (root, regKey, MDREG_JAVA_SDK, wow, "bin", JarSigner)) - return RegistryEx.GetValueString (root, regKey, MDREG_JAVA_SDK, wow); + if (CheckRegistryKeyForExecutable (root, regKey, MDREG_JAVA_SDK, wow, "bin", _JarSigner)) + yield return RegistryEx.GetValueString (root, regKey, MDREG_JAVA_SDK, wow); } - - return null; } - private string GetOpenJdkPath () + private static IEnumerable GetOpenJdkPaths () { var root = RegistryEx.LocalMachine; var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }; @@ -138,42 +157,32 @@ private string GetOpenJdkPath () var valueName = "JavaHome"; foreach (var wow in wows) { - if (CheckRegistryKeyForExecutable (root, subKey, valueName, wow, "bin", JarSigner)) - return RegistryEx.GetValueString (root, subKey, valueName, wow); + if (CheckRegistryKeyForExecutable (root, subKey, valueName, wow, "bin", _JarSigner)) + yield return RegistryEx.GetValueString (root, subKey, valueName, wow); } - - return null; } - private string GetOracleJdkPath () + private static IEnumerable GetOracleJdkPaths () { string subkey = @"SOFTWARE\JavaSoft\Java Development Kit"; - Logger (TraceLevel.Info, "Looking for Java 6 SDK..."); - foreach (var wow64 in new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }) { string key_name = string.Format (@"{0}\{1}\{2}", "HKLM", subkey, "CurrentVersion"); var currentVersion = RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey, "CurrentVersion", wow64); if (!string.IsNullOrEmpty (currentVersion)) { - Logger (TraceLevel.Info, $" Key {key_name} found."); // No matter what the CurrentVersion is, look for 1.6 or 1.7 or 1.8 - if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64, "bin", JarSigner)) - return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64); + if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64, "bin", _JarSigner)) + yield return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.8", "JavaHome", wow64); - if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.7", "JavaHome", wow64, "bin", JarSigner)) - return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.7", "JavaHome", wow64); + if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.7", "JavaHome", wow64, "bin", _JarSigner)) + yield return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.7", "JavaHome", wow64); - if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.6", "JavaHome", wow64, "bin", JarSigner)) - return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.6", "JavaHome", wow64); + if (CheckRegistryKeyForExecutable (RegistryEx.LocalMachine, subkey + "\\" + "1.6", "JavaHome", wow64, "bin", _JarSigner)) + yield return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.6", "JavaHome", wow64); } - - Logger (TraceLevel.Info, $" Key {key_name} not found."); } - - // We ran out of things to check.. - return null; } protected override IEnumerable GetAllAvailableAndroidNdks () @@ -234,24 +243,20 @@ public override void SetPreferredAndroidNdkPath (string path) } #region Helper Methods - private bool CheckRegistryKeyForExecutable (UIntPtr key, string subkey, string valueName, RegistryEx.Wow64 wow64, string subdir, string exe) + private static bool CheckRegistryKeyForExecutable (UIntPtr key, string subkey, string valueName, RegistryEx.Wow64 wow64, string subdir, string exe) { string key_name = string.Format (@"{0}\{1}\{2}", key == RegistryEx.CurrentUser ? "HKCU" : "HKLM", subkey, valueName); var path = NullIfEmpty (RegistryEx.GetValueString (key, subkey, valueName, wow64)); if (path == null) { - Logger (TraceLevel.Info, $" Key {key_name} not found."); return false; } - if (!FindExecutableInDirectory (exe, Path.Combine (path, subdir)).Any ()) { - Logger (TraceLevel.Info, $" Key {key_name} found:\n Path does not contain {exe} in \\{subdir} ({path})."); + if (!ProcessUtils.FindExecutablesInDirectory (Path.Combine (path, subdir), exe).Any ()) { return false; } - Logger (TraceLevel.Info, $" Key {key_name} found:\n Path contains {exe} in \\{subdir} ({path})."); - return true; } #endregion diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Tests/JdkInfoTests.cs b/src/Xamarin.Android.Tools.AndroidSdk/Tests/JdkInfoTests.cs new file mode 100644 index 0000000..babac4f --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Tests/JdkInfoTests.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.IO; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class JdkInfoTests + { + [Test] + public void Constructor_NullPath () + { + Assert.Throws(() => new JdkInfo (null)); + } + + [Test] + public void Constructor_InvalidPath () + { + var dir = Path.GetTempFileName (); + File.Delete (dir); + Directory.CreateDirectory (dir); + Assert.Throws(() => new JdkInfo (dir)); + Directory.Delete (dir); + } + + string FauxJdkDir; + + [OneTimeSetUp] + public void CreateFauxJdk () + { + var dir = Path.GetTempFileName(); + File.Delete (dir); + Directory.CreateDirectory (dir); + + using (var release = new StreamWriter (Path.Combine (dir, "release"))) { + release.WriteLine ("JAVA_VERSION=\"1.2.3.4\""); + } + + var bin = Path.Combine (dir, "bin"); + var inc = Path.Combine (dir, "include"); + var jli = Path.Combine (dir, "jli"); + var jre = Path.Combine (dir, "jre"); + + Directory.CreateDirectory (bin); + Directory.CreateDirectory (inc); + Directory.CreateDirectory (jli); + Directory.CreateDirectory (jre); + + CreateShellScript (Path.Combine (bin, "jar"), ""); + CreateShellScript (Path.Combine (bin, "java"), JavaScript); + CreateShellScript (Path.Combine (bin, "javac"), ""); + CreateShellScript (Path.Combine (dir, "jli", "libjli.dylib"), ""); + CreateShellScript (Path.Combine (jre, "libjvm.so"), ""); + CreateShellScript (Path.Combine (jre, "jvm.dll"), ""); + + FauxJdkDir = dir; + } + + [OneTimeTearDown] + public void DeleteFauxJdk () + { + Directory.Delete (FauxJdkDir, recursive: true); + } + + static readonly string JavaScript = + $"echo Property settings:{Environment.NewLine}" + + $"echo \" java.vendor = Xamarin.Android Unit Tests\"{Environment.NewLine}" + + $"echo \" java.version = 100.100.100\"{Environment.NewLine}" + + $"echo \" xamarin.multi-line = line the first\"{Environment.NewLine}" + + $"echo \" line the second\"{Environment.NewLine}" + + $"echo \" .\"{Environment.NewLine}"; + + static void CreateShellScript (string path, string contents) + { + if (OS.IsWindows) + path += ".cmd"; + using (var script = new StreamWriter (path)) { + if (!OS.IsWindows) { + script.WriteLine ("#!/bin/sh"); + } + script.WriteLine (contents); + } + if (OS.IsWindows) + return; + var chmod = new ProcessStartInfo { + FileName = "chmod", + Arguments = $"+x \"{path}\"", + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + var p = Process.Start (chmod); + p.WaitForExit (); + } + + [Test] + public void PathPropertyValidation () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (jdk.HomePath, FauxJdkDir); + Assert.IsTrue (File.Exists (jdk.JarPath)); + Assert.IsTrue (File.Exists (jdk.JavaPath)); + Assert.IsTrue (File.Exists (jdk.JavacPath)); + Assert.IsTrue (File.Exists (jdk.JdkJvmPath)); + Assert.IsTrue (Directory.Exists (jdk.IncludePath [0])); + } + + [Test] + public void VersionPrefersRelease () + { + var jdk = new JdkInfo (FauxJdkDir); + // Note: `release` has JAVA_VERSION=1.2.3.4, while `java` prints java.version=100.100.100. + // We prefer the value within `releas`. + Assert.AreEqual (jdk.Version, new Version ("1.2.3.4")); + } + + [Test] + public void ReleaseProperties () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (1, jdk.ReleaseProperties.Count); + Assert.AreEqual ("1.2.3.4", jdk.ReleaseProperties ["JAVA_VERSION"]); + } + + [Test] + public void JavaSettingsProperties () + { + var jdk = new JdkInfo (FauxJdkDir); + + Assert.AreEqual (3, jdk.JavaSettingsPropertyKeys.Count ()); + + Assert.IsFalse(jdk.GetJavaSettingsPropertyValue ("does-not-exist", out var _)); + Assert.IsFalse(jdk.GetJavaSettingsPropertyValues ("does-not-exist", out var _)); + + Assert.IsTrue (jdk.GetJavaSettingsPropertyValue ("java.version", out var version)); + Assert.AreEqual ("100.100.100", version); + + Assert.IsTrue (jdk.GetJavaSettingsPropertyValue ("java.vendor", out var vendor)); + Assert.AreEqual ("Xamarin.Android Unit Tests", vendor); + Assert.AreEqual (vendor, jdk.Vendor); + + Assert.Throws(() => jdk.GetJavaSettingsPropertyValue ("xamarin.multi-line", out var _)); + Assert.IsTrue (jdk.GetJavaSettingsPropertyValues ("xamarin.multi-line", out var lines)); + Assert.AreEqual (3, lines.Count ()); + Assert.AreEqual ("line the first", lines.ElementAt (0)); + Assert.AreEqual ("line the second", lines.ElementAt (1)); + Assert.AreEqual (".", lines.ElementAt (2)); + } + } +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj index b3ff7e1..7cb376d 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj +++ b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj index df78969..5adcf4f 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj +++ b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj @@ -43,6 +43,7 @@ +