Skip to content

Commit ebd3aaf

Browse files
Add JDK discovery for user-writable paths on Windows and macOS (#269)
- Windows: `%LocalAppData%\Android\*jdk*` - matches `jdk-21`, `jdk-21.0.8.1-hotspot`, `microsoft-21.jdk`, etc. - macOS: `~/Library/Android/*jdk*/` - supports both bundle (`Contents/Home`) and flat JDK structures - Both integrated into `MicrosoftOpenJdkLocations.GetMicrosoftOpenJdks()` discovery chain - Rely on `TryGetJdkInfo()` for validation - Add parameterized tests for multiple folder naming patterns Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
1 parent 604940c commit ebd3aaf

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.MacOS.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,42 @@ static IEnumerable<string> GetUnixConfiguredJdkPaths (Action<TraceLevel, string>
3131

3232
const string MacOSJavaVirtualMachinesRoot = "/Library/Java/JavaVirtualMachines";
3333

34+
protected static IEnumerable<JdkInfo> GetMacOSUserFileSystemJdks (Action<TraceLevel, string> logger)
35+
{
36+
if (!OS.IsMac) {
37+
return Array.Empty<JdkInfo> ();
38+
}
39+
40+
// Search ~/Library/Android/*jdk*/ (matches microsoft-21.jdk, jdk-21, etc.)
41+
var libraryAndroidRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Android");
42+
if (!Directory.Exists (libraryAndroidRoot)) {
43+
return Array.Empty<JdkInfo> ();
44+
}
45+
46+
IEnumerable<string> dirs;
47+
try {
48+
dirs = Directory.EnumerateDirectories (libraryAndroidRoot, "*jdk*");
49+
}
50+
catch (IOException) {
51+
return Array.Empty<JdkInfo> ();
52+
}
53+
54+
var toHome = Path.Combine ("Contents", "Home");
55+
var paths = new List<string> ();
56+
foreach (var dir in dirs) {
57+
// Check for macOS .jdk bundle structure (Contents/Home)
58+
var bundleHome = Path.Combine (dir, toHome);
59+
if (Directory.Exists (bundleHome)) {
60+
paths.Add (bundleHome);
61+
} else {
62+
// Flat JDK structure - let TryGetJdkInfo validate
63+
paths.Add (dir);
64+
}
65+
}
66+
67+
return FromPaths (paths, logger, "~/Library/Android/*jdk*/");
68+
}
69+
3470
protected static IEnumerable<JdkInfo> GetMacOSSystemJdks (string pattern, Action<TraceLevel, string> logger, string? locator = null)
3571
{
3672
if (!OS.IsMac) {

src/Xamarin.Android.Tools.AndroidSdk/Jdks/JdkLocations.Windows.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,34 @@ protected static IEnumerable<JdkInfo> GetWindowsFileSystemJdks (string pattern,
5454
}
5555
}
5656

57+
protected static IEnumerable<JdkInfo> GetWindowsUserFileSystemJdks (Action<TraceLevel, string> logger)
58+
{
59+
if (!OS.IsWindows) {
60+
yield break;
61+
}
62+
63+
var root = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android");
64+
if (!Directory.Exists (root)) {
65+
yield break;
66+
}
67+
68+
IEnumerable<string> homes;
69+
try {
70+
// Match any folder containing "jdk" (jdk-21, microsoft-21.jdk, jdk-21.0.8.1-hotspot, etc.)
71+
homes = Directory.EnumerateDirectories (root, "*jdk*");
72+
}
73+
catch (IOException) {
74+
yield break;
75+
}
76+
77+
foreach (var home in homes) {
78+
var jdk = JdkInfo.TryGetJdkInfo (home, logger, @"%LocalAppData%\Android\*jdk*");
79+
if (jdk == null)
80+
continue;
81+
yield return jdk;
82+
}
83+
}
84+
5785
protected static IEnumerable<JdkInfo> GetWindowsRegistryJdks (
5886
Action<TraceLevel, string> logger,
5987
string parentKey,

src/Xamarin.Android.Tools.AndroidSdk/Jdks/MicrosoftOpenJdkLocations.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ class MicrosoftOpenJdkLocations : JdkLocations {
1212
internal static IEnumerable<JdkInfo> GetMicrosoftOpenJdks (Action<TraceLevel, string> logger)
1313
{
1414
return GetMacOSSystemJdks ("microsoft-*.jdk", logger)
15+
.Concat (GetMacOSUserFileSystemJdks (logger))
1516
.Concat (GetWindowsFileSystemJdks (Path.Combine ("Android", "openjdk", "jdk-*"), logger))
1617
.Concat (GetWindowsFileSystemJdks (Path.Combine ("Microsoft", "jdk-*"), logger))
1718
.Concat (GetWindowsRegistryJdks (logger, @"SOFTWARE\Microsoft\JDK", "*", @"hotspot\MSI", "Path"))
19+
.Concat (GetWindowsUserFileSystemJdks (logger))
1820
.OrderByDescending (jdk => jdk, JdkInfoVersionComparer.Default);
1921
}
2022
}

tests/Xamarin.Android.Tools.AndroidSdk-Tests/JdkInfoTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,5 +271,91 @@ public void Version_ThrowsNotSupportedException ()
271271
Directory.Delete (dir, recursive: true);
272272
}
273273
}
274+
275+
[Test]
276+
[TestCase ("jdk-21.0.99")]
277+
[TestCase ("jdk-21.0.8.1-hotspot")]
278+
[TestCase ("microsoft-21.jdk")]
279+
public void GetKnownSystemJdkInfos_DiscoversWindowsUserJdk (string folderName)
280+
{
281+
if (!OS.IsWindows) {
282+
Assert.Ignore ("This test is only valid on Windows.");
283+
return;
284+
}
285+
286+
var userJdkRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "Android");
287+
var testJdkDir = Path.Combine (userJdkRoot, folderName);
288+
289+
try {
290+
CreateFauxJdk (testJdkDir, releaseVersion: "21.0.99", releaseBuildNumber: "1", javaVersion: "21.0.99-1");
291+
292+
Action<TraceLevel, string> logger = (level, message) => {
293+
Console.WriteLine ($"[{level}] {message}");
294+
};
295+
296+
var jdks = JdkInfo.GetKnownSystemJdkInfos (logger).ToList ();
297+
var foundJdk = jdks.FirstOrDefault (j => j.HomePath == testJdkDir);
298+
299+
Assert.IsNotNull (foundJdk, $"Expected to find JDK at {testJdkDir} with folder name '{folderName}'");
300+
Assert.AreEqual (@"%LocalAppData%\Android\*jdk*", foundJdk.Locator, $"Locator should indicate user JDK path for folder '{folderName}'");
301+
Assert.AreEqual (new Version (21, 0, 99, 1), foundJdk.Version);
302+
}
303+
finally {
304+
if (Directory.Exists (testJdkDir))
305+
Directory.Delete (testJdkDir, recursive: true);
306+
// Clean up the parent directories if they're empty
307+
if (Directory.Exists (userJdkRoot) && !Directory.EnumerateFileSystemEntries (userJdkRoot).Any ())
308+
Directory.Delete (userJdkRoot);
309+
}
310+
}
311+
312+
[Test]
313+
[TestCase ("microsoft-21.jdk", false)]
314+
[TestCase ("jdk-21", true)]
315+
[TestCase ("jdk-21.0.8.1-hotspot", true)]
316+
[TestCase ("temurin-21.jdk", false)]
317+
public void GetKnownSystemJdkInfos_DiscoversMacOSUserJdk (string folderName, bool isFlat)
318+
{
319+
if (!OS.IsMac) {
320+
Assert.Ignore ("This test is only valid on macOS.");
321+
return;
322+
}
323+
324+
var userJdkRoot = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Android");
325+
string testJdkDir;
326+
string testJdkBundle;
327+
328+
if (isFlat) {
329+
// Flat structure: release file directly in folder
330+
testJdkDir = Path.Combine (userJdkRoot, folderName);
331+
testJdkBundle = testJdkDir;
332+
} else {
333+
// Bundle structure: Contents/Home inside folder
334+
testJdkBundle = Path.Combine (userJdkRoot, folderName);
335+
testJdkDir = Path.Combine (testJdkBundle, "Contents", "Home");
336+
}
337+
338+
try {
339+
CreateFauxJdk (testJdkDir, releaseVersion: "21.0.99", releaseBuildNumber: "1", javaVersion: "21.0.99-1");
340+
341+
Action<TraceLevel, string> logger = (level, message) => {
342+
Console.WriteLine ($"[{level}] {message}");
343+
};
344+
345+
var jdks = JdkInfo.GetKnownSystemJdkInfos (logger).ToList ();
346+
var foundJdk = jdks.FirstOrDefault (j => j.HomePath == testJdkDir);
347+
348+
Assert.IsNotNull (foundJdk, $"Expected to find JDK at {testJdkDir} with folder name '{folderName}' (isFlat={isFlat})");
349+
Assert.AreEqual ("~/Library/Android/*jdk*/", foundJdk.Locator, $"Locator should indicate user JDK path for folder '{folderName}'");
350+
Assert.AreEqual (new Version (21, 0, 99, 1), foundJdk.Version);
351+
}
352+
finally {
353+
if (Directory.Exists (testJdkBundle))
354+
Directory.Delete (testJdkBundle, recursive: true);
355+
// Clean up the parent directories if they're empty
356+
if (Directory.Exists (userJdkRoot) && !Directory.EnumerateFileSystemEntries (userJdkRoot).Any ())
357+
Directory.Delete (userJdkRoot);
358+
}
359+
}
274360
}
275361
}

0 commit comments

Comments
 (0)