Skip to content

Commit 237642c

Browse files
authored
[Xamarin.Android.Tools.AndroidSdk] Probe for Microsoft OpenJDK dirs (#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.
1 parent e618e00 commit 237642c

File tree

7 files changed

+265
-30
lines changed

7 files changed

+265
-30
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public static void DetectAndSetPreferredJavaSdkPathToLatest (Action<TraceLevel,
185185

186186
logger = logger ?? DefaultConsoleLogger;
187187

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

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public static IEnumerable<JdkInfo> GetKnownSystemJdkInfos (Action<TraceLevel, st
287287
return GetEnvironmentVariableJdks ("JI_JAVA_HOME", logger)
288288
.Concat (GetWindowsJdks (logger))
289289
.Concat (GetConfiguredJdks (logger))
290-
.Concat (GetMacOSMicrosoftJdks (logger))
290+
.Concat (GetMacOSMicrosoftOpenJdks (logger))
291291
.Concat (GetEnvironmentVariableJdks ("JAVA_HOME", logger))
292292
.Concat (GetPathEnvironmentJdks (logger))
293293
.Concat (GetLibexecJdks (logger))
@@ -313,27 +313,47 @@ static IEnumerable<string> GetConfiguredJdkPaths (Action<TraceLevel, string> log
313313
}
314314
}
315315

316-
internal static IEnumerable<JdkInfo> GetMacOSMicrosoftJdks (Action<TraceLevel, string> logger)
316+
internal static IEnumerable<JdkInfo> GetMicrosoftOpenJdks (Action<TraceLevel, string> logger)
317317
{
318-
return GetMacOSMicrosoftJdkPaths ()
319-
.Select (p => TryGetJdkInfo (p, logger, "$HOME/Library/Developer/Xamarin/jdk"))
318+
foreach (var dir in GetMacOSMicrosoftOpenJdks (logger))
319+
yield return dir;
320+
if (Path.DirectorySeparatorChar != '\\')
321+
yield break;
322+
foreach (var dir in AndroidSdkWindows.GetJdkInfos (logger)) {
323+
yield return dir;
324+
}
325+
}
326+
327+
static IEnumerable<JdkInfo> GetMacOSMicrosoftOpenJdks (Action<TraceLevel, string> logger)
328+
{
329+
return GetMacOSMicrosoftOpenJdkPaths ()
330+
.Select (p => TryGetJdkInfo (p, logger, "/Library/Java/JavaVirtualMachines/microsoft-*.jdk"))
320331
.Where (jdk => jdk != null)
321332
.Select (jdk => jdk!)
322333
.OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default);
323334
}
324335

325-
static IEnumerable<string> GetMacOSMicrosoftJdkPaths ()
336+
static IEnumerable<string> GetMacOSMicrosoftOpenJdkPaths ()
326337
{
338+
var root = "/Library/Java/JavaVirtualMachines";
339+
var pattern = "microsoft-*.jdk";
340+
var toHome = Path.Combine ("Contents", "Home");
327341
var jdks = AppDomain.CurrentDomain.GetData ($"GetMacOSMicrosoftJdkPaths jdks override! {typeof (JdkInfo).AssemblyQualifiedName}")
328342
?.ToString ();
329-
if (jdks == null) {
330-
var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
331-
jdks = Path.Combine (home, "Library", "Developer", "Xamarin", "jdk");
343+
if (jdks != null) {
344+
root = jdks;
345+
toHome = "";
346+
pattern = "*";
347+
}
348+
if (!Directory.Exists (root)) {
349+
yield break;
350+
}
351+
foreach (var dir in Directory.EnumerateDirectories (root, pattern)) {
352+
var home = Path.Combine (dir, toHome);
353+
if (!Directory.Exists (home))
354+
continue;
355+
yield return home;
332356
}
333-
if (!Directory.Exists (jdks))
334-
return Enumerable.Empty <string> ();
335-
336-
return Directory.EnumerateDirectories (jdks);
337357
}
338358

339359
static JdkInfo? TryGetJdkInfo (string path, Action<TraceLevel, string> logger, string locator)

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Runtime.InteropServices;
34
using System.IO;
45
using System.Text;
@@ -139,9 +140,89 @@ static extern int RegSetValueExW (UIntPtr hKey, string lpValueName, int lpReserv
139140
static extern int RegCreateKeyEx (UIntPtr hKey, string subKey, uint reserved, string? @class, uint options,
140141
uint samDesired, IntPtr lpSecurityAttributes, out UIntPtr phkResult, out Disposition lpdwDisposition);
141142

143+
// https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regenumkeyexw
144+
[DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)]
145+
static extern int RegEnumKeyExW (
146+
UIntPtr hKey,
147+
uint dwIndex,
148+
[Out] char[] lpName,
149+
ref uint lpcchName,
150+
IntPtr lpReserved,
151+
IntPtr lpClass,
152+
IntPtr lpcchClass,
153+
IntPtr lpftLastWriteTime
154+
);
155+
156+
// https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regqueryinfokeyw
157+
[DllImport (ADVAPI, CharSet = CharSet.Unicode, SetLastError = true)]
158+
static extern int RegQueryInfoKey (
159+
UIntPtr hKey,
160+
IntPtr lpClass,
161+
IntPtr lpcchClass,
162+
IntPtr lpReserved,
163+
out uint lpcSubkey,
164+
out uint lpcchMaxSubkeyLen,
165+
IntPtr lpcchMaxClassLen,
166+
IntPtr lpcValues,
167+
IntPtr lpcchMaxValueNameLen,
168+
IntPtr lpcbMaxValueLen,
169+
IntPtr lpSecurityDescriptor,
170+
IntPtr lpftLastWriteTime
171+
);
172+
142173
[DllImport ("advapi32.dll", SetLastError = true)]
143174
static extern int RegCloseKey (UIntPtr hKey);
144175

176+
public static IEnumerable<string> EnumerateSubkeys (UIntPtr key, string subkey, Wow64 wow64)
177+
{
178+
UIntPtr regKeyHandle;
179+
uint sam = (uint)Rights.Read + (uint)wow64;
180+
int r = RegOpenKeyEx (key, subkey, 0, sam, out regKeyHandle);
181+
if (r != 0) {
182+
yield break;
183+
}
184+
try {
185+
r = RegQueryInfoKey (
186+
hKey: regKeyHandle,
187+
lpClass: IntPtr.Zero,
188+
lpcchClass: IntPtr.Zero,
189+
lpReserved: IntPtr.Zero,
190+
lpcSubkey: out uint cSubkeys,
191+
lpcchMaxSubkeyLen: out uint cchMaxSubkeyLen,
192+
lpcchMaxClassLen: IntPtr.Zero,
193+
lpcValues: IntPtr.Zero,
194+
lpcchMaxValueNameLen: IntPtr.Zero,
195+
lpcbMaxValueLen: IntPtr.Zero,
196+
lpSecurityDescriptor: IntPtr.Zero,
197+
lpftLastWriteTime: IntPtr.Zero
198+
);
199+
if (r != 0) {
200+
yield break;
201+
}
202+
var name = new char [cchMaxSubkeyLen+1];
203+
for (uint i = 0; i < cSubkeys; ++i) {
204+
var nameLen = (uint) name.Length;
205+
r = RegEnumKeyExW (
206+
hKey: regKeyHandle,
207+
dwIndex: i,
208+
lpName: name,
209+
lpcchName: ref nameLen,
210+
lpReserved: IntPtr.Zero,
211+
lpClass: IntPtr.Zero,
212+
lpcchClass: IntPtr.Zero,
213+
lpftLastWriteTime: IntPtr.Zero
214+
);
215+
if (r != 0) {
216+
continue;
217+
}
218+
yield return new string (name, 0, (int) nameLen);
219+
}
220+
}
221+
finally {
222+
RegCloseKey (regKeyHandle);
223+
}
224+
}
225+
145226
public static string? GetValueString (UIntPtr key, string subkey, string valueName, Wow64 wow64)
146227
{
147228
UIntPtr regKeyHandle;
@@ -192,6 +273,9 @@ enum Rights : uint
192273
SetValue = 0x0002,
193274
CreateSubKey = 0x0004,
194275
EnumerateSubKey = 0x0008,
276+
Notify = 0x0010,
277+
Read = _StandardRead | QueryValue | EnumerateSubKey | Notify,
278+
_StandardRead = 0x20000,
195279
}
196280

197281
enum Options
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo (
4+
"Xamarin.Android.Tools.AndroidSdk-Tests, PublicKey=" +
5+
"0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" +
6+
"6cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2" +
7+
"814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" +
8+
"d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" +
9+
"2c9733db"
10+
)]

src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class AndroidSdkWindows : AndroidSdkBase
1818
const string ANDROID_INSTALLER_KEY = "Path";
1919
const string XAMARIN_ANDROID_INSTALLER_PATH = @"SOFTWARE\Xamarin\MonoAndroid";
2020
const string XAMARIN_ANDROID_INSTALLER_KEY = "PrivateAndroidSdkPath";
21+
const string MICROSOFT_OPENJDK_PATH = @"SOFTWARE\Microsoft\JDK";
2122

2223
public AndroidSdkWindows (Action<TraceLevel, string> logger)
2324
: base (logger)
@@ -131,8 +132,9 @@ IEnumerable<JdkInfo> ToJdkInfos (IEnumerable<string> paths, string locator)
131132
}
132133

