diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java index 593a44c..609c924 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java @@ -130,8 +130,7 @@ protected Stream listJdkPaths() throws IOException { } protected boolean acceptFolder(@NonNull Path jdkFolder) { - return jdkFolder.startsWith(jdksRoot) && isValidId(jdkFolder.getFileName().toString()) - && JavaUtils.hasJavacCmd(jdkFolder); + return jdkFolder.startsWith(jdksRoot) && JavaUtils.hasJavacCmd(jdkFolder); } private final Pattern validId = Pattern.compile("^[a-zA-Z0-9._+-]+$"); diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java index 54371c3..336cb5f 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java @@ -88,7 +88,8 @@ protected boolean acceptFolder(@NonNull Path jdkFolder) { // We additionally allow folders that are named with a number // (e.g. "11", "17", etc.) for backwards compatibility with older // JBang versions - return (super.acceptFolder(jdkFolder) || JavaUtils.parseToInt(jdkFolder.getFileName().toString(), 0) > 0) + return (super.acceptFolder(jdkFolder) && isValidId(jdkFolder.getFileName().toString()) + || JavaUtils.parseToInt(jdkFolder.getFileName().toString(), 0) > 0) && !FileUtils.isLink(jdkFolder); } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java index 6a438ad..9b907eb 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java @@ -65,7 +65,8 @@ public LinkedJdkProvider(Path jdksRoot) { @Override protected boolean acceptFolder(@NonNull Path jdkFolder) { - return super.acceptFolder(jdkFolder) && FileUtils.isLink(jdkFolder); + return super.acceptFolder(jdkFolder) && isValidId(jdkFolder.getFileName().toString()) + && FileUtils.isLink(jdkFolder); } @Override diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java index f2b27cb..b7701fd 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java @@ -14,18 +14,22 @@ * standard location of the users linux distro. * *

- * For now just using `/usr/lib/devkitman` as apparently fedora, debian, ubuntu - * and centos/rhel use it. + * For now just using `/usr/lib/jvm` as apparently fedora, debian, ubuntu and + * centos/rhel use it. * *

* If need different behavior per linux distro its intended this provider will * adjust based on identified distro. */ public class LinuxJdkProvider extends BaseFoldersJdkProvider { - protected static final Path JDKS_ROOT = Paths.get("/usr/lib/devkitman"); + private static final Path JDKS_ROOT = Paths.get("/usr/lib/jvm"); public LinuxJdkProvider() { - super(JDKS_ROOT); + super(jdksRoot()); + } + + public static Path jdksRoot() { + return JDKS_ROOT; } @Override @@ -35,7 +39,7 @@ public LinuxJdkProvider() { @Override protected boolean acceptFolder(@NonNull Path jdkFolder) { - return super.acceptFolder(jdkFolder) && !FileUtils.isSameFolderLink(jdkFolder); + return super.acceptFolder(jdkFolder) && !FileUtils.isLink(jdkFolder); } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java index 0e0179b..1c80f46 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java @@ -14,11 +14,14 @@ * (https://mise.jdx.dev/) */ public class MiseJdkProvider extends BaseFoldersJdkProvider { - private static final Path JDKS_ROOT = Paths.get(System.getProperty("user.home")) - .resolve(".local/share/mise/installs/java"); + private static final Path JDKS_ROOT = Paths.get(".local", "share", "mise", "installs", "java"); public MiseJdkProvider() { - super(JDKS_ROOT); + super(jdksRoot()); + } + + public static Path jdksRoot() { + return Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT); } @Override diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java index f0731a4..dad5af7 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java @@ -1,14 +1,13 @@ 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 org.jspecify.annotations.Nullable; -import dev.jbang.devkitman.Jdk; import dev.jbang.devkitman.JdkDiscovery; import dev.jbang.devkitman.JdkProvider; import dev.jbang.devkitman.util.OsUtils; @@ -18,10 +17,14 @@ * package manager. Windows only. */ public class ScoopJdkProvider extends BaseFoldersJdkProvider { - private static final Path SCOOP_APPS = Paths.get(System.getProperty("user.home")).resolve("scoop/apps"); + private static final Path JDKS_ROOT = Paths.get("scoop", "apps"); public ScoopJdkProvider() { - super(SCOOP_APPS); + super(jdksRoot()); + } + + public static Path jdksRoot() { + return Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT); } @Override @@ -32,23 +35,18 @@ public ScoopJdkProvider() { @NonNull @Override protected Stream listJdkPaths() throws IOException { - return super.listJdkPaths().map(p -> p.resolve("current")); + if (Files.isDirectory(jdksRoot)) { + return Files.list(jdksRoot) + .filter(p -> p.getFileName().toString().startsWith("openjdk")) + .map(p -> p.resolve("current")) + .filter(this::acceptFolder); + } + return Stream.empty(); } @Override protected boolean acceptFolder(@NonNull Path jdkFolder) { - return jdkFolder.getFileName().startsWith("openjdk") && super.acceptFolder(jdkFolder); - } - - @Override - protected Jdk.@Nullable InstalledJdk createJdk(@NonNull Path home) { - try { - // Try to resolve any links - home = home.toRealPath(); - } catch (IOException e) { - throw new IllegalStateException("Couldn't resolve 'current' link: " + home, e); - } - return super.createJdk(home); + return jdkFolder.getParent().getFileName().toString().startsWith("openjdk") && super.acceptFolder(jdkFolder); } @Override diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java index fe421eb..d36d8fc 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java @@ -8,7 +8,6 @@ import dev.jbang.devkitman.JdkDiscovery; import dev.jbang.devkitman.JdkProvider; import dev.jbang.devkitman.util.FileUtils; -import dev.jbang.devkitman.util.JavaUtils; /** * This JDK provider detects any JDKs that have been installed using the SDKMAN @@ -18,7 +17,11 @@ public class SdkmanJdkProvider extends BaseFoldersJdkProvider { private static final Path JDKS_ROOT = Paths.get(".sdkman", "candidates", "java"); public SdkmanJdkProvider() { - super(Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT)); + super(jdksRoot()); + } + + public static Path jdksRoot() { + return Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT); } @Override @@ -28,9 +31,7 @@ public SdkmanJdkProvider() { @Override protected boolean acceptFolder(@NonNull Path jdkFolder) { - return jdkFolder.startsWith(jdksRoot) - && !FileUtils.isSameFolderLink(jdkFolder) - && JavaUtils.hasJavacCmd(jdkFolder); + return super.acceptFolder(jdkFolder) && !FileUtils.isSameFolderLink(jdkFolder); } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProvider.java new file mode 100644 index 0000000..b35c259 --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProvider.java @@ -0,0 +1,159 @@ +package dev.jbang.devkitman.jdkproviders; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkDiscovery; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.JavaUtils; +import dev.jbang.devkitman.util.OsUtils; + +/** + * This JDK provider detects JDKs registered under HKLM\SOFTWARE\JavaSoft. + */ +public class WindowsJdkProvider extends BaseJdkProvider { + public static final String JAVA_SOFT_KEY = "HKLM\\SOFTWARE\\JavaSoft"; + + private static final Logger LOGGER = Logger.getLogger(WindowsJdkProvider.class.getName()); + private static final Pattern KEY_LINE = Pattern.compile("^\\s*HKEY_.*$"); + private static final Pattern JAVA_HOME_LINE = Pattern.compile("^\\s*JavaHome\\s+REG_\\w+\\s+(.+?)\\s*$"); + private static final Pattern VALID_ID = Pattern.compile("^[a-zA-Z0-9._+-]+-windows$"); + + private final RegistryReader registryReader; + + public WindowsJdkProvider() { + this(new RegCommandRegistryReader()); + } + + public WindowsJdkProvider(@NonNull RegistryReader registryReader) { + this.registryReader = registryReader; + } + + @Override + @NonNull + public String name() { + return Discovery.PROVIDER_ID; + } + + @Override + public @NonNull String description() { + return "The JDKs registered in the HKLM\\SOFTWARE\\JavaSoft registry key."; + } + + @Override + public boolean canUse() { + return OsUtils.isWindows(); + } + + @Override + public boolean isValidId(@NonNull String id) { + return VALID_ID.matcher(id).matches(); + } + + @NonNull + @Override + public Stream listInstalled() { + return dedupeByJavaHome(registryReader.listJavaHomes(JAVA_SOFT_KEY)) + .entrySet() + .stream() + .map(this::createJdk) + .filter(Objects::nonNull); + } + + private Map dedupeByJavaHome(Map javaHomes) { + Map> selectedByHome = new LinkedHashMap<>(); + for (Entry entry : javaHomes.entrySet()) { + selectedByHome.merge(entry.getValue(), entry, this::preferLongestKey); + } + Map deduped = new LinkedHashMap<>(); + for (Entry entry : selectedByHome.values()) { + deduped.put(entry.getKey(), entry.getValue()); + } + return deduped; + } + + private Entry preferLongestKey(Entry existing, Entry replacement) { + return replacement.getKey().length() > existing.getKey().length() ? replacement : existing; + } + + private Jdk.InstalledJdk createJdk(Map.Entry javaHomeEntry) { + Path javaHome = javaHomeEntry.getValue(); + if (!Files.isDirectory(javaHome) || !JavaUtils.hasJavacCmd(javaHome)) { + return null; + } + String id = registryVersionSegment(javaHomeEntry.getKey()) + "-" + name(); + return createJdk(id, javaHome); + } + + private String registryVersionSegment(String registryKey) { + int idx = registryKey.lastIndexOf('\\'); + String segment = idx >= 0 && idx + 1 < registryKey.length() ? registryKey.substring(idx + 1) : registryKey; + return segment.replaceAll("[^a-zA-Z0-9._+-]+", "_"); + } + + public interface RegistryReader { + @NonNull + Map listJavaHomes(@NonNull String rootKey); + } + + static class RegCommandRegistryReader implements RegistryReader { + @Override + public @NonNull Map listJavaHomes(@NonNull String rootKey) { + Map javaHomes = new LinkedHashMap<>(); + String output = OsUtils.runCommand("reg", "query", rootKey, "/s", "/v", "JavaHome"); + if (output == null || output.trim().isEmpty()) { + return javaHomes; + } + String currentKey = null; + for (String line : output.split("\\r?\\n")) { + Matcher keyMatcher = KEY_LINE.matcher(line); + if (keyMatcher.matches()) { + currentKey = line.trim(); + continue; + } + if (currentKey == null) { + continue; + } + Matcher javaHomeMatcher = JAVA_HOME_LINE.matcher(line); + if (javaHomeMatcher.matches()) { + String home = javaHomeMatcher.group(1).trim(); + try { + javaHomes.put(currentKey, Paths.get(home)); + } catch (InvalidPathException ex) { + LOGGER.log(Level.FINE, "Ignoring invalid registry JavaHome: " + home, ex); + } + } + } + return javaHomes; + } + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "windows"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(@NonNull Config config) { + return new WindowsJdkProvider(); + } + } +} 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 13b505a..56a02a0 100644 --- a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery +++ b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkDiscovery @@ -9,4 +9,5 @@ dev.jbang.devkitman.jdkproviders.MiseJdkProvider$Discovery dev.jbang.devkitman.jdkproviders.MultiHomeJdkProvider$Discovery dev.jbang.devkitman.jdkproviders.ScoopJdkProvider$Discovery dev.jbang.devkitman.jdkproviders.SdkmanJdkProvider$Discovery +dev.jbang.devkitman.jdkproviders.WindowsJdkProvider$Discovery diff --git a/src/test/java/dev/jbang/devkitman/TestJdkProviders.java b/src/test/java/dev/jbang/devkitman/TestJdkProviders.java index 78f7a38..2fd4bcf 100644 --- a/src/test/java/dev/jbang/devkitman/TestJdkProviders.java +++ b/src/test/java/dev/jbang/devkitman/TestJdkProviders.java @@ -37,7 +37,8 @@ void testAllNames() { "mise", "multihome", "scoop", - "sdkman")); + "sdkman", + "windows")); } @Test @@ -78,7 +79,8 @@ void testAll() { instanceOf(MiseJdkProvider.class), instanceOf(MultiHomeJdkProvider.class), instanceOf(ScoopJdkProvider.class), - instanceOf(SdkmanJdkProvider.class))); + instanceOf(SdkmanJdkProvider.class), + instanceOf(WindowsJdkProvider.class))); } @Test diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/ExternalJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/ExternalJdkProviderTest.java new file mode 100644 index 0000000..4069ed2 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/ExternalJdkProviderTest.java @@ -0,0 +1,47 @@ +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +import java.nio.file.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; + +public class ExternalJdkProviderTest extends BaseTest { + + @Test + void testExternalProviderCreatesStableIdForSamePath() { + Path jdkHome = createMockJdkExt(24); + ExternalJdkProvider provider = new ExternalJdkProvider(); + + Jdk.InstalledJdk jdk1 = provider.getInstalledByPath(jdkHome); + Jdk.InstalledJdk jdk2 = provider.getInstalledByPath(jdkHome.toAbsolutePath()); + + assertThat(jdk1, notNullValue()); + assertThat(jdk2, notNullValue()); + assertThat(jdk1.provider(), instanceOf(ExternalJdkProvider.class)); + assertThat(jdk1.home(), Matchers.is(jdkHome)); + assertThat(jdk1.id(), Matchers.startsWith("external-")); + assertThat(jdk1.id(), Matchers.is(jdk2.id())); + } + + @Test + void testExternalProviderCreatesDifferentIdsForDifferentPaths() { + Path jdkHome1 = createMockJdkExt(25); + Path jdkHome2 = createMockJdkExt(26); + ExternalJdkProvider provider = new ExternalJdkProvider(); + + Jdk.InstalledJdk jdk1 = provider.getInstalledByPath(jdkHome1); + Jdk.InstalledJdk jdk2 = provider.getInstalledByPath(jdkHome2); + + assertThat(jdk1, notNullValue()); + assertThat(jdk2, notNullValue()); + assertThat(jdk1.id(), not(jdk2.id())); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProviderTest.java new file mode 100644 index 0000000..a44f2cc --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProviderTest.java @@ -0,0 +1,41 @@ +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; + +public class JavaHomeJdkProviderTest extends BaseTest { + + @Test + void testJavaHomeProviderFindsInstalledJdkByVersionPattern() { + Path jdkHome = createMockJdkExt(23); + environmentVariables.set("JAVA_HOME", jdkHome.toString()); + + Jdk.InstalledJdk jdk = jdkManager("javahome").getInstalledJdk("23+"); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(JavaHomeJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is("javahome")); + } + + @Test + void testJavaHomeProviderIgnoresInvalidJavaHomePath() throws IOException { + Path invalidHome = config.cachePath().resolve("invalid-java-home"); + Files.createDirectories(invalidHome); + environmentVariables.set("JAVA_HOME", invalidHome.toString()); + + assertThat(jdkManager("javahome").listInstalledJdks(), empty()); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/MiseJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/MiseJdkProviderTest.java new file mode 100644 index 0000000..dbdefbe --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/MiseJdkProviderTest.java @@ -0,0 +1,54 @@ +package dev.jbang.devkitman.jdkproviders; + +import static dev.jbang.devkitman.jdkproviders.MiseJdkProvider.jdksRoot; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.util.FileUtils; + +public class MiseJdkProviderTest extends BaseTest { + + @Test + void testMiseProviderFindsInstalledJdkByVersionPattern() { + Path jdkHome = installMiseJdk("25.0.1-tem", "25.0.1"); + Jdk.InstalledJdk jdk = jdkManager("mise").getInstalledJdk("25+"); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(MiseJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is(jdkHome.getFileName().toString())); + } + + @Test + void testMiseProviderIgnoresCurrentSymlink() { + Path jdkHome = installMiseJdk("26.0.1-tem", "26.0.1"); + Path current = jdksRoot().resolve("current"); + FileUtils.createLink(current, jdkHome); + + List ids = jdkManager("mise").listInstalledJdks() + .stream() + .map(Jdk::id) + .collect(Collectors.toList()); + assertThat(ids, hasSize(1)); + assertThat(ids, Matchers.contains("26.0.1-tem")); + assertThat(new MiseJdkProvider().getInstalledByPath(current), nullValue()); + } + + private Path installMiseJdk(String folderName, String releaseVersion) { + Path jdkHome = jdksRoot().resolve(folderName); + initMockJdkDir(jdkHome, releaseVersion); + return jdkHome; + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProviderTest.java new file mode 100644 index 0000000..45354db --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProviderTest.java @@ -0,0 +1,71 @@ +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; + +public class MultiHomeJdkProviderTest extends BaseTest { + + @Test + void testMultiHomeProviderFindsInstalledJdkByVersionPattern() { + clearJavaHomeVariables(); + + Path jdkHome = createMockJdkExt(21); + environmentVariables.set("JAVA_HOME_21_X64", jdkHome.toString()); + + Jdk.InstalledJdk jdk = jdkManager("multihome").getInstalledJdk("21+"); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(MultiHomeJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is("multihome_21_x64")); + } + + @Test + void testMultiHomeProviderIgnoresNonExistingHomes() { + clearJavaHomeVariables(); + + Path validJdkHome = createMockJdkExt(17); + environmentVariables.set("JAVA_HOME_17_X64", validJdkHome.toString()); + environmentVariables.set("JAVA_HOME_18_X64", config.cachePath().resolve("missing-jdk").toString()); + environmentVariables.set("NOT_A_JAVA_HOME", validJdkHome.toString()); + + List jdks = jdkManager("multihome").listInstalledJdks(); + + assertThat(jdks, hasSize(1)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), contains("multihome_17_x64")); + } + + @Test + void testMultiHomeProviderNormalizesEnvSuffixInJdkId() { + clearJavaHomeVariables(); + + Path jdkHome = createMockJdkExt(12); + environmentVariables.set("JAVA_HOME_12_X64_ARM", jdkHome.toString()); + + Jdk.InstalledJdk jdk = new MultiHomeJdkProvider().getInstalledByPath(jdkHome); + + assertThat(jdk, notNullValue()); + assertThat(jdk.id(), Matchers.is("multihome_12_x64_arm")); + } + + private void clearJavaHomeVariables() { + environmentVariables.getVariables() + .keySet() + .stream() + .filter(key -> key.startsWith("JAVA_HOME_")) + .forEach(environmentVariables::remove); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/PathJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/PathJdkProviderTest.java new file mode 100644 index 0000000..ccfd3d0 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/PathJdkProviderTest.java @@ -0,0 +1,39 @@ +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +import java.nio.file.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; + +public class PathJdkProviderTest extends BaseTest { + + @Test + void testPathProviderFindsInstalledJdkByVersionPattern() { + Path jdkHome = createMockJdkExt(22); + environmentVariables.set("PATH", jdkHome.resolve("bin").toString()); + + Jdk.InstalledJdk jdk = jdkManager("path").getInstalledJdk("22+"); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(PathJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is("path")); + } + + @Test + void testPathProviderIgnoresPathWithoutJavac() { + Path jreHome = config.cachePath().resolve("jre17"); + initMockJdkDir(jreHome, "17.0.7", "JAVA_RUNTIME_VERSION", false, false, false, false); + environmentVariables.set("PATH", jreHome.resolve("bin").toString()); + + assertThat(jdkManager("path").listInstalledJdks(), empty()); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProviderTest.java new file mode 100644 index 0000000..594b2ce --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProviderTest.java @@ -0,0 +1,71 @@ +package dev.jbang.devkitman.jdkproviders; + +import static dev.jbang.devkitman.jdkproviders.ScoopJdkProvider.jdksRoot; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.JavaUtils; + +public class ScoopJdkProviderTest extends BaseTest { + @Test + void testScoopProviderFindsInstalledJdkByVersionPattern() { + Path jdkHome = installScoopJdk("25.0.1"); + Jdk.InstalledJdk jdk = jdkManager("scoop").getInstalledJdk("25+"); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(ScoopJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is(jdkHome.getFileName().toString())); + } + + @Test + void testScoopProviderIgnoresOtherScoopApps() { + Path jdkHome = installScoopJdk("25.0.1"); + Path gitHome = installOtherScoopApp("git", true); + installOtherScoopApp("7zip", false); + Path nodejsHome = installOtherScoopApp("nodejs", true); + + ScoopJdkProvider provider = new ScoopJdkProvider(); + Jdk.InstalledJdk jdk = provider.getInstalledByPath(jdkHome); + assertThat(jdk, notNullValue()); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(provider.getInstalledByPath(gitHome), nullValue()); + assertThat(provider.getInstalledByPath(nodejsHome), nullValue()); + } + + private Path installScoopJdk(String releaseVersion) { + int majorVersion = JavaUtils.parseJavaVersion(releaseVersion); + Path scoopPackagePath = jdksRoot().resolve("openjdk" + majorVersion); + + Path jdkHome = scoopPackagePath.resolve(releaseVersion); + initMockJdkDir(jdkHome, releaseVersion); + Path current = scoopPackagePath.resolve("current"); + FileUtils.createLink(current, jdkHome); + return current; + } + + private Path installOtherScoopApp(String appName, boolean looksLikeJdk) { + Path appPath = jdksRoot().resolve(appName).resolve("99.0.1"); + if (looksLikeJdk) { + initMockJdkDir(appPath, "99.0.1"); + } else { + try { + Files.createDirectories(appPath); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return appPath; + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProviderTest.java new file mode 100644 index 0000000..bab5dd4 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/WindowsJdkProviderTest.java @@ -0,0 +1,85 @@ +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; + +public class WindowsJdkProviderTest extends BaseTest { + @Test + void testWindowsProviderFindsInstalledJdksFromRegistry() { + Path jdk17 = createMockJdkExt(17); + Path jdk21 = createMockJdkExt(21); + + WindowsJdkProvider provider = new WindowsJdkProvider(rootKey -> { + Map homes = new LinkedHashMap<>(); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\17", jdk17); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\21", jdk21); + return homes; + }); + + List installed = provider.listInstalled().collect(Collectors.toList()); + + assertThat(installed, hasSize(2)); + assertThat( + installed.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("17-windows", "21-windows")); + Jdk.InstalledJdk resolved = provider.getInstalledById("21-windows"); + assertThat(resolved, notNullValue()); + assertThat(resolved.home(), is(jdk21)); + } + + @Test + void testWindowsProviderIgnoresInvalidRegistryEntries() { + Path validJdk = createMockJdkExt(23); + Path jreHome = config.cachePath().resolve("jre23"); + initMockJdkDir(jreHome, "23.0.7", "JAVA_RUNTIME_VERSION", false, false, false, false); + Path missingPath = config.cachePath().resolve("missing-jdk"); + + WindowsJdkProvider provider = new WindowsJdkProvider(rootKey -> { + Map homes = new LinkedHashMap<>(); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\23", validJdk); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\23", jreHome); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\99", missingPath); + return homes; + }); + + List installed = provider.listInstalled().collect(Collectors.toList()); + + assertThat(installed, hasSize(1)); + assertThat(installed.get(0).id(), is("23-windows")); + assertThat(installed.get(0).home(), is(validJdk)); + } + + @Test + void testWindowsProviderDeduplicatesSameHomeKeepingLongestKey() { + Path jdk8 = createMockJdk("1.8.0_333-distro-jbang", "1.8.0_333"); + + WindowsJdkProvider provider = new WindowsJdkProvider(rootKey -> { + Map homes = new LinkedHashMap<>(); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\1.8", jdk8); + homes.put("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\1.8.0_333", jdk8); + return homes; + }); + + List installed = provider.listInstalled().collect(Collectors.toList()); + + assertThat(installed, hasSize(1)); + assertThat(installed.get(0).id(), is("1.8.0_333-windows")); + assertThat(provider.getInstalledById("1.8-windows"), nullValue()); + assertThat(provider.getInstalledById("1.8.0_333-windows"), notNullValue()); + } +}