|
| 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 | +} |
0 commit comments