133134
return ToJdkInfos (GetPreferredJdkPaths (), "Preferred Registry")
134-
.Concat (ToJdkInfos (GetOpenJdkPaths (), "OpenJDK"))
135-
.Concat (ToJdkInfos (GetKnownOpenJdkPaths (), "Well-known OpenJDK paths"))
135+
.Concat (ToJdkInfos (GetMicrosoftOpenJdkFilesystemPaths (), "Microsoft OpenJDK Filesystem"))
136+
.Concat (ToJdkInfos (GetMicrosoftOpenJdkRegistryPaths (), "Microsoft OpenJDK Registry"))
137+
.Concat (ToJdkInfos (GetVSAndroidJdkPaths (), @"HKLM\SOFTWARE\Microsoft\VisualStudio\Android@JavaHome"))
136138
.Concat (ToJdkInfos (GetOracleJdkPaths (), "Oracle JDK"))
137139
;
138140
}
@@ -150,7 +152,7 @@ private static IEnumerable<string> GetPreferredJdkPaths ()
150152
}
151153
}
152154

153-
private static IEnumerable<string> GetOpenJdkPaths ()
155+
private static IEnumerable<string> GetVSAndroidJdkPaths ()
154156
{
155157
var root = RegistryEx.LocalMachine;
156158
var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 };
@@ -163,28 +165,24 @@ private static IEnumerable<string> GetOpenJdkPaths ()
163165
}
164166
}
165167

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

