From d03ff8c50a21bb3b3bf6e758d66e5fc36a271ef1 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 25 May 2018 17:27:25 -0400 Subject: [PATCH] [Java.Interop.BootstrapTasks] Support single-digit versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `` task sorts preferred JDK versions based on the version number embedded into the directory name. This unfortunately requires that the version number embedded into the directory name contain two or more version parts. This is true on macOS and Windows: # macOS $ ls -1tr /Library/Java/JavaVirtualMachines/ 1.6.0_65-b14-462.jdk jdk1.8.0_77.jdk jdk-9.0.4.jdk But this is not necessarily the case on Linux: $ ls -1 /usr/lib/jvm java-8-openjdk-amd64 java-8-oracle Single-part version numbers aren't supported by `Version.TryParse()`, so when `$(JI_MAX_JDK)` is set -- causing `maxVersion` to be non-null -- while the version *isn't* extracted from the directory name, the [comparison can fail][0]: [0]: https://jenkins.mono-project.com/job/xamarin-anroid-linux-pr-builder/2917/console The "JdkInfo" task failed unexpectedly. System.ArgumentNullException: Value cannot be null. Parameter name: v1 at System.Version.op_LessThanOrEqual (System.Version v1, System.Version v2) [0x00003] in <2f83d2ff70e3444cb3582fe4e97bad63>:0 at Java.Interop.BootstrapTasks.JdkInfo+<>c__DisplayClass31_0.b__2 (<>f__AnonymousType0`2[j__TPar,j__TPar] v) [0x00000] in …/xamarin-android/external/Java.Interop/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs:172 ... at Java.Interop.BootstrapTasks.JdkInfo.GetJavaHomePathFromMachine (System.Version maxVersion) [0x00015] in …/xamarin-android/external/Java.Interop/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs:163 at Java.Interop.BootstrapTasks.JdkInfo.Execute () [0x00176] in …/xamarin-android/external/Java.Interop/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs:56 Fix this by allowing extraction of Version numbers from single-part version codes as seen on Linux, allowing us to perform comparisons against non-null versions. If multiple directories *still* have matching version numbers, then we sort the output so that the location with the most recent write time is used. Additionally, check some OS-specific preferred locations: * On macOS, `/usr/libexec/java_home`, which is a program which prints out the preferred JDK location to stdout. * On Linux, `/usr/alternatives/java` is a symlink to the preferred JDK location, which we can read using `readlink`. Treat the OS-specific locations similar to `$JAVA_HOME`, and check them *before* attempting the `JdkInfo.JdksRoot` children. --- build-tools/scripts/jdk.mk | 5 +- .../Java.Interop.BootstrapTasks/JdkInfo.cs | 238 ++++++++++++++---- 2 files changed, 187 insertions(+), 56 deletions(-) diff --git a/build-tools/scripts/jdk.mk b/build-tools/scripts/jdk.mk index 79a800c53..f078f78a6 100644 --- a/build-tools/scripts/jdk.mk +++ b/build-tools/scripts/jdk.mk @@ -27,7 +27,8 @@ OS ?= $(shell uname) -_INCLUDE_MK = bin/Build$(CONFIGURATION)/JdkInfo.mk +_INCLUDE_MK = bin/Build$(CONFIGURATION)/JdkInfo.mk +_INCLUDE_PROPS = bin/Build$(CONFIGURATION)/JdkInfo.props prepare:: $(_INCLUDE_MK) @@ -41,7 +42,7 @@ ifeq ($(OS),Linux) _JDKS_ROOT := /usr/lib/jvm endif # $(OS)=Linux -$(_INCLUDE_MK): bin/Build$(CONFIGURATION)/Java.Interop.BootstrapTasks.dll +$(_INCLUDE_MK) $(_INCLUDE_PROPS): bin/Build$(CONFIGURATION)/Java.Interop.BootstrapTasks.dll $(MSBUILD) $(MSBUILD_FLAGS) build-tools/scripts/jdk.targets /t:GetPreferredJdkRoot \ /p:JdksRoot="$(_JDKS_ROOT)" \ $(if $(JI_MAX_JDK),"/p:MaximumJdkVersion=$(JI_MAX_JDK)") diff --git a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs index e1ab52470..ac915d925 100644 --- a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs +++ b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs @@ -40,41 +40,46 @@ public override bool Execute () Log.LogMessage (MessageImportance.Low, $" {nameof (MaximumJdkVersion)}: {MaximumJdkVersion}"); Log.LogMessage (MessageImportance.Low, $" {nameof (PropertyFile)}: {PropertyFile}"); - var maxVersion = GetMaxJdkVersion (); + var maxVersion = GetVersion (MaximumJdkVersion); + + string jarPath = null; + string javacPath = null; + string jdkJvmPath = null; + string includePath = null; + var java_home = GetJavaHomePathFromEnvironment (); - if (java_home != null) { - var java_home_v = GetVersionFromPath (java_home); - if (maxVersion != null && java_home_v != null && java_home_v > maxVersion) { - Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME default value of `{java_home}` as it exceeds MaximumJdkVersion={MaximumJdkVersion}."); - java_home = null; - } - if (java_home != null && !Directory.Exists (Path.Combine (java_home, "include"))) { - Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME default value of `{java_home}` as it does not contain an `include` subdirectory."); - java_home = null; - } + if (!ValidateJdkPath (maxVersion, java_home, out jarPath, out javacPath, out jdkJvmPath, out includePath)) { + java_home = null; + } + + if (java_home == null && + (java_home = GetJavaHomePathFromLibexec ()) != null && + !ValidateJdkPath (maxVersion, java_home, out jarPath, out javacPath, out jdkJvmPath, out includePath)) { + java_home = null; + } + + if (java_home == null && + (java_home = GetJavaHomePathFromAlternatives ()) != null && + !ValidateJdkPath (maxVersion, java_home, out jarPath, out javacPath, out jdkJvmPath, out includePath)) { + java_home = null; + } + + if (java_home == null && + (java_home = GetJavaHomePathFromMachine (maxVersion)) != null && + !ValidateJdkPath (maxVersion, java_home, out jarPath, out javacPath, out jdkJvmPath, out includePath)) { + java_home = null; } - java_home = java_home ?? GetJavaHomePathFromMachine (maxVersion); - if (string.IsNullOrEmpty (java_home)) { + if (java_home == null) { Log.LogError ("Could not determine JAVA_HOME location. Please set JdksRoot or export the JAVA_HOME environment variable."); return false; } var includes = new List () { - Path.Combine (java_home, "include"), + includePath, }; - includes.AddRange (Directory.GetDirectories (includes [0])); + includes.AddRange (Directory.GetDirectories (includePath)); - var jarPath = FindExecutablesInDirectory (Path.Combine (java_home, "bin"), "jar").First (); - var javacPath = FindExecutablesInDirectory (Path.Combine (java_home, "bin"), "javac").First (); - var jdkJvmPaths = OS.IsMacOS - ? FindLibrariesInDirectory (java_home, "jli") - : FindLibrariesInDirectory (Path.Combine (java_home, "jre"), "jvm"); - var jdkJvmPath = jdkJvmPaths.First (); - - FileExists (jarPath); - FileExists (javacPath); - FileExists (jdkJvmPath); if (Log.HasLoggedErrors) { return false; } @@ -92,14 +97,17 @@ public override bool Execute () return !Log.HasLoggedErrors; } - Version GetMaxJdkVersion () + Version GetVersion (string value) { - if (string.IsNullOrEmpty (MaximumJdkVersion)) + if (string.IsNullOrEmpty (value)) return null; - if (!MaximumJdkVersion.Contains (".")) { - MaximumJdkVersion += ".0"; + if (!value.Contains (".")) { + value += ".0"; } - return new Version (MaximumJdkVersion); + Version v; + if (Version.TryParse (value, out v)) + return v; + return null; } Version GetVersionFromPath (string path) @@ -107,11 +115,7 @@ Version GetVersionFromPath (string path) var m = VersionExtractor.Match (path); if (!m.Success) return null; - Version v; - if (!Version.TryParse (m.Groups ["version"].Value, out v)) { - return null; - } - return v; + return GetVersion (m.Groups ["version"].Value); } void FileExists (string path) @@ -150,6 +154,56 @@ void WriteMakeFragmentFile (string jarPath, string javacPath, string jdkJvmPath, } } + bool ValidateJdkPath (Version maxVersion, string java_home) + { + return ValidateJdkPath (maxVersion, java_home, + out _, out _, out _, out _); + } + + bool ValidateJdkPath (Version maxVersion, string java_home, + out string jarPath, out string javacPath, out string jdkJvmPath, out string includePath) + { + jarPath = javacPath = jdkJvmPath = includePath = null; + + if (string.IsNullOrEmpty (java_home) || !Directory.Exists (java_home)) + return false; + + var pathVersion = GetVersionFromPath (java_home); + if (maxVersion != null && pathVersion != null && pathVersion > maxVersion) { + Log.LogMessage (MessageImportance.Low, + $" Skipping JAVA_HOME value of `{java_home}` as it exceeds MaximumJdkVersion={MaximumJdkVersion}."); + return false; + } + + jarPath = FindExecutablesInDirectory (Path.Combine (java_home, "bin"), "jar").FirstOrDefault (); + javacPath = FindExecutablesInDirectory (Path.Combine (java_home, "bin"), "javac").FirstOrDefault (); + var jdkJvmPaths = OS.IsMacOS + ? FindLibrariesInDirectory (java_home, "jli") + : FindLibrariesInDirectory (Path.Combine (java_home, "jre"), "jvm"); + jdkJvmPath = jdkJvmPaths.FirstOrDefault (); + includePath = Path.Combine (java_home, "include"); + + if (jarPath == null) { + Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME value of `{java_home}` as `jar` could not be found."); + return false; + } + if (javacPath == null) { + Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME value of `{java_home}` as `javac` could not be found."); + return false; + } + if (jdkJvmPath == null) { + var jvm = OS.IsMacOS ? "libjli.dylib" : string.Format (OS.NativeLibraryFormat, "jvm"); + Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME value of `{java_home}` as `{jvm} could not be found."); + return false; + } + if (!Directory.Exists (includePath)) { + Log.LogMessage (MessageImportance.Low, $" Skipping JAVA_HOME value of `{java_home}` as the `include` directory could not be found."); + return false; + } + + return true; + } + string GetJavaHomePathFromEnvironment () { var java_home = Environment.GetEnvironmentVariable ("JAVA_HOME"); @@ -158,24 +212,74 @@ string GetJavaHomePathFromEnvironment () return null; } + // macOS + string GetJavaHomePathFromLibexec () + { + var java_home = Path.GetFullPath ("/usr/libexec/java_home"); + if (!File.Exists (java_home)) { + return null; + } + string path = null; + Exec (java_home, "", (o, e) => { + if (string.IsNullOrEmpty (e.Data)) + return; + Log.LogMessage (MessageImportance.Low, $" {e.Data}"); + path = e.Data; + }); + return path; + } + + // Linux + string GetJavaHomePathFromAlternatives () + { + var alternatives = Path.GetFullPath ("/etc/alternatives/java"); + if (!File.Exists (alternatives)) + return null; + string targetJava = null; + Exec ("readlink", $"\"{alternatives}\"", (o, e) => { + if (string.IsNullOrEmpty (e.Data)) + return; + Log.LogMessage (MessageImportance.Low, $" {e.Data}"); + targetJava = e.Data; + }); + if (string.IsNullOrEmpty (targetJava)) + return null; + return GetJavaHomePathFromJava (targetJava); + } + string GetJavaHomePathFromMachine (Version maxVersion) { var java_homes = GetJavaHomePathsFromDirectory (JdksRoot) .Concat (GetJavaHomePathsFromJava ()) .Concat (GetJavaHomePathsFromWindowsRegistry ()) - .Distinct () .Where (d => Directory.Exists (d)) - .Select (jh => new { + .Distinct () + .ToList (); + + foreach (var p in java_homes) { + Log.LogMessage (MessageImportance.Low, $" Possible JAVA_HOME location: {p}"); + } + + var versionComparer = new ComparisonComparer((x, y) => { + int r = 0; + if (x.Version != null && y.Version != null) + r = x.Version.CompareTo (y.Version); + return r; + }); + + java_homes = java_homes.Where (d => ValidateJdkPath (maxVersion, d)) + .Select (jh => new JdkComparisonInfo { Path = jh, Version = GetVersionFromPath (jh), }) - .Where (v => maxVersion == null ? true : v.Version <= maxVersion) - .OrderByDescending (v => v.Version) + .Where (v => (maxVersion == null || v.Version == null) ? true : v.Version <= maxVersion) + .OrderByDescending (v => v, versionComparer) + .ThenByDescending (v => Directory.GetLastWriteTimeUtc (v.Path)) .Select (v => v.Path) .ToList (); foreach (var p in java_homes) { - Log.LogMessage (MessageImportance.Low, $" Possible JAVA_HOME location: {p}"); + Log.LogMessage (MessageImportance.Low, $" Filtered JAVA_HOME location: {p}"); } return java_homes.FirstOrDefault (); @@ -201,26 +305,32 @@ IEnumerable GetJavaHomePathsFromJava () .SelectMany (p => FindExecutablesInDirectory (p, "java")); foreach (var exe in javas) { - const string JavaHome = "java.home = "; - string java_home = null; - Exec (exe, "-XshowSettings:properties -version", (o, e) => { - int i = e.Data?.IndexOf (JavaHome) ?? -1; - if (i < 0) - return; - Log.LogMessage (MessageImportance.Low, $" {e.Data}"); - java_home = e.Data.Substring (JavaHome.Length + i); - // `java -XshowSettings:properties -version | 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); - } - }); + var java_home = GetJavaHomePathFromJava (exe); if (string.IsNullOrEmpty (java_home)) continue; yield return java_home; } } + string GetJavaHomePathFromJava (string java) + { + const string JavaHome = "java.home = "; + string java_home = null; + Exec (java, "-XshowSettings:properties -version", (o, e) => { + int i = e.Data?.IndexOf (JavaHome) ?? -1; + if (i < 0) + return; + Log.LogMessage (MessageImportance.Low, $" {e.Data}"); + java_home = e.Data.Substring (JavaHome.Length + i); + // `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); + } + }); + return java_home; + } + void Exec (string java, string arguments, DataReceivedEventHandler output) { Log.LogMessage (MessageImportance.Low, $" Tool {java} execution started with arguments: {arguments}"); @@ -346,5 +456,25 @@ IEnumerable FindLibrariesInDirectory (string dir, string libraryName) var library = string.Format (OS.NativeLibraryFormat, libraryName); return Directory.EnumerateFiles (dir, library, SearchOption.AllDirectories); } + + class JdkComparisonInfo { + public string Path; + public Version Version; + } + } + + class ComparisonComparer : IComparer { + + Comparison comparison; + + public ComparisonComparer (Comparison comparison) + { + this.comparison = comparison; + } + + public int Compare (T x, T y) + { + return comparison (x, y); + } } }