From b6c5439b88c0b4f86ea65c2365f473af3ba607e2 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 7 May 2018 16:11:55 -0400 Subject: [PATCH] [build] Don't use ls(1) to find JDK paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/xamarin/xamarin-android/issues/1493 On some machines, `make prepare` fails: build-tools/scripts/jdk.mk:130: *** missing separator. Stop. make: *** [prepare-external] Error 2 Eventually, we "found" the "cause": This make fragment: $(shell ls -dtr $(_DARWIN_JDK_FALLBACK_DIRS) | sort | tail -1) was dying a terrible horrible no good death: _DARWIN_JDK_ROOT=/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkin/sh: 49m: command not found [Turns Outâ„¢][0], **ls**(1) should be *avoided*, as its output is *unsafe* (I'm not sure *why*, and I'm not able to repro the above failure on my machine, but it's *clearly* bad). [0]: http://mywiki.wooledge.org/ParsingLs Why are we using **ls**(1)? To sort by timestamp, via `ls -dtr`. What's the recommended replacement? > If you truly need a list of all the files in a directory in order > by mtime so that you can process them in sequence, switch to perl, > and have your perl program do its own directory opening and sorting LOL? Which brings us to the solution: we don't want to use Perl -- we *want* something plausibly cross-platform -- so let's use our existing cross-platform dependency: MSBuild! Update the `` task so that in addition to probing a variety of Windows-specific registry locations, it can now do `jdk.mk`-style directory probing, allowing us to find the maximum installed JDK version which is (optionally) less than `$(JI_MAX_JDK)`. Additionally, further enhance `` so that it will *also* check `$JAVA_HOME` and execute the following command: java -XshowSettings:properties -version 2>&1 | grep java.home `$JAVA_HOME` is preferred, if specified and it fulfills the requirements of `$(JI_MAX_JDK)`. Additionally, drastically simplify `jdk.mk`. Instead of computing the desired JDK directory *on every build*, instead generate a new `bin/Build$(CONFIGURATION)/JdkInfo.mk` file which contains the JDK informatino. The `make prepare` target will generate this file. This approach simplifes `jdk.mk` -- trying to maintain it was beginning to give me a headache, so as part of this we're dropping support for 32-bit JVMs on macOS, as if anyone uses those -- and also brings the macOS/Linux build closer-in-spirit to Windows, which was already using the `` task. --- Makefile | 11 +- build-tools/scripts/PrepareWindows.targets | 8 +- build-tools/scripts/jdk.mk | 157 +--------- build-tools/scripts/jdk.targets | 13 + .../Java.Interop.BootstrapTasks.csproj | 4 +- .../Java.Interop.BootstrapTasks/JdkInfo.cs | 296 ++++++++++++++---- .../Java.Interop.BootstrapTasks/OS.cs | 22 ++ 7 files changed, 305 insertions(+), 206 deletions(-) create mode 100644 build-tools/scripts/jdk.targets create mode 100644 src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/OS.cs diff --git a/Makefile b/Makefile index 57568473c..7d6b76706 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,15 @@ xa-all: $(PACKAGES) $(XA_INTEGRATION_OUTPUTS) run-all-tests: run-tests run-test-jnimarshal run-test-generator-core run-ptests -prepare:: prepare-external +include build-tools/scripts/msbuild.mk + +prepare:: prepare-bootstrap prepare-external + +prepare-bootstrap: bin/Build$(CONFIGURATION)/Java.Interop.BootstrapTasks.dll + +bin/Build$(CONFIGURATION)/Java.Interop.BootstrapTasks.dll: src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj \ + $(wildcard src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/*.cs) + $(MSBUILD) $(MSBUILD_FLAGS) src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj prepare-external: $(PACKAGES) $(NUNIT_CONSOLE) git submodule update --init --recursive @@ -65,7 +73,6 @@ clean: include build-tools/scripts/mono.mk include build-tools/scripts/jdk.mk -include build-tools/scripts/msbuild.mk $(PACKAGES) $(NUNIT_CONSOLE): nuget restore diff --git a/build-tools/scripts/PrepareWindows.targets b/build-tools/scripts/PrepareWindows.targets index 2c68c94b6..7cbba368f 100644 --- a/build-tools/scripts/PrepareWindows.targets +++ b/build-tools/scripts/PrepareWindows.targets @@ -19,8 +19,12 @@ DestinationFiles="$(_NuGet)" /> - - + + \ No newline at end of file diff --git a/build-tools/scripts/jdk.mk b/build-tools/scripts/jdk.mk index 65d686386..79a800c53 100644 --- a/build-tools/scripts/jdk.mk +++ b/build-tools/scripts/jdk.mk @@ -26,157 +26,22 @@ # Location of the JDK `/bin` directory, which contains `java/`javac`/etc. OS ?= $(shell uname) -JI_JAVAC_PATH = javac -JI_JAR_PATH = jar -JI_JDK_BIN_PATH = $(dir $(shell which java)) +_INCLUDE_MK = bin/Build$(CONFIGURATION)/JdkInfo.mk +prepare:: $(_INCLUDE_MK) -# Filter on <= JI_MAX_JDK -ifneq ($(JI_MAX_JDK),) -_VERSION_MAX := | awk '$$1 <= $(JI_MAX_JDK)' -endif #JI_MAX_JDK - -# Sort numerically on version numbers with `sort -n`, filtering on $(JI_MAX_JDK) if needed -# Replace each line so it starts with a number (sed 's/...'\1 &/), sort on the leading number, then remove the leading number. -# Grab the last path name printed. -_VERSION_SORT := sed 's/[^0-9]*\([0-9.]*\)/\1 &/' $(_VERSION_MAX) | sort -n | sed 's/^[0-9.]* //g' | tail -1 +-include $(_INCLUDE_MK) ifeq ($(OS),Darwin) - -_MONO_BITNESS = $(shell file `which $(word 1, $(RUNTIME))` | awk 'BEGIN { val = "32-bit" } /64-bit/ { val = "64-bit" } END { print val; }') - -ifeq ($(_MONO_BITNESS),32-bit) -# The only 32-bit JVM I know of is the Apple-provided one. -JI_JVM_PATH = /System/Library/Frameworks/JavaVM.framework/JavaVM -endif # 32-bit - -# Darwin supports three possible search locations: -# -# 1. `/Library/Java/JavaVirtualMachines/jdk*` -# These are where 3rd party JDKs are installed, such as the Oracle JDK. -# This is the preferred search directory. -# -# 2. The Xcode.app MacOSX.platform SDK, which is for the ancient JDK6 that -# continues to be available -# -# 3. A "locally" hosted .pkg, in case Xcode.app isn't installed. - -_DARWIN_JDK_FALLBACK_DIRS = $(wildcard /Library/Java/JavaVirtualMachines/jdk*) -_DARWIN_JDK_JNI_INCLUDE_DIR = Contents/Home/include -_DARWIN_JDK_JNI_OS_INCLUDE_DIR = $(_DARWIN_JDK_JNI_INCLUDE_DIR)/darwin - -_XCODE_APP_JAVAVM_FRAMEWORK_PATH = \ - $(word 1, $(wildcard /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk/System/Library/Frameworks/JavaVM.framework/Headers)) - - -_LOCAL_JDK_PKG = JavaDeveloper-2013005_dp__11m4609.pkg -_LOCAL_JDK_URL = http://storage.bos.xamarin.com/android-sdk-tool/archives/$(FALLBACK_JDK_PKG) -_LOCAL_JDK_HEADERS = LocalJDK/System/Library/Frameworks/JavaVM.framework/Versions/A/Headers - -# Ancient source for (3) -_APPLE_JDK6_URL = http://adcdownload.apple.com/Developer_Tools/java_for_os_x_2013005_developer_package/java_for_os_x_2013005_dp__11m4609.dmg - -ifneq ($(_DARWIN_JDK_FALLBACK_DIRS),) -_DARWIN_JDK_ROOT := $(shell ls -dtr $(_DARWIN_JDK_FALLBACK_DIRS) | $(_VERSION_SORT)) -JI_JDK_BIN_PATH = $(_DARWIN_JDK_ROOT)/Contents/Home/bin -JI_JAVAC_PATH = $(_DARWIN_JDK_ROOT)/Contents/Home/bin/javac -JI_JAR_PATH = $(_DARWIN_JDK_ROOT)/Contents/Home/bin/jar -JI_JDK_INCLUDE_PATHS = \ - $(_DARWIN_JDK_ROOT)/$(_DARWIN_JDK_JNI_INCLUDE_DIR) \ - $(_DARWIN_JDK_ROOT)/$(_DARWIN_JDK_JNI_OS_INCLUDE_DIR) - -ifeq ($(_MONO_BITNESS),64-bit) -JI_JVM_PATH = $(shell find $(_DARWIN_JDK_ROOT)/Contents/Home -name libjli.dylib) -endif # 64-bit - -else # (1) failed; try Xcode.app's copy? -ifneq ($(_XCODE_APP_JAVAVM_FRAMEWORK_PATH),) -JI_JDK_INCLUDE_PATHS = $(_XCODE_APP_JAVAVM_FRAMEWORK_PATH) -else # (2) failed; hail mary pass! -JI_JDK_INCLUDE_PATHS = LocalJDK/System/Library/Frameworks/JavaVM.framework/Versions/A/Headers - -bin/Build$(CONFIGURATION)/JdkHeaders.props: $(JI_JDK_INCLUDE_PATHS)/jni.h - -$(JI_JDK_INCLUDE_PATHS)/jni.h: - @if [ ! -f $(_LOCAL_JDK_PKG) ]; then \ - curl -o $(_LOCAL_JDK_PKG) $(_LOCAL_JDK_URL) ; \ - fi - -mkdir LocalJDK - _jdk="$$(cd `dirname "$(_LOCAL_JDK_PKG)"`; pwd)/`basename "$(_LOCAL_JDK_PKG)"`" ; \ - (cd LocalJDK; xar -xf $$_jdk) - (cd LocalJDK; gunzip -c JavaEssentialsDev.pkg/Payload | cpio -i) -endif # (3) -endif # (1) - -endif # Darwin - +_JDKS_ROOT := /Library/Java/JavaVirtualMachines +endif # $(OS)=Darwin ifeq ($(OS),Linux) +_JDKS_ROOT := /usr/lib/jvm +endif # $(OS)=Linux -# This is for all linux distributions with which and java installed -_DEFAULT_LINUX_JAVA_ROOT = $(shell java -XshowSettings:properties -help 2>&1 | grep java.home | sed 's/^.*java.home = //g')/../ -_DEFAULT_LINUX_JAVA_INCLUDE_DIRS = $(_DEFAULT_LINUX_JAVA_ROOT)/include/ -_LINUX_JAVA_FALLBACK_DIRS = /usr/lib/jvm/java* -_LINUX_JAVA_JNI_INCLUDE_DIR = include -_LINUX_JAVA_ROOT = $(_DEFAULT_LINUX_JAVA_ROOT) -_LINUX_JAVA_ARCH_64 = amd64 -_LINUX_JAVA_ARCH_32 = i386 - -_DESKTOP_JAVA_INCLUDE_DIRS = $(_DEFAULT_LINUX_JAVA_INCLUDE_DIRS) - -ifeq ($(wildcard $(_DESKTOP_JAVA_INCLUDE_DIRS)),) -_DESKTOP_JAVA_INCLUDE_DIRS = $(wildcard $(JAVA_HOME)/include) -_LINUX_JAVA_ROOT = $(JAVA_HOME) -endif # No default Java location, $JAVA_HOME check - -ifeq ($(wildcard $(_DESKTOP_JAVA_INCLUDE_DIRS)),) -LATEST_JDK := $(shell ls -dtr $(_LINUX_JAVA_FALLBACK_DIRS) | $(_VERSION_SORT)) -_DESKTOP_JAVA_INCLUDE_DIRS = $(LATEST_JDK)/$(_LINUX_JAVA_JNI_INCLUDE_DIR) -_LINUX_JAVA_ROOT = $(LATEST_JDK) -endif # No $JAVA_HOME, find the latest version - -JI_JDK_INCLUDE_PATHS = $(_DESKTOP_JAVA_INCLUDE_DIRS) $(_DESKTOP_JAVA_INCLUDE_DIRS)/linux - -ifneq ($(wildcard $(_LINUX_JAVA_ROOT)/jre/lib/$(_LINUX_JAVA_ARCH_64)/server/libjvm.so),) -JI_JVM_PATH = $(_LINUX_JAVA_ROOT)/jre/lib/$(_LINUX_JAVA_ARCH_64)/server/libjvm.so -endif # Find 64-bit libjvm - -ifeq ($(JI_JVM_PATH),) # (1) No 64-bit java arch -ifneq ($(wildcard $(_LINUX_JAVA_ROOT)/jre/lib/$(_LINUX_JAVA_ARCH_32)/server/libjvm.so),) # (2) check 32-bit instead, even on a 64-bit system -JI_JVM_PATH = $(_LINUX_JAVA_ROOT)/jre/lib/$(_LINUX_JAVA_ARCH_32)/server/libjvm.so -endif # (2) -endif # (1) - -JI_JDK_BIN_PATH = $(_LINUX_JAVA_ROOT)/bin -JI_JAVAC_PATH = $(_LINUX_JAVA_ROOT)/bin/javac -JI_JAR_PATH = $(_LINUX_JAVA_ROOT)/bin/jar - -endif # Linux - -$(JI_JVM_PATH): - @echo "error: No JVM found\!"; - @exit 1 - -bin/Build$(CONFIGURATION)/JdkInfo.props: $(JI_JDK_INCLUDE_PATHS) $(JI_JVM_PATH) - -mkdir -p `dirname "$@"` - -rm "$@" - echo '' > "$@" - echo ' ' >> "$@" - echo " " >> "$@" - echo ' ' >> "$@" - echo " $(JI_JVM_PATH)" >> "$@" - echo ' ' >> "$@" - echo ' ' >> "$@" - for p in $(JI_JDK_INCLUDE_PATHS); do \ - echo " " >> "$@"; \ - done - echo ' ' >> "$@" - echo ' ' >> "$@" - echo ' ' >> "$@" - echo ' ' >> "$@" - echo " $(JI_JDK_BIN_PATH)" >> "$@" - echo " $(JI_JAVAC_PATH)" >> "$@" - echo " $(JI_JAR_PATH)" >> "$@" - echo ' ' >> "$@" - echo '' >> "$@" +$(_INCLUDE_MK): 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/build-tools/scripts/jdk.targets b/build-tools/scripts/jdk.targets new file mode 100644 index 000000000..df083e8aa --- /dev/null +++ b/build-tools/scripts/jdk.targets @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj index ee37a9159..83567f08d 100644 --- a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj +++ b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks.csproj @@ -33,15 +33,17 @@ + + - + \ No newline at end of file diff --git a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs index aca5a8007..e1ab52470 100644 --- a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs +++ b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs @@ -2,9 +2,12 @@ using Microsoft.Build.Utilities; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; namespace Java.Interop.BootstrapTasks { @@ -14,66 +17,243 @@ public class JdkInfo : Task const string MDREG_KEY = @"SOFTWARE\Novell\Mono for Android"; const string MDREG_JAVA_SDK = "JavaSdkDirectory"; + public string JdksRoot { get; set; } + + public string MaximumJdkVersion { get; set; } + + static Regex VersionExtractor = new Regex (@"(?[\d]+(\.\d+)+)", RegexOptions.Compiled); + + [Required] + public ITaskItem PropertyFile { get; set; } + [Required] - public ITaskItem Output { get; set; } + public ITaskItem MakeFragmentFile { get; set; } [Output] - public string JavaSdkDirectory { get; set; } + public string JavaHomePath { get; set; } public override bool Execute () { Log.LogMessage (MessageImportance.Low, $"Task {nameof (JdkInfo)}"); - Log.LogMessage (MessageImportance.Low, $" {nameof (Output)}: {Output}"); + Log.LogMessage (MessageImportance.Low, $" {nameof (JdksRoot)}: {JdksRoot}"); + Log.LogMessage (MessageImportance.Low, $" {nameof (MakeFragmentFile)}: {MakeFragmentFile}"); + Log.LogMessage (MessageImportance.Low, $" {nameof (MaximumJdkVersion)}: {MaximumJdkVersion}"); + Log.LogMessage (MessageImportance.Low, $" {nameof (PropertyFile)}: {PropertyFile}"); + + var maxVersion = GetMaxJdkVersion (); + 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; + } + } + java_home = java_home ?? GetJavaHomePathFromMachine (maxVersion); - var javaSdkPath = GetJavaSdkPath (); - if (string.IsNullOrEmpty(javaSdkPath)) { - Log.LogError ("JavaSdkPath is blank"); + if (string.IsNullOrEmpty (java_home)) { + Log.LogError ("Could not determine JAVA_HOME location. Please set JdksRoot or export the JAVA_HOME environment variable."); return false; } - Log.LogMessage (MessageImportance.Low, $" JavaSdkPath: {javaSdkPath}"); - - var jvmPath = Path.Combine (javaSdkPath, "jre", "bin", "server", "jvm.dll"); - if (!File.Exists (jvmPath)) { - Log.LogError ($"JdkJvmPath not found at {jvmPath}"); + var includes = new List () { + Path.Combine (java_home, "include"), + }; + includes.AddRange (Directory.GetDirectories (includes [0])); + + 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; } - var javaIncludePath = Path.Combine (javaSdkPath, "include"); - var includes = new List { javaIncludePath }; - includes.AddRange (Directory.GetDirectories (javaIncludePath)); //Include dirs such as "win32" - - var includeXmlTags = new StringBuilder (); - foreach (var include in includes) { - includeXmlTags.AppendLine ($""); - } - - Directory.CreateDirectory (Path.GetDirectoryName (Output.ItemSpec)); - File.WriteAllText (Output.ItemSpec, $@" - - - - {jvmPath} - - - {includeXmlTags} - - - - - {Path.Combine (javaSdkPath, "bin", "javac.exe")} - {Path.Combine (javaSdkPath, "bin", "jar.exe")} - -"); - - JavaSdkDirectory = javaSdkPath; - Log.LogMessage (MessageImportance.Low, $" [Output] {nameof (JavaSdkDirectory)}: {JavaSdkDirectory}"); + JavaHomePath = java_home; + + Directory.CreateDirectory (Path.GetDirectoryName (PropertyFile.ItemSpec)); + Directory.CreateDirectory (Path.GetDirectoryName (MakeFragmentFile.ItemSpec)); + + WritePropertyFile (jarPath, javacPath, jdkJvmPath, includes); + WriteMakeFragmentFile (jarPath, javacPath, jdkJvmPath, includes); + + Log.LogMessage (MessageImportance.Low, $" [Output] {nameof (JavaHomePath)}: {JavaHomePath}"); return !Log.HasLoggedErrors; } - string GetJavaSdkPath () + Version GetMaxJdkVersion () + { + if (string.IsNullOrEmpty (MaximumJdkVersion)) + return null; + if (!MaximumJdkVersion.Contains (".")) { + MaximumJdkVersion += ".0"; + } + return new Version (MaximumJdkVersion); + } + + 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; + } + + void FileExists (string path) + { + if (!File.Exists (path)) { + var name = Path.GetFileName (path); + Log.LogError ($"Could not determine location of `{name}`; tried `{path}`."); + } + } + + void WritePropertyFile (string jarPath, string javacPath, string jdkJvmPath, IEnumerable includes) + { + var msbuild = XNamespace.Get ("http://schemas.microsoft.com/developer/msbuild/2003"); + var project = new XElement (msbuild + "Project", + new XElement (msbuild + "Choose", + new XElement (msbuild + "When", new XAttribute ("Condition", " '$(JdkJvmPath)' == '' "), + new XElement (msbuild + "PropertyGroup", + new XElement (msbuild + "JdkJvmPath", jdkJvmPath)), + new XElement (msbuild + "ItemGroup", + includes.Select (i => new XElement (msbuild + "JdkIncludePath", new XAttribute ("Include", i)))))), + new XElement (msbuild + "PropertyGroup", + new XElement (msbuild + "JavaCPath", new XAttribute ("Condition", " '$(JavaCPath)' == '' "), + javacPath), + new XElement (msbuild + "JarPath", new XAttribute ("Condition", " '$(JarPath)' == '' "), + jarPath))); + project.Save (PropertyFile.ItemSpec); + } + + void WriteMakeFragmentFile (string jarPath, string javacPath, string jdkJvmPath, IEnumerable includes) + { + using (var o = new StreamWriter (MakeFragmentFile.ItemSpec)) { + o.WriteLine ($"JI_JAR_PATH := {jarPath}"); + o.WriteLine ($"JI_JAVAC_PATH := {javacPath}"); + o.WriteLine ($"JI_JDK_INCLUDE_PATHS := {string.Join (" ", includes)}"); + o.WriteLine ($"JI_JVM_PATH := {jdkJvmPath}"); + } + } + + string GetJavaHomePathFromEnvironment () + { + var java_home = Environment.GetEnvironmentVariable ("JAVA_HOME"); + if (!string.IsNullOrEmpty (java_home)) + return java_home; + return null; + } + + string GetJavaHomePathFromMachine (Version maxVersion) + { + var java_homes = GetJavaHomePathsFromDirectory (JdksRoot) + .Concat (GetJavaHomePathsFromJava ()) + .Concat (GetJavaHomePathsFromWindowsRegistry ()) + .Distinct () + .Where (d => Directory.Exists (d)) + .Select (jh => new { + Path = jh, + Version = GetVersionFromPath (jh), + }) + .Where (v => maxVersion == null ? true : v.Version <= maxVersion) + .OrderByDescending (v => v.Version) + .Select (v => v.Path) + .ToList (); + + foreach (var p in java_homes) { + Log.LogMessage (MessageImportance.Low, $" Possible JAVA_HOME location: {p}"); + } + + return java_homes.FirstOrDefault (); + } + + IEnumerable GetJavaHomePathsFromDirectory (string jdksRoot) + { + if (string.IsNullOrEmpty (jdksRoot)) + yield break; + if (!Directory.Exists (jdksRoot)) + yield break; + foreach (var d in Directory.EnumerateDirectories (jdksRoot)) { + var h = d; + if (OS.IsMacOS) + h = Path.Combine (h, "Contents", "Home"); + yield return h; + } + } + + IEnumerable GetJavaHomePathsFromJava () + { + var javas = Environment.GetEnvironmentVariable ("PATH").Split (Path.PathSeparator) + .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); + } + }); + if (string.IsNullOrEmpty (java_home)) + continue; + yield return java_home; + } + } + + void Exec (string java, string arguments, DataReceivedEventHandler output) + { + Log.LogMessage (MessageImportance.Low, $" Tool {java} execution started with arguments: {arguments}"); + var psi = new ProcessStartInfo () { + FileName = java, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardInput = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + var p = new Process () { + StartInfo = psi, + }; + p.OutputDataReceived += output; + p.ErrorDataReceived += output; + + using (p) { + p.StartInfo = psi; + p.Start (); + p.BeginOutputReadLine (); + p.BeginErrorReadLine (); + p.WaitForExit (); + } + } + + IEnumerable GetJavaHomePathsFromWindowsRegistry () + { + if (Path.DirectorySeparatorChar == '/') + yield break; + // check the user specified path var roots = new [] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; const RegistryEx.Wow64 wow = RegistryEx.Wow64.Key32; @@ -81,7 +261,7 @@ string GetJavaSdkPath () foreach (var root in roots) { if (CheckRegistryKeyForExecutable (root, regKey, MDREG_JAVA_SDK, wow, "bin", JARSIGNER)) - return RegistryEx.GetValueString (root, regKey, MDREG_JAVA_SDK, wow); + yield return RegistryEx.GetValueString (root, regKey, MDREG_JAVA_SDK, wow); } string subkey = @"SOFTWARE\JavaSoft\Java Development Kit"; @@ -97,20 +277,17 @@ string GetJavaSdkPath () // 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); + 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); + 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); + yield return RegistryEx.GetValueString (RegistryEx.LocalMachine, subkey + "\\" + "1.6", "JavaHome", wow64); } Log.LogMessage (MessageImportance.Low, $" Key {key_name} not found."); } - - // We ran out of things to check.. - return null; } string GetMDRegistryKey () @@ -131,7 +308,7 @@ private bool CheckRegistryKeyForExecutable (UIntPtr key, string subkey, string v return false; } - if (!FindExecutableInDirectory (exe, Path.Combine (path, subdir)).Any ()) { + if (!FindExecutablesInDirectory (Path.Combine (path, subdir), exe).Any ()) { Log.LogMessage (MessageImportance.Low, $" Key {key_name} found:\n Path does not contain {exe} in \\{subdir} ({path})."); return false; } @@ -141,24 +318,33 @@ private bool CheckRegistryKeyForExecutable (UIntPtr key, string subkey, string v return true; } - IEnumerable FindExecutableInDirectory (string executable, string dir) + IEnumerable FindExecutablesInDirectory (string dir, string executable) { - foreach (var exe in Executables (executable)) - if (File.Exists (Path.Combine (dir, exe))) - yield return dir; + foreach (var exe in Executables (executable)) { + var p = Path.Combine (dir, exe); + if (File.Exists (p)) + yield return p; + } } 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) + if (pathExts == null) { + yield return executable; yield break; + } foreach (var ext in pathExts) yield return Path.ChangeExtension (executable, ext); } + + IEnumerable FindLibrariesInDirectory (string dir, string libraryName) + { + var library = string.Format (OS.NativeLibraryFormat, libraryName); + return Directory.EnumerateFiles (dir, library, SearchOption.AllDirectories); + } } } diff --git a/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/OS.cs b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/OS.cs new file mode 100644 index 000000000..585c20ec5 --- /dev/null +++ b/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/OS.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; + +namespace Java.Interop.BootstrapTasks { + class OS { + public static readonly bool IsWindows = Path.DirectorySeparatorChar == '\\'; + public static readonly bool IsMacOS = !IsWindows && Directory.Exists ("/Applications"); + public static readonly bool IsLinux = !IsWindows && !IsMacOS; + + public static readonly string NativeLibraryFormat; + + static OS () + { + if (IsWindows) + NativeLibraryFormat = "{0}.dll"; + if (IsMacOS) + NativeLibraryFormat = "lib{0}.dylib"; + if (IsLinux) + NativeLibraryFormat = "lib{0}.so"; + } + } +}