diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/MacJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/MacJdkProvider.java
new file mode 100644
index 0000000..9a8debf
--- /dev/null
+++ b/src/main/java/dev/jbang/devkitman/jdkproviders/MacJdkProvider.java
@@ -0,0 +1,99 @@
+package dev.jbang.devkitman.jdkproviders;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.NonNull;
+
+import dev.jbang.devkitman.JdkDiscovery;
+import dev.jbang.devkitman.JdkProvider;
+import dev.jbang.devkitman.util.FileUtils;
+import dev.jbang.devkitman.util.OsUtils;
+
+/**
+ * This JDK provider detects JDKs that have been installed in the standard
+ * location on macOS: {@code /Library/Java/JavaVirtualMachines/}.
+ *
+ *
+ * On macOS, JDKs are stored as bundles with the structure:
+ * {@code /Library/Java/JavaVirtualMachines/.jdk/Contents/Home}
+ */
+public class MacJdkProvider extends BaseFoldersJdkProvider {
+ private static final Path JDKS_ROOT = Paths.get("/Library/Java/JavaVirtualMachines");
+ private static final String CONTENTS_HOME = "Contents/Home";
+
+ public MacJdkProvider() {
+ super(jdksRoot());
+ }
+
+ MacJdkProvider(@NonNull Path jdksRoot) {
+ super(jdksRoot);
+ }
+
+ public static Path jdksRoot() {
+ return JDKS_ROOT;
+ }
+
+ @Override
+ public @NonNull String description() {
+ return "The JDKs installed in /Library/Java/JavaVirtualMachines on macOS.";
+ }
+
+ @Override
+ public boolean canUse() {
+ return OsUtils.isMac() && super.canUse();
+ }
+
+ @Override
+ @NonNull
+ protected Stream listJdkPaths() throws IOException {
+ if (Files.isDirectory(jdksRoot)) {
+ return Files.list(jdksRoot)
+ .map(bundle -> bundle.resolve(CONTENTS_HOME))
+ .filter(this::acceptFolder);
+ }
+ return Stream.empty();
+ }
+
+ @Override
+ protected boolean acceptFolder(@NonNull Path jdkFolder) {
+ return super.acceptFolder(jdkFolder) && !FileUtils.isLink(jdkFolder.getParent().getParent());
+ }
+
+ @Override
+ public String jdkId(@NonNull Path jdkFolder) {
+ // jdkFolder is /.jdk/Contents/Home
+ // Use the bundle name (without .jdk extension) as the ID
+ String bundleName = jdkFolder.getParent().getParent().getFileName().toString();
+ if (bundleName.endsWith(".jdk")) {
+ bundleName = bundleName.substring(0, bundleName.length() - 4);
+ }
+ return bundleName;
+ }
+
+ @Override
+ @NonNull
+ protected Path getJdkPath(@NonNull String id) {
+ // Strip the provider suffix to get the bundle base name
+ String bundleBase = id.endsWith("-" + name()) ? id.substring(0, id.length() - name().length() - 1) : id;
+ return jdksRoot.resolve(bundleBase + ".jdk").resolve(CONTENTS_HOME);
+ }
+
+ public static class Discovery implements JdkDiscovery {
+ public static final String PROVIDER_ID = "mac";
+
+ @Override
+ @NonNull
+ public String name() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public JdkProvider create(@NonNull Config config) {
+ return new MacJdkProvider();
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery
index 56a02a0..8b8a140 100644
--- a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery
+++ b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery
@@ -5,6 +5,7 @@ dev.jbang.devkitman.jdkproviders.PathJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.LinkedJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.JBangJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.LinuxJdkProvider$Discovery
+dev.jbang.devkitman.jdkproviders.MacJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.MiseJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.MultiHomeJdkProvider$Discovery
dev.jbang.devkitman.jdkproviders.ScoopJdkProvider$Discovery
diff --git a/src/test/java/dev/jbang/devkitman/TestJdkProviders.java b/src/test/java/dev/jbang/devkitman/TestJdkProviders.java
index 2fd4bcf..44b2015 100644
--- a/src/test/java/dev/jbang/devkitman/TestJdkProviders.java
+++ b/src/test/java/dev/jbang/devkitman/TestJdkProviders.java
@@ -34,6 +34,7 @@ void testAllNames() {
"linked",
"jbang",
"linux",
+ "mac",
"mise",
"multihome",
"scoop",
@@ -76,6 +77,7 @@ void testAll() {
instanceOf(LinkedJdkProvider.class),
instanceOf(JBangJdkProvider.class),
instanceOf(LinuxJdkProvider.class),
+ instanceOf(MacJdkProvider.class),
instanceOf(MiseJdkProvider.class),
instanceOf(MultiHomeJdkProvider.class),
instanceOf(ScoopJdkProvider.class),
diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/MacJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/MacJdkProviderTest.java
new file mode 100644
index 0000000..ffbeb1d
--- /dev/null
+++ b/src/test/java/dev/jbang/devkitman/jdkproviders/MacJdkProviderTest.java
@@ -0,0 +1,82 @@
+package dev.jbang.devkitman.jdkproviders;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import dev.jbang.devkitman.BaseTest;
+import dev.jbang.devkitman.Jdk;
+
+public class MacJdkProviderTest extends BaseTest {
+
+ private Path createMockMacJdkBundle(Path jvmRoot, String bundleName, int version) throws IOException {
+ Path bundlePath = jvmRoot.resolve(bundleName + ".jdk");
+ Path home = bundlePath.resolve("Contents/Home");
+ Files.createDirectories(home);
+ initMockJdkDir(home, version + ".0.7");
+ return home;
+ }
+
+ @Test
+ void testMacProviderFindsInstalledJdks() throws IOException {
+ Path jvmRoot = config.cachePath().resolve("JavaVirtualMachines");
+ Files.createDirectories(jvmRoot);
+
+ createMockMacJdkBundle(jvmRoot, "temurin-17", 17);
+ createMockMacJdkBundle(jvmRoot, "temurin-21", 21);
+
+ MacJdkProvider provider = new MacJdkProvider(jvmRoot);
+
+ List installed = provider.listInstalled().collect(Collectors.toList());
+
+ assertThat(installed, hasSize(2));
+ List ids = installed.stream().map(Jdk::id).toList();
+ assertThat(ids.contains("temurin-17"), is(true));
+ assertThat(ids.contains("temurin-21"), is(true));
+ }
+
+ @Test
+ void testMacProviderIgnoresJreOnlyBundles() throws IOException {
+ Path jvmRoot = config.cachePath().resolve("JavaVirtualMachines");
+ Files.createDirectories(jvmRoot);
+
+ Path home21 = createMockMacJdkBundle(jvmRoot, "temurin-21", 21);
+
+ // Create a JRE bundle (no javac)
+ Path jreBundle = jvmRoot.resolve("corretto-jre-11.jre");
+ Path jreHome = jreBundle.resolve("Contents/Home");
+ Files.createDirectories(jreHome);
+ initMockJdkDir(jreHome, "11.0.7", "JAVA_RUNTIME_VERSION", false, false, false, false);
+
+ MacJdkProvider provider = new MacJdkProvider(jvmRoot);
+
+ List installed = provider.listInstalled().collect(Collectors.toList());
+
+ assertThat(installed, hasSize(1));
+ assertThat(installed.get(0).id(), is("temurin-21"));
+ assertThat(installed.get(0).home(), is(home21));
+ }
+
+ @Test
+ void testMacProviderJdkId() throws IOException {
+ Path jvmRoot = config.cachePath().resolve("JavaVirtualMachines");
+ Files.createDirectories(jvmRoot);
+
+ createMockMacJdkBundle(jvmRoot, "zulu-17.0.10", 17);
+
+ MacJdkProvider provider = new MacJdkProvider(jvmRoot);
+
+ List installed = provider.listInstalled().collect(Collectors.toList());
+
+ assertThat(installed, hasSize(1));
+ assertThat(installed.get(0).id(), is("zulu-17.0.10"));
+ }
+}