From 21ea061c7be68316213de9fccd8b20e094f826bf Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 18 Mar 2021 22:02:35 -0400 Subject: [PATCH] [Xamarin.Android.Tools.AndroidSdk] Probe for Microsoft OpenJDK dirs Context: https://aka.ms/getopenjdk Context: https://devblogs.microsoft.com/java Remove support for the obsolete (and never updated) `microsoft_dist_openjdk_*` builds of OpenJDK. Despite having "microsoft" in the name, it was maintained by a different team which has moved on to other things. --- .../AndroidSdkInfo.cs | 2 +- .../JdkInfo.cs | 44 +++++++--- src/Xamarin.Android.Tools.AndroidSdk/OS.cs | 84 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 10 +++ .../Sdks/AndroidSdkWindows.cs | 85 +++++++++++++++---- .../AndroidSdkWindowsTests.cs | 68 +++++++++++++++ ...arin.Android.Tools.AndroidSdk-Tests.csproj | 2 + 7 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs create mode 100644 tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs index 1297e51..7f6cd0c 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs @@ -185,7 +185,7 @@ public static void DetectAndSetPreferredJavaSdkPathToLatest (Action GetKnownSystemJdkInfos (Action GetConfiguredJdkPaths (Action log } } - internal static IEnumerable GetMacOSMicrosoftJdks (Action logger) + internal static IEnumerable GetMicrosoftOpenJdks (Action logger) { - return GetMacOSMicrosoftJdkPaths () - .Select (p => TryGetJdkInfo (p, logger, "$HOME/Library/Developer/Xamarin/jdk")) + foreach (var dir in GetMacOSMicrosoftOpenJdks (logger)) + yield return dir; + if (Path.DirectorySeparatorChar != '\\') + yield break; + foreach (var dir in AndroidSdkWindows.GetJdkInfos (logger)) { + yield return dir; + } + } + + static IEnumerable GetMacOSMicrosoftOpenJdks (Action logger) + { + return GetMacOSMicrosoftOpenJdkPaths () + .Select (p => TryGetJdkInfo (p, logger, "/Library/Java/JavaVirtualMachines/microsoft-*.jdk")) .Where (jdk => jdk != null) .Select (jdk => jdk!) .OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default); } - static IEnumerable GetMacOSMicrosoftJdkPaths () + static IEnumerable GetMacOSMicrosoftOpenJdkPaths () { + var root = "/Library/Java/JavaVirtualMachines"; + var pattern = "microsoft-*.jdk"; + var toHome = Path.Combine ("Contents", "Home"); var jdks = AppDomain.CurrentDomain.GetData ($"GetMacOSMicrosoftJdkPaths jdks override! {typeof (JdkInfo).AssemblyQualifiedName}") ?.ToString (); - if (jdks == null) { - var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal); - jdks = Path.Combine (home, "Library", "Developer", "Xamarin", "jdk"); + if (jdks != null) { + root = jdks; + toHome = ""; + pattern = "*"; + } + if (!Directory.Exists (root)) { + yield break; + } + foreach (var dir in Directory.EnumerateDirectories (root, pattern)) { + var home = Path.Combine (dir, toHome); + if (!Directory.Exists (home)) + continue; + yield return home; } - if (!Directory.Exists (jdks)) - return Enumerable.Empty (); - - return Directory.EnumerateDirectories (jdks); } static JdkInfo? TryGetJdkInfo (string path, Action logger, string locator) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/OS.cs b/src/Xamarin.Android.Tools.AndroidSdk/OS.cs index ffb395f..338b0fd 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/OS.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/OS.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.IO; using System.Text; @@ -139,9 +140,89 @@ static extern int RegSetValueExW (UIntPtr hKey, string lpValueName, int lpReserv static extern int RegCreateKeyEx (UIntPtr hKey, string subKey, uint reserved, string? @class, uint options, uint samDesired, IntPtr lpSecurityAttributes, out UIntPtr phkResult, out Disposition lpdwDisposition); + // https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regenumkeyexw + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegEnumKeyExW ( + UIntPtr hKey, + uint dwIndex, + [Out] char[] lpName, + ref uint lpcchName, + IntPtr lpReserved, + IntPtr lpClass, + IntPtr lpcchClass, + IntPtr lpftLastWriteTime + ); + + // https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryinfokeyw + [DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)] + static extern int RegQueryInfoKey ( + UIntPtr hKey, + IntPtr lpClass, + IntPtr lpcchClass, + IntPtr lpReserved, + out uint lpcSubkey, + out uint lpcchMaxSubkeyLen, + IntPtr lpcchMaxClassLen, + IntPtr lpcValues, + IntPtr lpcchMaxValueNameLen, + IntPtr lpcbMaxValueLen, + IntPtr lpSecurityDescriptor, + IntPtr lpftLastWriteTime + ); + [DllImport ("advapi32.dll", SetLastError = true)] static extern int RegCloseKey (UIntPtr hKey); + public static IEnumerable EnumerateSubkeys (UIntPtr key, string subkey, Wow64 wow64) + { + UIntPtr regKeyHandle; + uint sam = (uint)Rights.Read + (uint)wow64; + int r = RegOpenKeyEx (key, subkey, 0, sam, out regKeyHandle); + if (r != 0) { + yield break; + } + try { + r = RegQueryInfoKey ( + hKey: regKeyHandle, + lpClass: IntPtr.Zero, + lpcchClass: IntPtr.Zero, + lpReserved: IntPtr.Zero, + lpcSubkey: out uint cSubkeys, + lpcchMaxSubkeyLen: out uint cchMaxSubkeyLen, + lpcchMaxClassLen: IntPtr.Zero, + lpcValues: IntPtr.Zero, + lpcchMaxValueNameLen: IntPtr.Zero, + lpcbMaxValueLen: IntPtr.Zero, + lpSecurityDescriptor: IntPtr.Zero, + lpftLastWriteTime: IntPtr.Zero + ); + if (r != 0) { + yield break; + } + var name = new char [cchMaxSubkeyLen+1]; + for (uint i = 0; i < cSubkeys; ++i) { + var nameLen = (uint) name.Length; + r = RegEnumKeyExW ( + hKey: regKeyHandle, + dwIndex: i, + lpName: name, + lpcchName: ref nameLen, + lpReserved: IntPtr.Zero, + lpClass: IntPtr.Zero, + lpcchClass: IntPtr.Zero, + lpftLastWriteTime: IntPtr.Zero + ); + if (r != 0) { + continue; + } + yield return new string (name, 0, (int) nameLen); + } + } + finally { + RegCloseKey (regKeyHandle); + } + } + public static string? GetValueString (UIntPtr key, string subkey, string valueName, Wow64 wow64) { UIntPtr regKeyHandle; @@ -192,6 +273,9 @@ enum Rights : uint SetValue = 0x0002, CreateSubKey = 0x0004, EnumerateSubKey = 0x0008, + Notify = 0x0010, + Read = _StandardRead | QueryValue | EnumerateSubKey | Notify, + _StandardRead = 0x20000, } enum Options diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..05a6dfb --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo ( + "Xamarin.Android.Tools.AndroidSdk-Tests, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" + + "6cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2" + + "814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" + + "d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" + + "2c9733db" +)] diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs index 7391999..81c909c 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs @@ -18,6 +18,7 @@ class AndroidSdkWindows : AndroidSdkBase const string ANDROID_INSTALLER_KEY = "Path"; const string XAMARIN_ANDROID_INSTALLER_PATH = @"SOFTWARE\Xamarin\MonoAndroid"; const string XAMARIN_ANDROID_INSTALLER_KEY = "PrivateAndroidSdkPath"; + const string MICROSOFT_OPENJDK_PATH = @"SOFTWARE\Microsoft\JDK"; public AndroidSdkWindows (Action logger) : base (logger) @@ -131,8 +132,9 @@ IEnumerable ToJdkInfos (IEnumerable paths, string locator) } return ToJdkInfos (GetPreferredJdkPaths (), "Preferred Registry") - .Concat (ToJdkInfos (GetOpenJdkPaths (), "OpenJDK")) - .Concat (ToJdkInfos (GetKnownOpenJdkPaths (), "Well-known OpenJDK paths")) + .Concat (ToJdkInfos (GetMicrosoftOpenJdkFilesystemPaths (), "Microsoft OpenJDK Filesystem")) + .Concat (ToJdkInfos (GetMicrosoftOpenJdkRegistryPaths (), "Microsoft OpenJDK Registry")) + .Concat (ToJdkInfos (GetVSAndroidJdkPaths (), @"HKLM\SOFTWARE\Microsoft\VisualStudio\Android@JavaHome")) .Concat (ToJdkInfos (GetOracleJdkPaths (), "Oracle JDK")) ; } @@ -150,7 +152,7 @@ private static IEnumerable GetPreferredJdkPaths () } } - private static IEnumerable GetOpenJdkPaths () + private static IEnumerable GetVSAndroidJdkPaths () { var root = RegistryEx.LocalMachine; var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }; @@ -163,28 +165,24 @@ private static IEnumerable GetOpenJdkPaths () } } - /// - /// Locate OpenJDK installations by well known path. - /// - /// List of valid OpenJDK paths in version descending order. - private static IEnumerable GetKnownOpenJdkPaths () + static IEnumerable GetMicrosoftOpenJdkFilesystemPaths () { - string JdkFolderNamePattern = "microsoft_dist_openjdk_"; + const string JdkFolderNamePrefix = "jdk-"; var paths = new List> (); var rootPaths = new List { - Path.Combine (Environment.ExpandEnvironmentVariables ("%ProgramW6432%"), "Android", "jdk"), + Path.Combine (Environment.ExpandEnvironmentVariables ("%ProgramW6432%"), "Microsoft"), Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), "Android", "jdk"), }; foreach (var rootPath in rootPaths) { - if (Directory.Exists (rootPath)) { - foreach (var directoryName in Directory.EnumerateDirectories (rootPath, $"{JdkFolderNamePattern}*").ToList ()) { - var versionString = directoryName.Replace ($"{rootPath}\\{JdkFolderNamePattern}", string.Empty); - if (Version.TryParse (versionString, out Version? ver)) { - paths.Add (new Tuple(directoryName, ver)); - } - } + if (!Directory.Exists (rootPath)) + continue; + foreach (var directoryName in Directory.EnumerateDirectories (rootPath, $"{JdkFolderNamePrefix}*")) { + var version = ExtractVersion (directoryName, JdkFolderNamePrefix); + if (version == null) + continue; + paths.Add (Tuple.Create (directoryName, version)); } } @@ -193,6 +191,59 @@ private static IEnumerable GetKnownOpenJdkPaths () .Select (openJdk => openJdk.Item1); } + static IEnumerable GetMicrosoftOpenJdkRegistryPaths () + { + var paths = new List<(Version version, string path)> (); + var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine }; + var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 }; + foreach (var root in roots) + foreach (var wow in wows) { + foreach (var subkeyName in RegistryEx.EnumerateSubkeys (root, MICROSOFT_OPENJDK_PATH, wow)) { + if (!Version.TryParse (subkeyName, out var version)) + continue; + var msiKey = $@"{MICROSOFT_OPENJDK_PATH}\{subkeyName}\hotspot\MSI"; + var path = RegistryEx.GetValueString (root, msiKey, "Path", wow); + if (path == null) + continue; + paths.Add ((version, path)); + } + } + + return paths.OrderByDescending (e => e.version) + .Select (e => e.path); + } + + internal static Version? ExtractVersion (string path, string prefix) + { + var name = Path.GetFileName (path); + if (name.Length <= prefix.Length) + return null; + if (!name.StartsWith (prefix, StringComparison.OrdinalIgnoreCase)) + return null; + + var start = prefix.Length; + while (start < name.Length && !char.IsDigit (name, start)) { + ++start; + } + if (start == name.Length) + return null; + + name = name.Substring (start); + int end = 0; + while (end < name.Length && + (char.IsDigit (name [end]) || name [end] == '.')) { + end++; + } + + do { + if (Version.TryParse (name.Substring (0, end), out var v)) + return v; + end = name.LastIndexOf ('.', end-1); + } while (end > 0); + + return null; + } + private static IEnumerable GetOracleJdkPaths () { string subkey = @"SOFTWARE\JavaSoft\Java Development Kit"; diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs new file mode 100644 index 0000000..cde9f65 --- /dev/null +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkWindowsTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +using NUnit.Framework; + +namespace Xamarin.Android.Tools.Tests +{ + [TestFixture] + public class AndroidSdkWindowsTests + { + [Test] + public void ExtractVersion () + { + var sep = Path.DirectorySeparatorChar; + + var tests = new[]{ + new { + Path = $"foo{sep}", + Prefix = "", + Expected = (Version) null, + }, + new { + Path = $"foo{sep}bar-1-extra", + Prefix = "bar-", + Expected = (Version) null, + }, + new { + Path = $"foo{sep}abcdef", + Prefix = "a", + Expected = (Version) null, + }, + new { + Path = $"foo{sep}a{sep}b.c.d", + Prefix = "none-of-the-above", + Expected = (Version) null, + }, + new { + Path = $"jdks{sep}jdk-1.2.3-hotspot-extra", + Prefix = "jdk-", + Expected = new Version (1, 2, 3), + }, + new { + Path = $"jdks{sep}jdk-1.2.3-hotspot-extra", + Prefix = "jdk", + Expected = new Version (1, 2, 3), + }, + new { + Path = $"jdks{sep}jdk-1.2.3.4.5.6-extra", + Prefix = "jdk-", + Expected = new Version (1, 2, 3, 4), + }, + }; + + foreach (var test in tests) { + Assert.AreEqual ( + test.Expected, + AndroidSdkWindows.ExtractVersion (test.Path, test.Prefix), + $"Version couldn't be extracted from Path=`{test.Path}` Prefix=`{test.Prefix}`!" + ); + } + } + } +} diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj index ca3992e..4100134 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj @@ -5,6 +5,8 @@ If $(TargetFramework) is declared here instead, it will not be evaluated before Directory.Build.props is loaded and the wrong $(TestOutputFullPath) will be used. --> netcoreapp3.1 + true + ..\..\product.snk false $(TestOutputFullPath) false