Skip to content

Commit 81c42b6

Browse files
committed
[Xamarin.Android.Tools.AndroidSdk] Add JdkInfo
Context: #29 (comment) Refactor out the [underlying functionality][0] of the [`<JdkInfo/>` task][1] so that it is more easily usable. [0]: dotnet/java-interop@4bd9297 [1]: https://github.com/xamarin/java.interop/blob/master/src/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/JdkInfo.cs
1 parent 1b511f9 commit 81c42b6

File tree

9 files changed

+669
-82
lines changed

9 files changed

+669
-82
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Text.RegularExpressions;
9+
using System.Threading;
10+
using System.Xml;
11+
using System.Xml.Linq;
12+
13+
namespace Xamarin.Android.Tools
14+
{
15+
public class JdkInfo {
16+
17+
public string HomePath {get;}
18+
19+
public string JarPath {get;}
20+
public string JavaPath {get;}
21+
public string JavacPath {get;}
22+
public string JdkJvmPath {get;}
23+
public ReadOnlyCollection<string> IncludePath {get;}
24+
25+
public Version Version => javaVersion.Value;
26+
public string Vendor {
27+
get {
28+
if (GetJavaSettingsPropertyValue ("java.vendor", out string vendor))
29+
return vendor;
30+
return null;
31+
}
32+
}
33+
34+
public ReadOnlyDictionary<string, string> ReleaseProperties {get;}
35+
public IEnumerable<string> JavaSettingsPropertyKeys => javaProperties.Value.Keys;
36+
37+
Lazy<Dictionary<string, List<string>>> javaProperties;
38+
Lazy<Version> javaVersion;
39+
40+
public JdkInfo (string homePath)
41+
{
42+
if (homePath == null)
43+
throw new ArgumentNullException (nameof (homePath));
44+
if (!Directory.Exists (homePath))
45+
throw new ArgumentException ("Not a directory", nameof (homePath));
46+
47+
HomePath = homePath;
48+
49+
var binPath = Path.Combine (HomePath, "bin");
50+
JarPath = ProcessUtils.FindExecutablesInDirectory (binPath, "jar").FirstOrDefault ();
51+
JavaPath = ProcessUtils.FindExecutablesInDirectory (binPath, "java").FirstOrDefault ();
52+
JavacPath = ProcessUtils.FindExecutablesInDirectory (binPath, "javac").FirstOrDefault ();
53+
JdkJvmPath = OS.IsMac
54+
? FindLibrariesInDirectory (HomePath, "jli").FirstOrDefault ()
55+
: FindLibrariesInDirectory (Path.Combine (HomePath, "jre"), "jvm").FirstOrDefault ();
56+
57+
ValidateFile ("jar", JarPath);
58+
ValidateFile ("java", JavaPath);
59+
ValidateFile ("javac", JavacPath);
60+
ValidateFile ("jvm", JdkJvmPath);
61+
62+
var includes = new List<string> ();
63+
var jdkInclude = Path.Combine (HomePath, "include");
64+
65+
if (Directory.Exists (jdkInclude)) {
66+
includes.Add (jdkInclude);
67+
includes.AddRange (Directory.GetDirectories (jdkInclude));
68+
}
69+
70+
71+
ReleaseProperties = GetReleaseProperties();
72+
73+
IncludePath = new ReadOnlyCollection<string> (includes);
74+
75+
javaProperties = new Lazy<Dictionary<string, List<string>>> (GetJavaProperties, LazyThreadSafetyMode.ExecutionAndPublication);
76+
javaVersion = new Lazy<Version> (GetJavaVersion, LazyThreadSafetyMode.ExecutionAndPublication);
77+
}
78+
79+
public override string ToString()
80+
{
81+
return $"JdkInfo(Version={Version}, Vendor=\"{Vendor}\", HomePath=\"{HomePath}\")";
82+
}
83+
84+
public bool GetJavaSettingsPropertyValues (string key, out IEnumerable<string> value)
85+
{
86+
value = null;
87+
var props = javaProperties.Value;
88+
if (props.TryGetValue (key, out var v)) {
89+
value = v;
90+
return true;
91+
}
92+
return false;
93+
}
94+
95+
public bool GetJavaSettingsPropertyValue (string key, out string value)
96+
{
97+
value = null;
98+
var props = javaProperties.Value;
99+
if (props.TryGetValue (key, out var v)) {
100+
if (v.Count > 1)
101+
throw new InvalidOperationException ($"Requested to get one string value when property `{key}` contains `{v.Count}` values.");
102+
value = v [0];
103+
return true;
104+
}
105+
return false;
106+
}
107+
108+
static IEnumerable<string> FindLibrariesInDirectory (string dir, string libraryName)
109+
{
110+
var library = string.Format (OS.NativeLibraryFormat, libraryName);
111+
return Directory.EnumerateFiles (dir, library, SearchOption.AllDirectories);
112+
}
113+
114+
void ValidateFile (string name, string path)
115+
{
116+
if (path == null || !File.Exists (path))
117+
throw new ArgumentException ($"Could not find required file `{name}` within `{HomePath}`; is this a valid JDK?", "homePath");
118+
}
119+
120+
static Regex VersionExtractor = new Regex (@"(?<version>[\d]+(\.\d+)+)(_(?<patch>\d+))?", RegexOptions.Compiled);
121+
122+
Version GetJavaVersion ()
123+
{
124+
string version = null;
125+
if (!ReleaseProperties.TryGetValue ("JAVA_VERSION", out version)) {
126+
if (GetJavaSettingsPropertyValue ("java.version", out string vs))
127+
version = vs;
128+
}
129+
if (version == null)
130+
throw new NotSupportedException ("Could not determine Java version");
131+
var m = VersionExtractor.Match (version);
132+
if (!m.Success)
133+
return null;
134+
version = m.Groups ["version"].Value;
135+
var patch = m.Groups ["patch"].Value;
136+
if (!string.IsNullOrEmpty (patch))
137+
version += "." + patch;
138+
if (!version.Contains ("."))
139+
version += ".0";
140+
if (Version.TryParse (version, out Version v))
141+
return v;
142+
return null;
143+
}
144+
145+
ReadOnlyDictionary<string, string> GetReleaseProperties ()
146+
{
147+
var releasePath = Path.Combine (HomePath, "release");
148+
if (!File.Exists (releasePath))
149+
return new ReadOnlyDictionary<string, string> (new Dictionary<string, string> ());
150+
151+
var props = new Dictionary<string, string> ();
152+
using (var release = File.OpenText (releasePath)) {
153+
string line;
154+
while ((line = release.ReadLine ()) != null) {
155+
const string PropertyDelim = "=\"";
156+
int delim = line.IndexOf (PropertyDelim, StringComparison.Ordinal);
157+
if (delim < 0) {
158+
props [line] = "";
159+
}
160+
string key = line.Substring (0, delim);
161+
string value = line.Substring (delim + PropertyDelim.Length, line.Length - delim - PropertyDelim.Length - 1);
162+
props [key] = value;
163+
}
164+
}
165+
return new ReadOnlyDictionary<string, string>(props);
166+
}
167+
168+
Dictionary<string, List<string>> GetJavaProperties ()
169+
{
170+
return GetJavaProperties (ProcessUtils.FindExecutablesInDirectory (Path.Combine (HomePath, "bin"), "java").First ());
171+
}
172+
173+
static Dictionary<string, List<string>> GetJavaProperties (string java)
174+
{
175+
var javaProps = new ProcessStartInfo {
176+
FileName = java,
177+
Arguments = "-XshowSettings:properties -version",
178+
};
179+
180+
var props = new Dictionary<string, List<string>> ();
181+
string curKey = null;
182+
ProcessUtils.Exec (javaProps, (o, e) => {
183+
const string ContinuedValuePrefix = " ";
184+
const string NewValuePrefix = " ";
185+
const string NameValueDelim = " = ";
186+
if (string.IsNullOrEmpty (e.Data))
187+
return;
188+
if (e.Data.StartsWith (ContinuedValuePrefix, StringComparison.Ordinal)) {
189+
if (curKey == null)
190+
throw new InvalidOperationException ($"Unknown property key for value {e.Data}!");
191+
props [curKey].Add (e.Data.Substring (ContinuedValuePrefix.Length));
192+
return;
193+
}
194+
if (e.Data.StartsWith (NewValuePrefix, StringComparison.Ordinal)) {
195+
var delim = e.Data.IndexOf (NameValueDelim, StringComparison.Ordinal);
196+
if (delim <= 0)
197+
return;
198+
curKey = e.Data.Substring (NewValuePrefix.Length, delim - NewValuePrefix.Length);
199+
var value = e.Data.Substring (delim + NameValueDelim.Length);
200+
List<string> values;
201+
if (!props.TryGetValue (curKey, out values))
202+
props.Add (curKey, values = new List<string> ());
203+
values.Add (value);
204+
}
205+
});
206+
207+
return props;
208+
}
209+
210+
public static IEnumerable<JdkInfo> GetKnownSystemJdkInfos (Action<TraceLevel, string> logger)
211+
{
212+
return GetWindowsJdks (logger)
213+
.Concat (GetMacOSMicrosoftJdks (logger))
214+
.Concat (GetJavaHomeEnvironmentJdks (logger))
215+
.Concat (GetLibexecJdks (logger))
216+
.Concat (GetJavaAlternativesJdks (logger))
217+
.Concat (GetPathEnvironmentJdks (logger));
218+
}
219+
220+
static IEnumerable<JdkInfo> GetMacOSMicrosoftJdks (Action<TraceLevel, string> logger)
221+
{
222+
return GetMacOSMicrosoftJdkPaths ()
223+
.Select (p => TryGetJdkInfo (p, logger))
224+
.Where (jdk => jdk != null)
225+
.OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default);
226+
}
227+
228+
static IEnumerable<string> GetMacOSMicrosoftJdkPaths ()
229+
{
230+
var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
231+
var jdks = Path.Combine (home, "Library", "Developer", "Xamarin", "jdk");
232+
if (!Directory.Exists (jdks))
233+
return Enumerable.Empty <string> ();
234+
235+
return Directory.EnumerateDirectories (jdks);
236+
}
237+
238+
static JdkInfo TryGetJdkInfo (string path, Action<TraceLevel, string> logger)
239+
{
240+
JdkInfo jdk = null;
241+
try {
242+
jdk = new JdkInfo (path);
243+
}
244+
catch (Exception e) {
245+
logger (TraceLevel.Warning, e.ToString ());
246+
}
247+
return jdk;
248+
}
249+
250+
static IEnumerable<JdkInfo> GetWindowsJdks (Action<TraceLevel, string> logger)
251+
{
252+
if (!OS.IsWindows)
253+
return Enumerable.Empty<JdkInfo> ();
254+
return AndroidSdkWindows.GetJdkInfos (logger);
255+
}
256+
257+
static IEnumerable<JdkInfo> GetJavaHomeEnvironmentJdks (Action<TraceLevel, string> logger)
258+
{
259+
var java_home = Environment.GetEnvironmentVariable ("JAVA_HOME");
260+
if (string.IsNullOrEmpty (java_home))
261+
yield break;
262+
var jdk = TryGetJdkInfo (java_home, logger);
263+
if (jdk != null)
264+
yield return jdk;
265+
}
266+
267+
// macOS
268+
static IEnumerable<JdkInfo> GetLibexecJdks (Action<TraceLevel, string> logger)
269+
{
270+
return GetLibexecJdkPaths (logger)
271+
.Distinct ()
272+
.Select (p => TryGetJdkInfo (p, logger))
273+
.Where (jdk => jdk != null)
274+
.OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default);
275+
}
276+
277+
static IEnumerable<string> GetLibexecJdkPaths (Action<TraceLevel, string> logger)
278+
{
279+
var java_home = Path.GetFullPath ("/usr/libexec/java_home");
280+
if (!File.Exists (java_home)) {
281+
yield break;
282+
}
283+
var jhp = new ProcessStartInfo {
284+
FileName = java_home,
285+
Arguments = "-X",
286+
};
287+
var xml = new StringBuilder ();
288+
ProcessUtils.Exec (jhp, (o, e) => {
289+
if (string.IsNullOrEmpty (e.Data))
290+
return;
291+
xml.Append (e.Data);
292+
});
293+
var plist = XElement.Parse (xml.ToString ());
294+
foreach (var info in plist.Elements ("array").Elements ("dict")) {
295+
var JVMHomePath = (XNode) info.Elements ("key").FirstOrDefault (e => e.Value == "JVMHomePath");
296+
if (JVMHomePath == null)
297+
continue;
298+
while (JVMHomePath.NextNode.NodeType !=XmlNodeType.Element)
299+
JVMHomePath = JVMHomePath.NextNode;
300+
var strElement = (XElement) JVMHomePath.NextNode;
301+
var path = strElement.Value;
302+
yield return path;
303+
}
304+
}
305+
306+
// Linux
307+
static IEnumerable<JdkInfo> GetJavaAlternativesJdks (Action<TraceLevel, string> logger)
308+
{
309+
return GetJavaAlternativesJdkPaths ()
310+
.Distinct ()
311+
.Select (p => TryGetJdkInfo (p, logger))
312+
.Where (jdk => jdk != null)
313+
.OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default);
314+
}
315+
316+
static IEnumerable<string> GetJavaAlternativesJdkPaths ()
317+
{
318+
var alternatives = Path.GetFullPath ("/usr/bin/update-java-alternatives");
319+
if (!File.Exists (alternatives))
320+
return Enumerable.Empty<string> ();
321+
322+
var psi = new ProcessStartInfo {
323+
FileName = alternatives,
324+
Arguments = "-l",
325+
};
326+
var paths = new List<string> ();
327+
ProcessUtils.Exec (psi, (o, e) => {
328+
if (string.IsNullOrWhiteSpace (e.Data))
329+
return;
330+
// Example line:
331+
// java-1.8.0-openjdk-amd64 1081 /usr/lib/jvm/java-1.8.0-openjdk-amd64
332+
var columns = e.Data.Split (new[]{ ' ' }, StringSplitOptions.RemoveEmptyEntries);
333+
if (columns.Length <= 2)
334+
return;
335+
paths.Add (columns [2]);
336+
});
337+
return paths;
338+
}
339+
340+
// Last-ditch fallback!
341+
static IEnumerable<JdkInfo> GetPathEnvironmentJdks (Action<TraceLevel, string> logger)
342+
{
343+
return GetPathEnvironmentJdkPaths ()
344+
.Select (p => TryGetJdkInfo (p, logger))
345+
.Where (jdk => jdk != null);
346+
}
347+
348+
static IEnumerable<string> GetPathEnvironmentJdkPaths ()
349+
{
350+
foreach (var java in ProcessUtils.FindExecutablesInPath ("java")) {
351+
var props = GetJavaProperties (java);
352+
if (props.TryGetValue ("java.home", out var java_homes)) {
353+
var java_home = java_homes [0];
354+
// `java -XshowSettings:properties -version 2>&1 | grep java.home` ends with `/jre` on macOS.
355+
// We need the parent dir so we can properly lookup the `include` directories
356+
if (java_home.EndsWith ("jre", StringComparison.OrdinalIgnoreCase)) {
357+
java_home = Path.GetDirectoryName (java_home);
358+
}
359+
yield return java_home;
360+
}
361+
}
362+
}
363+
}
364+
365+
class JdkInfoVersionComparer : IComparer<JdkInfo>
366+
{
367+
public static readonly IComparer<JdkInfo> Default = new JdkInfoVersionComparer ();
368+
369+
public int Compare (JdkInfo x, JdkInfo y)
370+
{
371+
if (x.Version != null && y.Version != null)
372+
return x.Version.CompareTo (y.Version);
373+
return 0;
374+
}
375+
}
376+
}

src/Xamarin.Android.Tools.AndroidSdk/OS.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public class OS
1212

1313
internal readonly static string ProgramFilesX86;
1414

15+
internal readonly static string NativeLibraryFormat;
16+
1517
static OS ()
1618
{
1719
IsWindows = Path.DirectorySeparatorChar == '\\';
@@ -20,6 +22,13 @@ static OS ()
2022
if (IsWindows) {
2123
ProgramFilesX86 = GetProgramFilesX86 ();
2224
}
25+
26+
if (IsWindows)
27+
NativeLibraryFormat = "{0}.dll";
28+
if (IsMac)
29+
NativeLibraryFormat = "lib{0}.dylib";
30+
if (!IsWindows && !IsMac)
31+
NativeLibraryFormat = "lib{0}.so";
2332
}
2433

2534
//From Managed.Windows.Forms/XplatUI

0 commit comments

Comments
 (0)