Skip to content

Commit

Permalink
[Xamarin.Android.Tools.AndroidSdk] Probe for Microsoft OpenJDK dirs (#…
Browse files Browse the repository at this point in the history
…113)

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.
  • Loading branch information
jonpryor authored Mar 24, 2021
1 parent e618e00 commit 237642c
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/Xamarin.Android.Tools.AndroidSdk/AndroidSdkInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public static void DetectAndSetPreferredJavaSdkPathToLatest (Action<TraceLevel,

logger = logger ?? DefaultConsoleLogger;

var latestJdk = JdkInfo.GetMacOSMicrosoftJdks (logger).FirstOrDefault ();
var latestJdk = JdkInfo.GetMicrosoftOpenJdks (logger).FirstOrDefault ();
if (latestJdk == null)
throw new NotSupportedException ("No Microsoft OpenJDK could be found. Please re-run the Visual Studio installer or manually specify the JDK path in settings.");

Expand Down
44 changes: 32 additions & 12 deletions src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ public static IEnumerable<JdkInfo> GetKnownSystemJdkInfos (Action<TraceLevel, st
return GetEnvironmentVariableJdks ("JI_JAVA_HOME", logger)
.Concat (GetWindowsJdks (logger))
.Concat (GetConfiguredJdks (logger))
.Concat (GetMacOSMicrosoftJdks (logger))
.Concat (GetMacOSMicrosoftOpenJdks (logger))
.Concat (GetEnvironmentVariableJdks ("JAVA_HOME", logger))
.Concat (GetPathEnvironmentJdks (logger))
.Concat (GetLibexecJdks (logger))
Expand All @@ -313,27 +313,47 @@ static IEnumerable<string> GetConfiguredJdkPaths (Action<TraceLevel, string> log
}
}

internal static IEnumerable<JdkInfo> GetMacOSMicrosoftJdks (Action<TraceLevel, string> logger)
internal static IEnumerable<JdkInfo> GetMicrosoftOpenJdks (Action<TraceLevel, string> 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<JdkInfo> GetMacOSMicrosoftOpenJdks (Action<TraceLevel, string> 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<string> GetMacOSMicrosoftJdkPaths ()
static IEnumerable<string> 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 <string> ();

return Directory.EnumerateDirectories (jdks);
}

static JdkInfo? TryGetJdkInfo (string path, Action<TraceLevel, string> logger, string locator)
Expand Down
84 changes: 84 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/OS.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -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<string> 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;
Expand Down Expand Up @@ -192,6 +273,9 @@ enum Rights : uint
SetValue = 0x0002,
CreateSubKey = 0x0004,
EnumerateSubKey = 0x0008,
Notify = 0x0010,
Read = _StandardRead | QueryValue | EnumerateSubKey | Notify,
_StandardRead = 0x20000,
}

enum Options
Expand Down
10 changes: 10 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo (
"Xamarin.Android.Tools.AndroidSdk-Tests, PublicKey=" +
"0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" +
"6cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2" +
"814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" +
"d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" +
"2c9733db"
)]
85 changes: 68 additions & 17 deletions src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TraceLevel, string> logger)
: base (logger)
Expand Down Expand Up @@ -131,8 +132,9 @@ IEnumerable<JdkInfo> ToJdkInfos (IEnumerable<string> 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"))
;
}
Expand All @@ -150,7 +152,7 @@ private static IEnumerable<string> GetPreferredJdkPaths ()
}
}

private static IEnumerable<string> GetOpenJdkPaths ()
private static IEnumerable<string> GetVSAndroidJdkPaths ()
{
var root = RegistryEx.LocalMachine;
var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 };
Expand All @@ -163,28 +165,24 @@ private static IEnumerable<string> GetOpenJdkPaths ()
}
}

/// <summary>
/// Locate OpenJDK installations by well known path.
/// </summary>
/// <returns>List of valid OpenJDK paths in version descending order.</returns>
private static IEnumerable<string> GetKnownOpenJdkPaths ()
static IEnumerable<string> GetMicrosoftOpenJdkFilesystemPaths ()
{
string JdkFolderNamePattern = "microsoft_dist_openjdk_";
const string JdkFolderNamePrefix = "jdk-";

var paths = new List<Tuple<string, Version>> ();
var rootPaths = new List<string> {
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<string, Version>(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));
}
}

Expand All @@ -193,6 +191,59 @@ private static IEnumerable<string> GetKnownOpenJdkPaths ()
.Select (openJdk => openJdk.Item1);
}

static IEnumerable<string> 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<string> GetOracleJdkPaths ()
{
string subkey = @"SOFTWARE\JavaSoft\Java Development Kit";
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`!"
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<OutputPath>$(TestOutputFullPath)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
Expand Down

0 comments on commit 237642c

Please sign in to comment.