Skip to content

[Xamarin.Android.Tools.AndroidSdk] Probe for Microsoft OpenJDK dirs #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the need for p/invoke here? Is it an issue with 32-bit processes reading 64-bit keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure of the original need; this code never used Microsoft.Win32.Registry & co, and I'm not sure why. (Apparently a question for @mhutch?)

Thus, given that we're already a tangled mess of P/Invokes, I figured it "best" to continue with the current paradigm.

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