174172
var paths = new List<Tuple<string, Version>> ();
175173
var rootPaths = new List<string> {
176-
Path.Combine (Environment.ExpandEnvironmentVariables ("%ProgramW6432%"), "Android", "jdk"),
174+
Path.Combine (Environment.ExpandEnvironmentVariables ("%ProgramW6432%"), "Microsoft"),
177175
Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), "Android", "jdk"),
178176
};
179177

180178
foreach (var rootPath in rootPaths) {
181-
if (Directory.Exists (rootPath)) {
182-
foreach (var directoryName in Directory.EnumerateDirectories (rootPath, $"{JdkFolderNamePattern}*").ToList ()) {
183-
var versionString = directoryName.Replace ($"{rootPath}\\{JdkFolderNamePattern}", string.Empty);
184-
if (Version.TryParse (versionString, out Version? ver)) {
185-
paths.Add (new Tuple<string, Version>(directoryName, ver));
186-
}
187-
}
179+
if (!Directory.Exists (rootPath))
180+
continue;
181+
foreach (var directoryName in Directory.EnumerateDirectories (rootPath, $"{JdkFolderNamePrefix}*")) {
182+
var version = ExtractVersion (directoryName, JdkFolderNamePrefix);
183+
if (version == null)
184+
continue;
185+
paths.Add (Tuple.Create (directoryName, version));
188186
}
189187
}
190188

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

194+
static IEnumerable<string> GetMicrosoftOpenJdkRegistryPaths ()
195+
{
196+
var paths = new List<(Version version, string path)> ();
197+
var roots = new[] { RegistryEx.CurrentUser, RegistryEx.LocalMachine };
198+
var wows = new[] { RegistryEx.Wow64.Key32, RegistryEx.Wow64.Key64 };
199+
foreach (var root in roots)
200+
foreach (var wow in wows) {
201+
foreach (var subkeyName in RegistryEx.EnumerateSubkeys (root, MICROSOFT_OPENJDK_PATH, wow)) {
202+
if (!Version.TryParse (subkeyName, out var version))
203+
continue;
204+
var msiKey = $@"{MICROSOFT_OPENJDK_PATH}\{subkeyName}\hotspot\MSI";
205+
var path = RegistryEx.GetValueString (root, msiKey, "Path", wow);
206+
if (path == null)
207+
continue;
208+
paths.Add ((version, path));
209+
}
210+
}
211+
212+
return paths.OrderByDescending (e => e.version)
213+
.Select (e => e.path);
214+
}
215+
216+
internal static Version? ExtractVersion (string path, string prefix)
217+
{
218+
var name = Path.GetFileName (path);
219+
if (name.Length <= prefix.Length)
220+
return null;
221+
if (!name.StartsWith (prefix, StringComparison.OrdinalIgnoreCase))
222+
return null;
223+
224+
var start = prefix.Length;
225+
while (start < name.Length && !char.IsDigit (name, start)) {
226+
++start;
227+
}
228+
if (start == name.Length)
229+
return null;
230+
231+
name = name.Substring (start);
232+
int end = 0;
233+
while (end < name.Length &&
234+
(char.IsDigit (name [end]) || name [end] == '.')) {
235+
end++;
236+
}
237+
238+
do {
239+
if (Version.TryParse (name.Substring (0, end), out var v))
240+
return v;
241+
end = name.LastIndexOf ('.', end-1);
242+
} while (end > 0);
243+
244+
return null;
245+
}
246+
196247
private static IEnumerable<string> GetOracleJdkPaths ()
197248
{
198249
string subkey = @"SOFTWARE\JavaSoft\Java Development Kit";
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Xml.Linq;
8+
9+
using NUnit.Framework;
10+
11+
namespace Xamarin.Android.Tools.Tests
12+
{
13+
[TestFixture]
14+
public class AndroidSdkWindowsTests
15+
{
16+
[Test]
17+
public void ExtractVersion ()
18+
{
19+
var sep = Path.DirectorySeparatorChar;
20+
21+
var tests = new[]{
22+
new {
23+
Path = $"foo{sep}",
24+
Prefix = "",
25+
Expected = (Version) null,
26+
},
27+
new {
28+
Path = $"foo{sep}bar-1-extra",
29+
Prefix = "bar-",
30+
Expected = (Version) null,
31+
},
32+
new {
33+
Path = $"foo{sep}abcdef",
34+
Prefix = "a",
35+
Expected = (Version) null,
36+
},
37+
new {
38+
Path = $"foo{sep}a{sep}b.c.d",
39+
Prefix = "none-of-the-above",
40+
Expected = (Version) null,
41+
},
42+
new {
43+
Path = $"jdks{sep}jdk-1.2.3-hotspot-extra",
44+
Prefix = "jdk-",
45+
Expected = new Version (1, 2, 3),
46+
},
47+
new {
48+
Path = $"jdks{sep}jdk-1.2.3-hotspot-extra",
49+
Prefix = "jdk",
50+
Expected = new Version (1, 2, 3),
51+
},
52+
new {
53+
Path = $"jdks{sep}jdk-1.2.3.4.5.6-extra",
54+
Prefix = "jdk-",
55+
Expected = new Version (1, 2, 3, 4),
56+
},
57+
};
58+
59+
foreach (var test in tests) {
60+
Assert.AreEqual (
61+
test.Expected,
62+
AndroidSdkWindows.ExtractVersion (test.Path, test.Prefix),
63+
$"Version couldn't be extracted from Path=`{test.Path}` Prefix=`{test.Prefix}`!"
64+
);
65+
}
66+
}
67+
}
68+
}

tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
If $(TargetFramework) is declared here instead, it will not be evaluated before Directory.Build.props
66
is loaded and the wrong $(TestOutputFullPath) will be used. -->
77
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
8+
<SignAssembly>true</SignAssembly>
9+
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
810
<IsPackable>false</IsPackable>
911
<OutputPath>$(TestOutputFullPath)</OutputPath>
1012
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

0 commit comments

Comments
 (0)