diff --git a/build.gradle b/build.gradle index 0a443de16..10a251385 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,8 @@ sourceSets { sourceSets.main.compileClasspath += sourceSets.java9.output.classesDirs; dependencies { + implementation project(':jdkmanager') + implementation 'com.offbytwo:docopt:0.6.0.20150202' implementation 'org.apache.commons:commons-text:1.11.0' @@ -110,6 +112,7 @@ dependencies { runtimeOnly "eu.maveniverse.maven.mima.runtime:standalone-static:2.4.20" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.1" + testImplementation project(':jdkmanager') testImplementation "org.junit.jupiter:junit-jupiter:5.10.1" testImplementation "com.github.stefanbirkner:system-rules:1.17.2" testImplementation "org.hamcrest:hamcrest-library:2.2" diff --git a/jdkmanager/.gitignore b/jdkmanager/.gitignore new file mode 100644 index 000000000..b46b8e314 --- /dev/null +++ b/jdkmanager/.gitignore @@ -0,0 +1,22 @@ +.classpath +.project +.vscode +.settings +target +.idea +*.iml +/build +.gradle +.factorypath +bin +homebrew-tap +RESULTS +*.db +jbang-action +out +node_modules +package-lock.json +*.jfr +itests/hello.java +*.class +CHANGELOG.md diff --git a/jdkmanager/build.gradle b/jdkmanager/build.gradle new file mode 100644 index 000000000..c9282bfa6 --- /dev/null +++ b/jdkmanager/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' +} + +group = 'dev.jbang.jvm' +version = '0.1.0' + +sourceCompatibility = '8' +targetCompatibility = '8' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.apache.commons:commons-compress:1.26.2' + implementation 'org.apache.httpcomponents:httpclient:4.5.14' + implementation 'org.apache.httpcomponents:httpclient-cache:4.5.14' + implementation 'com.google.code.gson:gson:2.11.0' + + implementation "org.slf4j:slf4j-nop:1.7.30" + implementation "org.slf4j:jcl-over-slf4j:1.7.30" + implementation "org.jspecify:jspecify:1.0.0" + + testImplementation platform('org.junit:junit-bom:5.10.1') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/Jdk.java b/jdkmanager/src/main/java/dev/jbang/jvm/Jdk.java new file mode 100644 index 000000000..a07236c7c --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/Jdk.java @@ -0,0 +1,122 @@ +package dev.jbang.jvm; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public interface Jdk extends Comparable { + @NonNull JdkProvider getProvider(); + + @NonNull String getId(); + + @NonNull String getVersion(); + + @Nullable Path getHome(); + + int getMajorVersion(); + + @NonNull Jdk install(); + + void uninstall(); + + boolean isInstalled(); + + class Default implements Jdk { + @NonNull private final transient JdkProvider provider; + @NonNull private final String id; + @NonNull private final String version; + @Nullable private final Path home; + @NonNull private final Set tags = new HashSet<>(); + + Default( + @NonNull JdkProvider provider, + @NonNull String id, + @Nullable Path home, + @NonNull String version, + @NonNull String... tags) { + this.provider = provider; + this.id = id; + this.version = version; + this.home = home; + } + + @Override + @NonNull + public JdkProvider getProvider() { + return provider; + } + + /** Returns the id that is used to uniquely identify this JDK across all providers */ + @Override + @NonNull + public String getId() { + return id; + } + + /** Returns the JDK's version */ + @Override + @NonNull + public String getVersion() { + return version; + } + + /** + * The path to where the JDK is installed. Can be null which means the JDK + * isn't currently installed by that provider + */ + @Override + @Nullable + public Path getHome() { + return home; + } + + @Override + public int getMajorVersion() { + return JavaUtils.parseJavaVersion(getVersion()); + } + + @Override + @NonNull + public Jdk install() { + return provider.install(this); + } + + @Override + public void uninstall() { + provider.uninstall(this); + } + + @Override + public boolean isInstalled() { + return home != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Default jdk = (Default) o; + return id.equals(jdk.id) && Objects.equals(home, jdk.home); + } + + @Override + public int hashCode() { + return Objects.hash(home, id); + } + + @Override + public int compareTo(Jdk o) { + return Integer.compare(getMajorVersion(), o.getMajorVersion()); + } + + @Override + public String toString() { + return getMajorVersion() + " (" + version + ", " + id + ", " + home + ")"; + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/JdkManager.java b/jdkmanager/src/main/java/dev/jbang/jvm/JdkManager.java new file mode 100644 index 000000000..7ce5ef2c0 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkManager.java @@ -0,0 +1,504 @@ +package dev.jbang.jvm; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.jbang.jvm.jdkproviders.*; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class JdkManager { + private static final Logger LOGGER = Logger.getLogger(JdkManager.class.getName()); + + private final List providers; + private final int defaultJavaVersion; + + private final JdkProvider defaultProvider; + + public static class Builder { + protected final List providers = new ArrayList<>(); + protected int defaultJavaVersion = 0; + + protected Builder() {} + + public Builder provider(JdkProvider... provs) { + providers.addAll(Arrays.asList(provs)); + return this; + } + + public Builder defaultJavaVersion(int defaultJavaVersion) { + this.defaultJavaVersion = defaultJavaVersion; + return this; + } + + public JdkManager build() { + if (providers.isEmpty()) { + throw new IllegalStateException("No providers could be initialized. Aborting."); + } + return new JdkManager(providers, defaultJavaVersion); + } + } + + private JdkManager(List providers, int defaultJavaVersion) { + this.providers = Collections.unmodifiableList(providers); + this.defaultJavaVersion = defaultJavaVersion; + this.defaultProvider = provider("default"); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine( + "Using JDK provider(s): " + + providers.stream() + .map(p -> p.getClass().getSimpleName()) + .collect(Collectors.joining(", "))); + } + } + + @NonNull + private List providers() { + return providers; + } + + @NonNull + private List updatableProviders() { + return providers().stream().filter(JdkProvider::canUpdate).collect(Collectors.toList()); + } + + @Nullable + private JdkProvider provider(String name) { + return providers().stream() + .filter(p -> p.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + /** + * This method is like getJdk() but will make sure that the JDK being returned is + * actually installed. It will perform an installation if necessary. + * + * @param versionOrId A version pattern, id or null + * @return A Jdk object + * @throws IllegalArgumentException If no JDK could be found at all or if one failed to install + */ + @NonNull + public Jdk getOrInstallJdk(String versionOrId) { + if (versionOrId != null) { + if (JavaUtils.isRequestedVersion(versionOrId)) { + return getOrInstallJdkByVersion( + JavaUtils.minRequestedVersion(versionOrId), + JavaUtils.isOpenVersion(versionOrId), + false); + } else { + return getOrInstallJdkById(versionOrId, false); + } + } else { + return getOrInstallJdkByVersion(0, true, false); + } + } + + /** + * This method is like getJdkByVersion() but will make sure that the JDK being + * returned is actually installed. It will perform an installation if necessary. + * + * @param requestedVersion The (minimal) version to return, can be 0 + * @param openVersion Return newer version if exact is not available + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all or if one failed to install + */ + @NonNull + private Jdk getOrInstallJdkByVersion( + int requestedVersion, boolean openVersion, boolean updatableOnly) { + LOGGER.log(Level.FINE, "Looking for JDK: {0}", requestedVersion); + Jdk jdk = getJdkByVersion(requestedVersion, openVersion, updatableOnly); + if (jdk == null) { + if (requestedVersion > 0) { + throw new IllegalArgumentException( + "No suitable JDK was found for requested version: " + requestedVersion); + } else { + throw new IllegalArgumentException("No suitable JDK was found"); + } + } + jdk = ensureInstalled(jdk); + LOGGER.log(Level.FINE, "Using JDK: {0}", jdk); + + return jdk; + } + + /** + * This method is like getJdkByVersion() but will make sure that the JDK being + * returned is actually installed. It will perform an installation if necessary. + * + * @param requestedId The id of the JDK to return + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all or if one failed to install + */ + @NonNull + private Jdk getOrInstallJdkById(@NonNull String requestedId, boolean updatableOnly) { + LOGGER.log(Level.FINE, "Looking for JDK: {0}", requestedId); + Jdk jdk = getJdkById(requestedId, updatableOnly); + if (jdk == null) { + throw new IllegalArgumentException( + "No suitable JDK was found for requested id: " + requestedId); + } + jdk = ensureInstalled(jdk); + LOGGER.log(Level.FINE, "Using JDK: {0}", jdk); + + return jdk; + } + + private Jdk ensureInstalled(Jdk jdk) { + if (!jdk.isInstalled()) { + jdk = jdk.install(); + if (getDefaultJdk() == null) { + setDefaultJdk(jdk); + } + } + return jdk; + } + + /** + * Returns a Jdk object that matches the requested version from the list of + * currently installed JDKs or from the ones available for installation. The parameter is a + * string that either contains the actual (strict) major version of the JDK that should be + * returned, an open version terminated with a + sign to indicate that any later + * version is valid as well, or it is an id that will be matched against the ids of JDKs that + * are currently installed. If the requested version is null the "active" JDK will + * be returned, this is normally the JDK currently being used to run JBang itself. The method + * will return null if no installed or available JDK matches. NB: This method can + * return Jdk objects for JDKs that are currently _not_ installed. It will not + * cause any installs to be performed. See getOrInstallJdk() for that. + * + * @param versionOrId A version pattern, id or null + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + public Jdk getJdk(@Nullable String versionOrId, boolean updatableOnly) { + if (versionOrId != null) { + if (JavaUtils.isRequestedVersion(versionOrId)) { + return getJdkByVersion( + JavaUtils.minRequestedVersion(versionOrId), + JavaUtils.isOpenVersion(versionOrId), + updatableOnly); + } else { + return getJdkById(versionOrId, updatableOnly); + } + } else { + return getJdkByVersion(0, true, updatableOnly); + } + } + + /** + * Returns an Jdk object that matches the requested version from the list of + * currently installed JDKs or from the ones available for installation. The method will return + * null if no installed or available JDK matches. NB: This method can return + * Jdk objects for JDKs that are currently _not_ installed. It will not cause any + * installs to be performed. See getOrInstallJdkByVersion() for that. + * + * @param requestedVersion The (minimal) version to return, can be 0 + * @param openVersion Return newer version if exact is not available + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + private Jdk getJdkByVersion(int requestedVersion, boolean openVersion, boolean updatableOnly) { + Jdk jdk = getInstalledJdkByVersion(requestedVersion, openVersion, updatableOnly); + if (jdk == null) { + if (requestedVersion > 0 + && (requestedVersion >= defaultJavaVersion || !openVersion)) { + jdk = getAvailableJdkByVersion(requestedVersion, false); + } else { + jdk = getJdkByVersion(defaultJavaVersion, true, updatableOnly); + } + } + return jdk; + } + + /** + * Returns an Jdk object that matches the requested version from the list of + * currently installed JDKs or from the ones available for installation. The method will return + * null if no installed or available JDK matches. NB: This method can return + * Jdk objects for JDKs that are currently _not_ installed. It will not cause any + * installs to be performed. See getOrInstallJdkByVersion() for that. + * + * @param requestedId The id of the JDK to return + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + private Jdk getJdkById(@NonNull String requestedId, boolean updatableOnly) { + Jdk jdk = getInstalledJdkById(requestedId, updatableOnly); + if (jdk == null) { + jdk = getAvailableJdkById(requestedId); + } + return jdk; + } + + /** + * Returns an Jdk object for an installed JDK of the given version or id. Will + * return null if no JDK of that version or id is currently installed. + * + * @param versionOrId A version pattern, id or null + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + */ + @Nullable + public Jdk getInstalledJdk(String versionOrId, boolean updatableOnly) { + if (versionOrId != null) { + if (JavaUtils.isRequestedVersion(versionOrId)) { + return getInstalledJdkByVersion( + JavaUtils.minRequestedVersion(versionOrId), + JavaUtils.isOpenVersion(versionOrId), + updatableOnly); + } else { + return getInstalledJdkById(versionOrId, updatableOnly); + } + } else { + return getInstalledJdkByVersion(0, true, updatableOnly); + } + } + + /** + * Returns an Jdk object for an installed JDK of the given version. Will return + * null if no JDK of that version is currently installed. + * + * @param version The (major) version of the JDK to return + * @param openVersion Return newer version if exact is not available + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + */ + @Nullable + private Jdk getInstalledJdkByVersion(int version, boolean openVersion, boolean updatableOnly) { + return providers().stream() + .filter(p -> !updatableOnly || p.canUpdate()) + .map(p -> p.getInstalledByVersion(version, openVersion)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** + * Returns an Jdk object for an installed JDK with the given id. Will return + * null if no JDK with that id is currently installed. + * + * @param requestedId The id of the JDK to return + * @param updatableOnly Only return JDKs from updatable providers or not + * @return A Jdk object or null + */ + @Nullable + private Jdk getInstalledJdkById(String requestedId, boolean updatableOnly) { + return providers().stream() + .filter(p -> !updatableOnly || p.canUpdate()) + .map(p -> p.getInstalledById(requestedId)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @NonNull + private Jdk getAvailableJdkByVersion(int version, boolean openVersion) { + Optional jdks = getJdkByVersion(listAvailableJdks(), version, openVersion); + if (!jdks.isPresent()) { + throw new IllegalArgumentException( + "JDK version is not available for installation: " + + version + + "\n" + + "Use 'jbang jdk list --available' to see a list of JDKs available for installation"); + } + return jdks.get(); + } + + @NonNull + private Jdk getAvailableJdkById(String id) { + Optional jdks = getJdkById(listAvailableJdks(), id); + if (!jdks.isPresent()) { + throw new IllegalArgumentException( + "JDK id is not available for installation: " + + id + + "\n" + + "Use 'jbang jdk list --available --show-details' to see a list of JDKs available for installation"); + } + return jdks.get(); + } + + public void uninstallJdk(Jdk jdk) { + Jdk defaultJdk = getDefaultJdk(); + if (OsUtils.isWindows()) { + // On Windows we have to check nobody is currently using the JDK or we could + // be causing all kinds of trouble + try { + Path jdkTmpDir = + jdk.getHome() + .getParent() + .resolve("_delete_me_" + jdk.getHome().getFileName().toString()); + Files.move(jdk.getHome(), jdkTmpDir); + Files.move(jdkTmpDir, jdk.getHome()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Cannot uninstall JDK, it's being used: {0}", jdk); + return; + } + } + + boolean resetDefault = false; + if (defaultJdk != null) { + Path defHome = defaultJdk.getHome(); + try { + resetDefault = Files.isSameFile(defHome, jdk.getHome()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Error while trying to reset default JDK", ex); + resetDefault = defHome.equals(jdk.getHome()); + } + } + + jdk.uninstall(); + + if (resetDefault) { + Optional newjdk = nextInstalledJdk(jdk.getMajorVersion(), true); + if (!newjdk.isPresent()) { + newjdk = prevInstalledJdk(jdk.getMajorVersion(), true); + } + if (newjdk.isPresent()) { + setDefaultJdk(newjdk.get()); + } else { + removeDefaultJdk(); + LOGGER.log(Level.INFO, "Default JDK unset"); + } + } + } + + /** + * Links JBang JDK folder to an already existing JDK path with a link. It checks if the incoming + * version number is the same that the linked JDK has, if not an exception will be raised. + * + * @param path path to the pre-installed JDK. + * @param id id for the new JDK. + */ + public void linkToExistingJdk(String path, String id) { + JdkProvider linked = provider(LinkedJdkProvider.DEFAULT_ID); + if (linked == null) { + return; + } + Path linkPath = Paths.get(path); + if (!Files.isDirectory(linkPath)) { + throw new IllegalArgumentException("Unable to resolve path as directory: " + path); + } + Jdk linkedJdk = linked.getAvailableByIdOrToken(id + "@" + path); + if (linkedJdk == null) { + throw new IllegalArgumentException("Unable to create link to JDK in path: " + path); + } + LOGGER.log(Level.FINE, "Linking JDK: {0} to {1}", new Object[] {id, path}); + linked.install(linkedJdk); + } + + /** + * Returns an installed JDK that matches the requested version or the next available version. + * Returns Optional.empty() if no matching JDK was found; + * + * @param minVersion the minimal version to return + * @param updatableOnly Only return JDKs from updatable providers or not + * @return an optional JDK + */ + private Optional nextInstalledJdk(int minVersion, boolean updatableOnly) { + return listInstalledJdks().stream() + .filter(jdk -> !updatableOnly || jdk.getProvider().canUpdate()) + .filter(jdk -> jdk.getMajorVersion() >= minVersion) + .min(Jdk::compareTo); + } + + /** + * Returns an installed JDK that matches the requested version or the previous available + * version. Returns Optional.empty() if no matching JDK was found; + * + * @param maxVersion the maximum version to return + * @param updatableOnly Only return JDKs from updatable providers or not + * @return an optional JDK + */ + private Optional prevInstalledJdk(int maxVersion, boolean updatableOnly) { + return listInstalledJdks().stream() + .filter(jdk -> !updatableOnly || jdk.getProvider().canUpdate()) + .filter(jdk -> jdk.getMajorVersion() <= maxVersion) + .min(Jdk::compareTo); + } + + public List listAvailableJdks() { + return updatableProviders().stream() + .flatMap(p -> p.listAvailable().stream()) + .collect(Collectors.toList()); + } + + public List listInstalledJdks() { + return providers().stream() + .flatMap(p -> p.listInstalled().stream()) + .sorted() + .collect(Collectors.toList()); + } + + public boolean hasDefaultProvider() { + return defaultProvider != null; + } + + @Nullable + public Jdk getDefaultJdk() { + return hasDefaultProvider() ? + defaultProvider.getInstalledById(DefaultJdkProvider.DEFAULT_ID) : null; + } + + public void setDefaultJdk(Jdk jdk) { + if (hasDefaultProvider()) { + Jdk defJdk = getDefaultJdk(); + // Check if the new jdk exists and isn't the same as the current default + if (jdk.isInstalled() && !jdk.equals(defJdk)) { + // Special syntax for "installing" the default JDK + Jdk newDefJdk = defaultProvider.createJdk(DefaultJdkProvider.DEFAULT_ID, jdk.getHome(), jdk.getVersion()); + defaultProvider.install(newDefJdk); + LOGGER.log(Level.INFO, "Default JDK set to {0}", jdk); + } + } + } + + public void removeDefaultJdk() { + Jdk defJdk = getDefaultJdk(); + if (defJdk != null) { + defJdk.uninstall(); + } + } + + public boolean isCurrentJdkManaged() { + Path currentJdk = Paths.get(System.getProperty("java.home")); + return updatableProviders().stream().anyMatch(p -> p.getInstalledByPath(currentJdk) != null); + } + + @NonNull + static Optional getJdkByVersion(Collection jdks, int version, boolean openVersion) { + Stream s = jdks.stream(); + if (openVersion) { + s = s.filter(jdk -> jdk.getMajorVersion() >= version); + } else { + s = s.filter(jdk -> jdk.getMajorVersion() == version); + } + return s.sorted().findFirst(); + } + + @NonNull + static Optional getJdkById(@NonNull Collection jdks, @NonNull String id) { + return jdks.stream().filter(jdk -> jdk.getId().equals(id)).findFirst(); + } + + @NonNull + static Optional getJdkByPath(@NonNull Collection jdks, @NonNull Path jdkPath) { + return jdks.stream().filter(jdk -> jdk.getHome() != null && jdkPath.startsWith(jdk.getHome())).findFirst(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/JdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/JdkProvider.java new file mode 100644 index 000000000..75b0473db --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkProvider.java @@ -0,0 +1,190 @@ +package dev.jbang.jvm; + +import java.nio.file.Path; +import java.util.*; + +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This interface must be implemented by providers that are able to give access to JDKs installed on + * the user's system. Some providers will also be able to manage those JDKs by installing and + * uninstalling them at the user's request. In those cases the canUpdate() should + * return true. + * + *

The providers deal in JDK identifiers, not in versions. Those identifiers are specific to the + * implementation but should follow two important rules: 1. they must be unique across + * implementations 2. they must start with an integer specifying the main JDK version + */ +public interface JdkProvider { + + default Jdk createJdk(@NonNull String id, @Nullable Path home, @NonNull String version) { + return new Jdk.Default(this, id, home, version); + } + + default String name() { + String nm = getClass().getSimpleName(); + if (nm.endsWith("JdkProvider")) { + return nm.substring(0, nm.length() - 11).toLowerCase(); + } else { + return nm.toLowerCase(); + } + } + + /** + * For providers that can update this returns a set of JDKs that are available for installation. + * Providers might set the home field of the JDK objects if the respective JDK is + * currently installed on the user's system, but only if they can ensure that it's the exact + * same version, otherwise they should just leave the field null. + * + * @return List of Jdk objects + */ + @NonNull + default List listAvailable() { + throw new UnsupportedOperationException( + "Listing available JDKs is not supported by " + getClass().getName()); + } + + /** + * Determines if a JDK matching the given id or token is available for installation by this provider + * and if so returns its respective Jdk object, otherwise it returns null. + * The difference between ids and tokens is that ids are matched exactly against the ids of available + * JDKs, while tokens can use optional provider-specific matching logic. + * NB: In special cases, depending on the provider, this method might actually return Jdk + * objects that are not returned by listAvailable(). + * + * @param idOrToken The id or token to look for + * @return A Jdk object or null + */ + @Nullable + default Jdk getAvailableByIdOrToken(String idOrToken) { + return JdkManager.getJdkById(listAvailable(), idOrToken).orElse(null); + } + + /** + * Returns a set of JDKs that are currently installed on the user's system. + * + * @return List of Jdk objects, possibly empty + */ + @NonNull List listInstalled(); + + /** + * Determines if a JDK of the requested version is currently installed by this provider and if + * so returns its respective Jdk object, otherwise it returns null. If + * openVersion is set to true the method will also return the next installed + * version if the exact version was not found. + * + * @param version The specific JDK version to return + * @param openVersion Return newer version if exact is not available + * @return A Jdk object or null + */ + @Nullable + default Jdk getInstalledByVersion(int version, boolean openVersion) { + return JdkManager.getJdkByVersion(listInstalled(), version, openVersion).orElse(null); + } + + /** + * Determines if the given id refers to a JDK managed by this provider and if so returns its + * respective Jdk object, otherwise it returns null. + * + * @param id The id to look for + * @return A Jdk object or null + */ + @Nullable + default Jdk getInstalledById(@NonNull String id) { + return JdkManager.getJdkById(listInstalled(), id).orElse(null); + } + + /** + * Determines if the given path belongs to a JDK managed by this provider and if so returns its + * respective Jdk object, otherwise it returns null. + * + * @param jdkPath The path to look for + * @return A Jdk object or null + */ + @Nullable + default Jdk getInstalledByPath(@NonNull Path jdkPath) { + return JdkManager.getJdkByPath(listInstalled(), jdkPath).orElse(null); + } + + /** + * For providers that can update this installs the indicated JDK + * + * @param jdk The Jdk object of the JDK to install + * @return A Jdk object + * @throws UnsupportedOperationException if the provider can not update + */ + @NonNull + default Jdk install(@NonNull Jdk jdk) { + throw new UnsupportedOperationException( + "Installing a JDK is not supported by " + getClass().getName()); + } + + /** + * Uninstalls the indicated JDK + * + * @param jdk The Jdk object of the JDK to uninstall + * @throws UnsupportedOperationException if the provider can not update + */ + default void uninstall(@NonNull Jdk jdk) { + throw new UnsupportedOperationException( + "Uninstalling a JDK is not supported by " + getClass().getName()); + } + + /** + * Indicates if the provider can be used or not. This can perform sanity checks like the + * availability of certain package being installed on the system or even if the system is + * running a supported operating system. + * + * @return True if the provider can be used, false otherwise + */ + default boolean canUse() { + return true; + } + + /** + * Indicates if the provider is able to (un)install JDKs or not + * + * @return True if JDKs can be (un)installed, false otherwise + */ + default boolean canUpdate() { + return false; + } + + /** + * This is a special "dummy" provider that can be used to create Jdk objects for + * JDKs that don't seem to belong to any of the known providers but for which we still want an + * object to represent them. + */ + class UnknownJdkProvider implements JdkProvider { + private static final UnknownJdkProvider instance = new UnknownJdkProvider(); + + @NonNull + @Override + public List listInstalled() { + return Collections.emptyList(); + } + + @Nullable + @Override + public Jdk getInstalledById(@NonNull String id) { + return null; + } + + @Nullable + @Override + public Jdk getInstalledByPath(@NonNull Path jdkPath) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkPath); + if (version.isPresent()) { + return createJdk("unknown", jdkPath, version.get()); + } else { + return null; + } + } + + public static Jdk createJdk(Path jdkPath) { + return instance.getInstalledByPath(jdkPath); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseFoldersJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseFoldersJdkProvider.java new file mode 100644 index 000000000..102ad2c44 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseFoldersJdkProvider.java @@ -0,0 +1,125 @@ +package dev.jbang.jvm.jdkproviders; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public abstract class BaseFoldersJdkProvider implements JdkProvider { + protected final Path jdksRoot; + + private static final Logger LOGGER = Logger.getLogger(BaseFoldersJdkProvider.class.getName()); + + protected BaseFoldersJdkProvider(Path jdksRoot) { + this.jdksRoot = jdksRoot; + } + + @Override + public boolean canUse() { + return Files.isDirectory(jdksRoot); + } + + @Override + public @Nullable Jdk getAvailableByIdOrToken(String idOrToken) { + if (isValidId(idOrToken)) { + return JdkProvider.super.getAvailableByIdOrToken(idOrToken); + } else { + return null; + } + } + + @NonNull + @Override + public List listInstalled() { + if (Files.isDirectory(jdksRoot)) { + try (Stream jdkPaths = listJdkPaths()) { + return jdkPaths.map(this::createJdk) + .filter(Objects::nonNull) + .sorted(Jdk::compareTo) + .collect(Collectors.toList()); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); + } + } + return Collections.emptyList(); + } + + @Override + public @Nullable Jdk getInstalledById(@NonNull String id) { + if (isValidId(id)) { + return JdkProvider.super.getInstalledById(id); + } else { + return null; + } + } + + /** + * Returns a path to the requested JDK. This method should never return null and + * should return the path where the requested JDK is either currently installed or where it + * would be installed if it were available. This only needs to be implemented for providers that + * are updatable. + * + * @param jdk The identifier of the JDK to install + * @return A path to the requested JDK + */ + @NonNull + protected Path getJdkPath(@NonNull String jdk) { + return jdksRoot.resolve(jdk); + } + + protected Predicate sameJdk(Path jdkRoot) { + Path release = jdkRoot.resolve("release"); + return (Path p) -> { + try { + return Files.isSameFile(p.resolve("release"), release); + } catch (IOException e) { + return false; + } + }; + } + + protected Stream listJdkPaths() throws IOException { + if (Files.isDirectory(jdksRoot)) { + return Files.list(jdksRoot).filter(this::acceptFolder); + } + return Stream.empty(); + } + + @Nullable + protected Jdk createJdk(Path home) { + String name = home.getFileName().toString(); + Optional version = JavaUtils.resolveJavaVersionStringFromPath(home); + if (version.isPresent() && acceptFolder(home)) { + return createJdk(jdkId(name), home, version.get()); + } + return null; + } + + protected boolean acceptFolder(Path jdkFolder) { + return OsUtils.searchPath("javac", jdkFolder.resolve("bin").toString()) != null; + } + + protected boolean isValidId(String id) { + return id.endsWith("-" + name()); + } + + @NonNull + protected String jdkId(String name) { + return name + "-" + name(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/CurrentJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/CurrentJdkProvider.java new file mode 100644 index 000000000..69bbc56eb --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/CurrentJdkProvider.java @@ -0,0 +1,36 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This JDK provider returns the "current" JDK, which is the JDK that is currently being used to run + * JBang. + */ +public class CurrentJdkProvider implements JdkProvider { + public static final String DEFAULT_ID = "current"; + + @NonNull + @Override + public List listInstalled() { + String jh = System.getProperty("java.home"); + if (jh != null) { + Path jdkHome = Paths.get(jh); + jdkHome = JavaUtils.jre2jdk(jdkHome); + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); + if (version.isPresent()) { + return Collections.singletonList(createJdk(DEFAULT_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/DefaultJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/DefaultJdkProvider.java new file mode 100644 index 000000000..bee1156ff --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/DefaultJdkProvider.java @@ -0,0 +1,53 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; + +/** + * This JDK provider returns the "default" JDK if it was set (using jbang jdk default). + */ +public class DefaultJdkProvider implements JdkProvider { + @NonNull private final Path defaultJdkLink; + + public static final String DEFAULT_ID = "default"; + + public DefaultJdkProvider(@NonNull Path defaultJdkLink) { + this.defaultJdkLink = defaultJdkLink; + } + + @NonNull + @Override + public List listInstalled() { + if (Files.isDirectory(defaultJdkLink)) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(defaultJdkLink); + if (version.isPresent()) { + return Collections.singletonList(createJdk(DEFAULT_ID, defaultJdkLink, version.get())); + } + } + return Collections.emptyList(); + } + + @Override + public @NonNull Jdk install(@NonNull Jdk jdk) { + Jdk defJdk = getInstalledById(DEFAULT_ID); + if (defJdk != null && defJdk.isInstalled() && !jdk.equals(defJdk)) { + uninstall(defJdk); + } + FileUtils.createLink(defaultJdkLink, jdk.getHome()); + return defJdk; + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + FileUtils.deletePath(defaultJdkLink); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/FoojayJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/FoojayJdkProvider.java new file mode 100644 index 000000000..21754a205 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/FoojayJdkProvider.java @@ -0,0 +1,299 @@ +package dev.jbang.jvm.jdkproviders; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.util.*; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * JVM's main JDK provider that can download and install the JDKs provided by the Foojay Disco API. + * They get installed in JBang's cache folder. + */ +public class FoojayJdkProvider extends BaseFoldersJdkProvider { + private static final String FOOJAY_JDK_DOWNLOAD_URL = + "https://api.foojay.io/disco/v3.0/directuris?"; + private static final String FOOJAY_JDK_VERSIONS_URL = + "https://api.foojay.io/disco/v3.0/packages?"; + + private static final Logger LOGGER = Logger.getLogger(FoojayJdkProvider.class.getName()); + + private static class JdkResult { + String java_version; + int major_version; + String release_status; + } + + private static class VersionsResponse { + List result; + } + + public FoojayJdkProvider(Path jdksPath) { + super(jdksPath); + } + + @Override + public boolean canUse() { + return true; + } + + @NonNull + @Override + public List listAvailable() { + try { + List result = new ArrayList<>(); + Consumer addJdk = + version -> { + result.add(createJdk(jdkId(version), null, version)); + }; + String distro = getVendor(); + if (distro == null) { + VersionsResponse res = + NetUtils.readJsonFromUrl( + getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), "temurin"), + VersionsResponse.class); + filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); + res = + NetUtils.readJsonFromUrl( + getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), "aoj"), + VersionsResponse.class); + filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); + } else { + VersionsResponse res = + NetUtils.readJsonFromUrl( + getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), distro), + VersionsResponse.class); + filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); + } + result.sort(Jdk::compareTo); + return Collections.unmodifiableList(result); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + } + return Collections.emptyList(); + } + + // Filter out any EA releases for which a GA with + // the same major version exists + private List filterEA(List jdks) { + Set GAs = + jdks.stream() + .filter(jdk -> jdk.release_status.equals("ga")) + .map(jdk -> jdk.major_version) + .collect(Collectors.toSet()); + + JdkResult[] lastJdk = new JdkResult[] {null}; + return jdks.stream() + .filter( + jdk -> { + if (lastJdk[0] == null + || lastJdk[0].major_version != jdk.major_version + && (jdk.release_status.equals("ga") + || !GAs.contains(jdk.major_version))) { + lastJdk[0] = jdk; + return true; + } else { + return false; + } + }) + .collect(Collectors.toList()); + } + + @Nullable + @Override + public Jdk getInstalledByVersion(int version, boolean openVersion) { + Path jdk = jdksRoot.resolve(Integer.toString(version)); + if (Files.isDirectory(jdk)) { + return createJdk(jdk); + } else if (openVersion) { + return super.getInstalledByVersion(version, true); + } + return null; + } + + @NonNull + @Override + public Jdk install(@NonNull Jdk jdk) { + int version = jdkVersion(jdk.getId()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = getDownloadUrl(version, OsUtils.getOS(), OsUtils.getArch(), getVendor()); + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkDir = getJdkPath(jdk.getId()); + Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); + Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); + FileUtils.deletePath(jdkTmpDir); + FileUtils.deletePath(jdkOldDir); + try { + Path jdkPkg = NetUtils.downloadFromUrl(url); + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + LOGGER.log(Level.FINE, "Unpacking to {0}", jdkDir); + UnpackUtils.unpackJdk(jdkPkg, jdkTmpDir); + if (Files.isDirectory(jdkDir)) { + Files.move(jdkDir, jdkOldDir); + } else if (Files.isSymbolicLink(jdkDir)) { + // This means we have a broken/invalid link + FileUtils.deletePath(jdkDir); + } + Files.move(jdkTmpDir, jdkDir); + FileUtils.deletePath(jdkOldDir); + Optional fullVersion = JavaUtils.resolveJavaVersionStringFromPath(jdkDir); + if (!fullVersion.isPresent()) { + throw new IllegalStateException("Cannot obtain version of recently installed JDK"); + } + return createJdk(jdk.getId(), jdkDir, fullVersion.get()); + } catch (Exception e) { + FileUtils.deletePath(jdkTmpDir); + if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { + try { + Files.move(jdkOldDir, jdkDir); + } catch (IOException ex) { + // Ignore + } + } + String msg = "Required Java version not possible to download or install."; + /* + Jdk defjdk = JdkManager.getJdk(null, false); + if (defjdk != null) { + msg += + " You can run with '--java " + + defjdk.getMajorVersion() + + "' to force using the default installed Java."; + } + */ + LOGGER.log(Level.FINE, msg); + throw new IllegalStateException( + "Unable to download or install JDK version " + version, e); + } + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + Path jdkDir = getJdkPath(jdk.getId()); + FileUtils.deletePath(jdkDir); + } + + @NonNull + @Override + protected Path getJdkPath(@NonNull String jdk) { + return getJdksPath().resolve(Integer.toString(jdkVersion(jdk))); + } + + @Override + public boolean canUpdate() { + return true; + } + + private static String getDownloadUrl( + int version, OsUtils.OS os, OsUtils.Arch arch, String distro) { + return FOOJAY_JDK_DOWNLOAD_URL + getUrlParams(version, os, arch, distro); + } + + private static String getVersionsUrl(OsUtils.OS os, OsUtils.Arch arch, String distro) { + return FOOJAY_JDK_VERSIONS_URL + getUrlParams(null, os, arch, distro); + } + + private static String getUrlParams( + Integer version, OsUtils.OS os, OsUtils.Arch arch, String distro) { + Map params = new HashMap<>(); + if (version != null) { + params.put("version", String.valueOf(version)); + } + + if (distro == null) { + if (version == null || version == 8 || version == 11 || version >= 17) { + distro = "temurin"; + } else { + distro = "aoj"; + } + } + params.put("distro", distro); + + String archiveType; + if (os == OsUtils.OS.windows) { + archiveType = "zip"; + } else { + archiveType = "tar.gz"; + } + params.put("archive_type", archiveType); + + params.put("architecture", arch.name()); + params.put("package_type", "jdk"); + params.put("operating_system", os.name()); + + if (os == OsUtils.OS.windows) { + params.put("libc_type", "c_std_lib"); + } else if (os == OsUtils.OS.mac) { + params.put("libc_type", "libc"); + } else { + params.put("libc_type", "glibc"); + } + + params.put("javafx_bundled", "false"); + params.put("latest", "available"); + params.put("release_status", "ga,ea"); + params.put("directly_downloadable", "true"); + + return urlEncodeUTF8(params); + } + + static String urlEncodeUTF8(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append( + String.format( + "%s=%s", + urlEncodeUTF8(entry.getKey().toString()), + urlEncodeUTF8(entry.getValue().toString()))); + } + return sb.toString(); + } + + static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + @NonNull + public Path getJdksPath() { + return jdksRoot; + } + + @NonNull + @Override + protected String jdkId(String name) { + int majorVersion = JavaUtils.parseJavaVersion(name); + return majorVersion + "-jbang"; + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + // TODO refactor + private static String getVendor() { + return null; + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JavaHomeJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JavaHomeJdkProvider.java new file mode 100644 index 000000000..2c3e6b658 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JavaHomeJdkProvider.java @@ -0,0 +1,34 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This JDK provider detects if a JDK is already available on the system by looking at + * JAVA_HOME environment variable. + */ +public class JavaHomeJdkProvider implements JdkProvider { + public static final String DEFAULT_ID = "javahome"; + + @NonNull + @Override + public List listInstalled() { + Path jdkHome = JavaUtils.getJavaHomeEnv(); + if (jdkHome != null && Files.isDirectory(jdkHome)) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); + if (version.isPresent()) { + return Collections.singletonList(createJdk(DEFAULT_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinkedJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinkedJdkProvider.java new file mode 100644 index 000000000..48d598ba3 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinkedJdkProvider.java @@ -0,0 +1,107 @@ +package dev.jbang.jvm.jdkproviders; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LinkedJdkProvider extends BaseFoldersJdkProvider { + public static final String DEFAULT_ID = "linked"; + + private static final Logger LOGGER = Logger.getLogger(LinkedJdkProvider.class.getName()); + + public LinkedJdkProvider(Path jdksRoot) { + super(jdksRoot); + } + + @Override + public boolean canUse() { + return true; + } + + @Override + public @Nullable Jdk getAvailableByIdOrToken(String idOrToken) { + String[] parts = idOrToken.split("@", 2); + if (parts.length == 2 && isValidPath(parts[1])) { + Path jdkPath = Paths.get(parts[1]); + if (super.acceptFolder(jdkPath)) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkPath); + if (!version.isPresent()) { + throw new IllegalArgumentException( + "Unable to determine Java version in given path: " + jdkPath); + } + return createJdk(idOrToken, null, version.get()); + } + return null; + } else { + return super.getAvailableByIdOrToken(idOrToken); + } + } + + private static boolean isValidPath(String path) { + try { + Paths.get(path); + return true; + } catch (InvalidPathException e) { + return false; + } + } + + @Override + protected boolean acceptFolder(Path jdkFolder) { + return super.acceptFolder(jdkFolder) && FileUtils.isLink(jdkFolder); + } + + @Override + public @NonNull Jdk install(@NonNull Jdk jdk) { + if (jdk.isInstalled()) { + return jdk; + } + // Check this Jdk's id follows our special format + String[] parts = jdk.getId().split("@", 2); + if (parts.length != 2 || !isValidPath(parts[1])) { + throw new IllegalStateException("Invalid linked Jdk id: " + jdk.getId()); + } + String id = parts[0]; + Path jdkPath = Paths.get(parts[1]); + // If there's an existing installed Jdk with the same id, uninstall it + Jdk existingJdk = getInstalledById(jdkId(id)); + if (existingJdk != null && existingJdk.isInstalled() && !jdk.equals(existingJdk)) { + LOGGER.log( + Level.FINE, + "A managed JDK already exists, it must be deleted to make sure linking works"); + uninstall(existingJdk); + } + Path linkPath = getJdkPath(id); + // Remove anything that might be in the way + FileUtils.deletePath(linkPath); + // Now create the new link + FileUtils.createLink(linkPath, jdkPath); + Jdk newJdk = Objects.requireNonNull(createJdk(linkPath)); + LOGGER.log( + Level.INFO, + "JDK {0} has been linked to: {1}", + new Object[] {id, jdkPath}); + return newJdk; + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + if (jdk.isInstalled()) { + FileUtils.deletePath(jdk.getHome()); + LOGGER.log( + Level.INFO, + "JDK {0} has been uninstalled", + new Object[] {jdk.getId()}); + } + } +} diff --git a/src/main/java/dev/jbang/net/jdkproviders/LinuxJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java similarity index 57% rename from src/main/java/dev/jbang/net/jdkproviders/LinuxJdkProvider.java rename to jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java index c8dd49b84..4ea941f7e 100644 --- a/src/main/java/dev/jbang/net/jdkproviders/LinuxJdkProvider.java +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java @@ -1,13 +1,13 @@ -package dev.jbang.net.jdkproviders; +package dev.jbang.jvm.jdkproviders; + +import dev.jbang.jvm.util.FileUtils; +import org.jspecify.annotations.NonNull; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - /** * This JDK provider is intended to detects JDKs that have been installed in * standard location of the users linux distro. @@ -22,37 +22,25 @@ public class LinuxJdkProvider extends BaseFoldersJdkProvider { private static final Path JDKS_ROOT = Paths.get("/usr/lib/jvm"); - @Nonnull - @Override - protected Path getJdksRoot() { - return JDKS_ROOT; - } - - @Nullable - @Override - protected String jdkId(String name) { - return name + "-linux"; - } - - @Override - public boolean canUse() { - return Files.isDirectory(JDKS_ROOT); + public LinuxJdkProvider() { + super(JDKS_ROOT); } @Override protected boolean acceptFolder(Path jdkFolder) { - return super.acceptFolder(jdkFolder) && !isSameFolderSymLink(jdkFolder); + return super.acceptFolder(jdkFolder) && !isSameFolderLink(jdkFolder); } - // Returns true if a path is a symlink to an entry in the same folder - private boolean isSameFolderSymLink(Path jdkFolder) { + // Returns true if a path is a (sym)link to an entry in the same folder + private boolean isSameFolderLink(Path jdkFolder) { Path absFolder = jdkFolder.toAbsolutePath(); - if (Files.isSymbolicLink(absFolder)) { - try { + try { + if (FileUtils.isLink(absFolder)) { Path realPath = absFolder.toRealPath(); return Files.isSameFile(absFolder.getParent(), realPath.getParent()); - } catch (IOException e) { - /* ignore */ } + } + } catch (IOException e) { + /* ignore */ } return false; } diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/PathJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/PathJdkProvider.java new file mode 100644 index 000000000..ef4a97d2f --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/PathJdkProvider.java @@ -0,0 +1,39 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This JDK provider detects if a JDK is already available on the system by first looking at the + * user's PATH. + */ +public class PathJdkProvider implements JdkProvider { + public static final String DEFAULT_ID = "path"; + + @NonNull + @Override + public List listInstalled() { + Path jdkHome = null; + Path javac = OsUtils.searchPath("javac"); + if (javac != null) { + javac = javac.toAbsolutePath(); + jdkHome = javac.getParent().getParent(); + } + if (jdkHome != null) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); + if (version.isPresent()) { + return Collections.singletonList(createJdk(DEFAULT_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/ScoopJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/ScoopJdkProvider.java new file mode 100644 index 000000000..fde0d8403 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/ScoopJdkProvider.java @@ -0,0 +1,54 @@ +package dev.jbang.jvm.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 dev.jbang.jvm.Jdk; +import dev.jbang.jvm.util.OsUtils; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This JDK provider detects any JDKs that have been installed using the Scoop package manager. + * Windows only. + */ +public class ScoopJdkProvider extends BaseFoldersJdkProvider { + private static final Path SCOOP_APPS = + Paths.get(System.getProperty("user.home")).resolve("scoop/apps"); + + public ScoopJdkProvider() { + super(SCOOP_APPS); + } + + @NonNull + @Override + protected Stream listJdkPaths() throws IOException { + if (Files.isDirectory(jdksRoot)) { + try (Stream paths = Files.list(jdksRoot)) { + return paths.filter(p -> p.getFileName().startsWith("openjdk")) + .map(p -> p.resolve("current")); + } + } + return Stream.empty(); + } + + @Nullable + @Override + protected Jdk createJdk(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); + } + + @Override + public boolean canUse() { + return OsUtils.isWindows() && super.canUse(); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/SdkmanJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/SdkmanJdkProvider.java new file mode 100644 index 000000000..feaf756cf --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/SdkmanJdkProvider.java @@ -0,0 +1,17 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** This JDK provider detects any JDKs that have been installed using the SDKMAN package manager. */ +public class SdkmanJdkProvider extends BaseFoldersJdkProvider { + private static final Path JDKS_ROOT = + Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); + + public SdkmanJdkProvider() { + super(JDKS_ROOT); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/FileHttpCacheStorage.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileHttpCacheStorage.java new file mode 100644 index 000000000..7c7187903 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileHttpCacheStorage.java @@ -0,0 +1,73 @@ +package dev.jbang.jvm.util; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheStorage; +import org.apache.http.client.cache.HttpCacheUpdateCallback; +import org.apache.http.client.cache.HttpCacheUpdateException; +import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; + +public class FileHttpCacheStorage implements HttpCacheStorage { + + private final Path cacheDir; + private final DefaultHttpCacheEntrySerializer serializer; + + public FileHttpCacheStorage(Path cacheDir) { + this.cacheDir = cacheDir; + this.serializer = new DefaultHttpCacheEntrySerializer(); + try { + Files.createDirectories(cacheDir); + } catch (IOException e) { + throw new RuntimeException("Failed to create cache directory", e); + } + } + + @Override + public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException { + Path filePath = cacheDir.resolve(encodeKey(key)); + try (OutputStream os = Files.newOutputStream(filePath); + BufferedOutputStream bos = new BufferedOutputStream(os)) { + serializer.writeTo(entry, bos); + } + } + + @Override + public synchronized HttpCacheEntry getEntry(String key) throws IOException { + Path filePath = cacheDir.resolve(encodeKey(key)); + if (Files.exists(filePath)) { + try (InputStream is = Files.newInputStream(filePath); + BufferedInputStream bis = new BufferedInputStream(is)) { + return serializer.readFrom(bis); + } + } + return null; + } + + @Override + public synchronized void removeEntry(String key) throws IOException { + Path filePath = cacheDir.resolve(encodeKey(key)); + Files.deleteIfExists(filePath); + } + + @Override + public synchronized void updateEntry(String key, HttpCacheUpdateCallback callback) + throws IOException, HttpCacheUpdateException { + Path filePath = cacheDir.resolve(encodeKey(key)); + HttpCacheEntry existingEntry = null; + if (Files.exists(filePath)) { + try (InputStream is = Files.newInputStream(filePath); + BufferedInputStream bis = new BufferedInputStream(is)) { + existingEntry = serializer.readFrom(bis); + } + } + HttpCacheEntry updatedEntry = callback.update(existingEntry); + putEntry(key, updatedEntry); + } + + private String encodeKey(String key) { + // You can use more sophisticated encoding if necessary + return key.replaceAll("[^a-zA-Z0-9-_]", "_"); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/FileUtils.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileUtils.java new file mode 100644 index 000000000..107ebc1ca --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileUtils.java @@ -0,0 +1,122 @@ +package dev.jbang.jvm.util; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +public class FileUtils { + private static final Logger LOGGER = Logger.getLogger(JavaUtils.class.getName()); + + public static void createLink(Path link, Path target) { + if (!Files.exists(link)) { + // On Windows we use junction for directories because their + // creation doesn't require any special privileges. + if (OsUtils.isWindows() && Files.isDirectory(target)) { + if (createJunction(link, target.toAbsolutePath())) { + return; + } + } else { + if (createSymbolicLink(link, target.toAbsolutePath())) { + return; + } + } + throw new IllegalStateException("Failed to create link " + link + " -> " + target); + } + } + + private static boolean createSymbolicLink(Path link, Path target) { + try { + mkdirs(link.getParent()); + Files.createSymbolicLink(link, target); + return true; + } catch (IOException e) { + if (OsUtils.isWindows() + && e instanceof AccessDeniedException + && e.getMessage().contains("privilege")) { + LOGGER.log( + Level.INFO, + "Creation of symbolic link failed {0} -> {1}}", + new Object[] {link, target}); + LOGGER.info( + "This is a known issue with trying to create symbolic links on Windows."); + LOGGER.info("See the information available at the link below for a solution:"); + LOGGER.info( + "https://www.jbang.dev/documentation/guide/latest/usage.html#usage-on-windows"); + } + LOGGER.log(Level.FINE, "Failed to create symbolic link " + link + " -> " + target, e); + } + return false; + } + + private static boolean createJunction(Path link, Path target) { + if (!Files.exists(link) && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) { + // We automatically remove broken links + deletePath(link); + } + mkdirs(link.getParent()); + return OsUtils.runCommand( + "cmd.exe", "/c", "mklink", "/j", link.toString(), target.toString()) + != null; + } + + /** + * Returns true if the final part of the path is a symbolic link. + * @param path The path to check + * @return true if the final part of the path is a symbolic link + */ + public static boolean isLink(Path path) { + try { + Path parent = path.toAbsolutePath().getParent().toRealPath(); + Path absPath = parent.resolve(path.getFileName()); + return !absPath.toRealPath().equals(absPath.toRealPath(LinkOption.NOFOLLOW_LINKS)); + } catch (IOException e) { + return false; + } + } + + public static void mkdirs(Path p) { + try { + Files.createDirectories(p); + } catch (IOException e) { + throw new IllegalStateException("Failed to create directory " + p, e); + } + } + + public static void deletePath(Path path) { + try { + if (isLink(path)) { + LOGGER.log(Level.FINE, "Deleting link {0}", path); + Files.delete(path); + } else if (Files.isDirectory(path)) { + LOGGER.log(Level.FINE, "Deleting folder {0}", path); + try (Stream s = Files.walk(path)) { + s.sorted(Comparator.reverseOrder()) + .forEach( + f -> { + try { + Files.delete(f); + } catch (IOException e) { + throw new IllegalStateException("Failed to delete " + f, e); + } + }); + } + } else if (Files.exists(path)) { + LOGGER.log(Level.FINE, "Deleting file {0}", path); + Files.delete(path); + } else if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + LOGGER.log(Level.FINE, "Deleting broken link {0}", path); + Files.delete(path); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to delete " + path, e); + } + } + + public static String extension(String name) { + int p = name.lastIndexOf('.'); + return p > 0 ? name.substring(p + 1) : ""; + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/JavaUtils.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/JavaUtils.java new file mode 100644 index 000000000..042dd666a --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/JavaUtils.java @@ -0,0 +1,130 @@ +package dev.jbang.jvm.util; + +import static java.lang.System.getenv; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +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; + +public class JavaUtils { + + private static final Pattern javaVersionPattern = Pattern.compile("\"([^\"]+)\""); + + private static final Logger LOGGER = Logger.getLogger(JavaUtils.class.getName()); + + public static boolean isRequestedVersion(String rv) { + return rv.matches("\\d+[+]?"); + } + + public static int minRequestedVersion(String rv) { + return Integer.parseInt(isOpenVersion(rv) ? rv.substring(0, rv.length() - 1) : rv); + } + + public static boolean isOpenVersion(String version) { + return version.endsWith("+"); + } + + public static int parseJavaVersion(String version) { + if (version != null) { + try { + String[] nums = version.split("[-.+]"); + String num = nums.length > 1 && nums[0].equals("1") ? nums[1] : nums[0]; + return Integer.parseInt(num); + } catch (NumberFormatException ex) { + // Ignore + } + } + return 0; + } + + public static Optional resolveJavaVersionFromPath(Path home) { + return resolveJavaVersionStringFromPath(home).map(JavaUtils::parseJavaVersion); + } + + public static Optional resolveJavaVersionStringFromPath(Path home) { + Optional res = readJavaVersionStringFromReleaseFile(home); + if (!res.isPresent()) { + res = readJavaVersionStringFromJavaCommand(home); + } + return res; + } + + public static Optional readJavaVersionStringFromReleaseFile(Path home) { + try (Stream lines = Files.lines(home.resolve("release"))) { + return lines.filter( + l -> + l.startsWith("JAVA_VERSION=") + || l.startsWith("JAVA_RUNTIME_VERSION=")) + .map(JavaUtils::parseJavaOutput) + .findAny(); + } catch (IOException e) { + LOGGER.fine("Unable to read 'release' file in path: " + home); + return Optional.empty(); + } + } + + public static Optional readJavaVersionStringFromJavaCommand(Path home) { + Optional res; + Path javaCmd = OsUtils.searchPath("java", home.resolve("bin").toString()); + if (javaCmd != null) { + String output = OsUtils.runCommand(javaCmd.toString(), "-version"); + res = Optional.ofNullable(parseJavaOutput(output)); + } else { + res = Optional.empty(); + } + if (!res.isPresent()) { + LOGGER.log(Level.FINE, "Unable to obtain version from: '{0} -version'", javaCmd); + } + return res; + } + + public static String parseJavaOutput(String output) { + if (output != null) { + Matcher m = javaVersionPattern.matcher(output); + if (m.find() && m.groupCount() == 1) { + return m.group(1); + } + } + return null; + } + + /** + * Returns the Path to JAVA_HOME + * + * @return A Path pointing to JAVA_HOME or null if it isn't defined + */ + public static Path getJavaHomeEnv() { + if (getenv("JAVA_HOME") != null) { + return Paths.get(getenv("JAVA_HOME")); + } else { + return null; + } + } + + /** + * Method takes the given path which might point to a Java home directory or to the `jre` + * directory inside it and makes sure to return the path to the actual home directory. + */ + public static Path jre2jdk(Path jdkHome) { + // Detect if the current JDK is a JRE and try to find the real home + if (!Files.isRegularFile(jdkHome.resolve("release"))) { + Path jh = jdkHome.toAbsolutePath(); + try { + jh = jh.toRealPath(); + } catch (IOException e) { + // Ignore error + } + if (jh.endsWith("jre") && Files.isRegularFile(jh.getParent().resolve("release"))) { + jdkHome = jh.getParent(); + } + } + return jdkHome; + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/NetUtils.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/NetUtils.java new file mode 100644 index 000000000..8cec3b853 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/NetUtils.java @@ -0,0 +1,115 @@ +package dev.jbang.jvm.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Function; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClientBuilder; + +public class NetUtils { + + public static final RequestConfig DEFAULT_REQUEST_CONFIG = + RequestConfig.custom() + .setConnectionRequestTimeout(10000) + .setConnectTimeout(10000) + .setSocketTimeout(30000) + .build(); + + public static T readJsonFromUrl(String url, Class klass) throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return readJsonFromUrl(builder, url, klass); + } + + public static T readJsonFromUrl(HttpClientBuilder builder, String url, Class klass) + throws IOException { + return requestUrl(builder, url, response -> handleJsonResult(klass, response)); + } + + public static Path downloadFromUrl(String url) throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return downloadFromUrl(builder, url); + } + + public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { + return requestUrl(builder, url, NetUtils::handleDownloadResult); + } + + public static HttpClientBuilder createDefaultHttpClientBuilder() { + CacheConfig cacheConfig = CacheConfig.custom().setMaxCacheEntries(1000).build(); + + FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(Paths.get("http-cache")); + + // return HttpClientBuilder.create().setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); + return CachingHttpClientBuilder.create() + .setCacheConfig(cacheConfig) + .setHttpCacheStorage(cacheStorage) + .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); + } + + public static T requestUrl( + HttpClientBuilder builder, String url, Function responseHandler) + throws IOException { + try (CloseableHttpClient httpClient = builder.build()) { + HttpGet httpGet = new HttpGet(url); + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode != 200) { + throw new IOException( + "Failed to read from URL: " + + url + + ", response code: #" + + responseCode); + } + HttpEntity entity = response.getEntity(); + if (entity == null) { + throw new IOException("Failed to read from URL: " + url + ", no content"); + } + return responseHandler.apply(response); + } + } catch (UncheckedIOException e) { + throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); + } + } + + private static T handleJsonResult(Class klass, HttpResponse response) { + try { + String mimeType = ContentType.getOrDefault(response.getEntity()).getMimeType(); + if (!mimeType.equals("application/json")) { + throw new IOException("Unexpected MIME type: " + mimeType); + } + HttpEntity entity = response.getEntity(); + try (InputStream is = entity.getContent()) { + Gson parser = new GsonBuilder().create(); + return parser.fromJson(new InputStreamReader(is), klass); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static Path handleDownloadResult(HttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + try (InputStream is = entity.getContent()) { + // TODO implement + return null; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/OsUtils.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/OsUtils.java new file mode 100644 index 000000000..94e372ed1 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/OsUtils.java @@ -0,0 +1,181 @@ +package dev.jbang.jvm.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OsUtils { + + private static final Logger LOGGER = Logger.getLogger(OsUtils.class.getName()); + + public enum OS { + linux, + mac, + windows, + aix, + unknown + } + + public enum Arch { + x32, + x64, + aarch64, + arm, + arm64, + ppc64, + ppc64le, + s390x, + riscv64, + unknown + } + + public static OS getOS() { + String os = + System.getProperty("os.name") + .toLowerCase(Locale.ENGLISH) + .replaceAll("[^a-z0-9]+", ""); + if (os.startsWith("mac") || os.startsWith("osx")) { + return OS.mac; + } else if (os.startsWith("linux")) { + return OS.linux; + } else if (os.startsWith("win")) { + return OS.windows; + } else if (os.startsWith("aix")) { + return OS.aix; + } else { + LOGGER.log(Level.FINE, "Unknown OS: {0}", os); + return OS.unknown; + } + } + + public static Arch getArch() { + String arch = + System.getProperty("os.arch") + .toLowerCase(Locale.ENGLISH) + .replaceAll("[^a-z0-9]+", ""); + if (arch.matches("^(x8664|amd64|ia32e|em64t|x64)$")) { + return Arch.x64; + } else if (arch.matches("^(x8632|x86|i[3-6]86|ia32|x32)$")) { + return Arch.x32; + } else if (arch.matches("^(aarch64)$")) { + return Arch.aarch64; + } else if (arch.matches("^(arm)$")) { + return Arch.arm; + } else if (arch.matches("^(ppc64)$")) { + return Arch.ppc64; + } else if (arch.matches("^(ppc64le)$")) { + return Arch.ppc64le; + } else if (arch.matches("^(s390x)$")) { + return Arch.s390x; + } else if (arch.matches("^(arm64)$")) { + return Arch.arm64; + } else if (arch.matches("^(riscv64)$")) { + return Arch.riscv64; + } else { + LOGGER.log(Level.FINE, "Unknown Arch: {0}", arch); + return Arch.unknown; + } + } + + public static boolean isWindows() { + return getOS() == OS.windows; + } + + public static boolean isMac() { + return getOS() == OS.mac; + } + + /** + * Searches the locations defined by PATH for the given executable + * + * @param cmd The name of the executable to look for + * @return A Path to the executable, if found, null otherwise + */ + public static Path searchPath(String cmd) { + String envPath = System.getenv("PATH"); + envPath = envPath != null ? envPath : ""; + return searchPath(cmd, envPath); + } + + /** + * Searches the locations defined by `paths` for the given executable + * + * @param cmd The name of the executable to look for + * @param paths A string containing the paths to search + * @return A Path to the executable, if found, null otherwise + */ + public static Path searchPath(String cmd, String paths) { + return Arrays.stream(paths.split(File.pathSeparator)) + .map(dir -> Paths.get(dir).resolve(cmd)) + .flatMap(OsUtils::executables) + .filter(OsUtils::isExecutable) + .findFirst() + .orElse(null); + } + + private static Stream executables(Path base) { + if (isWindows()) { + return Stream.of( + Paths.get(base.toString() + ".exe"), + Paths.get(base.toString() + ".bat"), + Paths.get(base.toString() + ".cmd"), + Paths.get(base.toString() + ".ps1")); + } else { + return Stream.of(base); + } + } + + private static boolean isExecutable(Path file) { + if (Files.isRegularFile(file)) { + if (isWindows()) { + String nm = file.getFileName().toString().toLowerCase(); + return nm.endsWith(".exe") + || nm.endsWith(".bat") + || nm.endsWith(".cmd") + || nm.endsWith(".ps1"); + } else { + return Files.isExecutable(file); + } + } + return false; + } + + /** + * Runs the given command + arguments and returns its output (both stdout and stderr) as a + * string + * + * @param cmd The command to execute + * @return The output of the command or null if anything went wrong + */ + public static String runCommand(String... cmd) { + try { + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + Process p = pb.start(); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + String cmdOutput = br.lines().collect(Collectors.joining("\n")); + int exitCode = p.waitFor(); + if (exitCode == 0) { + return cmdOutput; + } else { + LOGGER.log( + Level.FINE, + "Command failed: #{0} - {1}", + new Object[] {exitCode, cmdOutput}); + } + } catch (IOException | InterruptedException ex) { + LOGGER.log(Level.FINE, "Error running: " + String.join(" ", cmd), ex); + } + return null; + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/util/UnpackUtils.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/UnpackUtils.java new file mode 100644 index 000000000..6ef6993e5 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/UnpackUtils.java @@ -0,0 +1,219 @@ +package dev.jbang.jvm.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.util.*; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; + +public class UnpackUtils { + + public static void unpackJdk(Path archive, Path outputDir) throws IOException { + String name = archive.toString().toLowerCase(Locale.ENGLISH); + Path selectFolder = OsUtils.isMac() ? Paths.get("Contents/Home") : null; + if (name.endsWith(".zip")) { + unzip(archive, outputDir, true, selectFolder, UnpackUtils::defaultZipEntryCopy); + } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) { + untargz(archive, outputDir, true, selectFolder); + } + } + + public static void unpack(Path archive, Path outputDir) throws IOException { + unpack(archive, outputDir, false); + } + + public static void unpack(Path archive, Path outputDir, boolean stripRootFolder) + throws IOException { + unpack(archive, outputDir, stripRootFolder, null); + } + + public static void unpack( + Path archive, Path outputDir, boolean stripRootFolder, Path selectFolder) + throws IOException { + String name = archive.toString().toLowerCase(Locale.ENGLISH); + if (name.endsWith(".zip") || name.endsWith(".jar")) { + unzip( + archive, + outputDir, + stripRootFolder, + selectFolder, + UnpackUtils::defaultZipEntryCopy); + } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) { + untargz(archive, outputDir, stripRootFolder, selectFolder); + } else { + throw new IllegalArgumentException( + "Unsupported archive format: " + FileUtils.extension(archive.toString())); + } + } + + public static void unzip( + Path zip, + Path outputDir, + boolean stripRootFolder, + Path selectFolder, + ExistingZipFileHandler onExisting) + throws IOException { + try (ZipFile zipFile = new ZipFile(zip.toFile())) { + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + ZipArchiveEntry zipEntry = entries.nextElement(); + Path entry = Paths.get(zipEntry.getName()); + if (stripRootFolder) { + if (entry.getNameCount() == 1) { + continue; + } + entry = entry.subpath(1, entry.getNameCount()); + } + if (selectFolder != null) { + if (!entry.startsWith(selectFolder) || entry.equals(selectFolder)) { + continue; + } + entry = entry.subpath(selectFolder.getNameCount(), entry.getNameCount()); + } + entry = outputDir.resolve(entry).normalize(); + if (!entry.startsWith(outputDir)) { + throw new IOException( + "Entry is outside of the target dir: " + zipEntry.getName()); + } + if (zipEntry.isDirectory()) { + Files.createDirectories(entry); + } else if (zipEntry.isUnixSymlink()) { + Scanner s = new Scanner(zipFile.getInputStream(zipEntry)).useDelimiter("\\A"); + String result = s.hasNext() ? s.next() : ""; + Files.createSymbolicLink(entry, Paths.get(result)); + } else { + if (!Files.isDirectory(entry.getParent())) { + Files.createDirectories(entry.getParent()); + } + if (Files.isRegularFile(entry)) { + onExisting.handle(zipFile, zipEntry, entry); + } else { + defaultZipEntryCopy(zipFile, zipEntry, entry); + } + } + } + } + } + + public interface ExistingZipFileHandler { + void handle(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) throws IOException; + } + + public static void defaultZipEntryCopy(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) + throws IOException { + try (InputStream zis = zipFile.getInputStream(zipEntry)) { + Files.copy(zis, outFile, StandardCopyOption.REPLACE_EXISTING); + } + int mode = zipEntry.getUnixMode(); + if (mode != 0 && !OsUtils.isWindows()) { + Set permissions = + PosixFilePermissionSupport.toPosixFilePermissions(mode); + Files.setPosixFilePermissions(outFile, permissions); + } + } + + public static void untargz( + Path targz, Path outputDir, boolean stripRootFolder, Path selectFolder) + throws IOException { + try (TarArchiveInputStream tarArchiveInputStream = + new TarArchiveInputStream( + new GzipCompressorInputStream( + Files.newInputStream(targz.toFile().toPath())))) { + TarArchiveEntry targzEntry; + while ((targzEntry = tarArchiveInputStream.getNextEntry()) != null) { + Path entry = Paths.get(targzEntry.getName()).normalize(); + if (stripRootFolder) { + if (entry.getNameCount() == 1) { + continue; + } + entry = entry.subpath(1, entry.getNameCount()); + } + if (selectFolder != null) { + if (!entry.startsWith(selectFolder) || entry.equals(selectFolder)) { + continue; + } + entry = entry.subpath(selectFolder.getNameCount(), entry.getNameCount()); + } + entry = outputDir.resolve(entry).normalize(); + if (!entry.startsWith(outputDir)) { + throw new IOException( + "Entry is outside of the target dir: " + targzEntry.getName()); + } + if (targzEntry.isDirectory()) { + Files.createDirectories(entry); + } else { + if (!Files.isDirectory(entry.getParent())) { + Files.createDirectories(entry.getParent()); + } + Files.copy(tarArchiveInputStream, entry, StandardCopyOption.REPLACE_EXISTING); + int mode = targzEntry.getMode(); + if (mode != 0 && !OsUtils.isWindows()) { + Set permissions = + PosixFilePermissionSupport.toPosixFilePermissions(mode); + Files.setPosixFilePermissions(entry, permissions); + } + } + } + } + } +} + +class PosixFilePermissionSupport { + + private static final int OWNER_READ_FILEMODE = 0b100_000_000; + private static final int OWNER_WRITE_FILEMODE = 0b010_000_000; + private static final int OWNER_EXEC_FILEMODE = 0b001_000_000; + + private static final int GROUP_READ_FILEMODE = 0b000_100_000; + private static final int GROUP_WRITE_FILEMODE = 0b000_010_000; + private static final int GROUP_EXEC_FILEMODE = 0b000_001_000; + + private static final int OTHERS_READ_FILEMODE = 0b000_000_100; + private static final int OTHERS_WRITE_FILEMODE = 0b000_000_010; + private static final int OTHERS_EXEC_FILEMODE = 0b000_000_001; + + private PosixFilePermissionSupport() {} + + static Set toPosixFilePermissions(int octalFileMode) { + Set permissions = new LinkedHashSet<>(); + // Owner + if ((octalFileMode & OWNER_READ_FILEMODE) == OWNER_READ_FILEMODE) { + permissions.add(PosixFilePermission.OWNER_READ); + } + if ((octalFileMode & OWNER_WRITE_FILEMODE) == OWNER_WRITE_FILEMODE) { + permissions.add(PosixFilePermission.OWNER_WRITE); + } + if ((octalFileMode & OWNER_EXEC_FILEMODE) == OWNER_EXEC_FILEMODE) { + permissions.add(PosixFilePermission.OWNER_EXECUTE); + } + // Group + if ((octalFileMode & GROUP_READ_FILEMODE) == GROUP_READ_FILEMODE) { + permissions.add(PosixFilePermission.GROUP_READ); + } + if ((octalFileMode & GROUP_WRITE_FILEMODE) == GROUP_WRITE_FILEMODE) { + permissions.add(PosixFilePermission.GROUP_WRITE); + } + if ((octalFileMode & GROUP_EXEC_FILEMODE) == GROUP_EXEC_FILEMODE) { + permissions.add(PosixFilePermission.GROUP_EXECUTE); + } + // Others + if ((octalFileMode & OTHERS_READ_FILEMODE) == OTHERS_READ_FILEMODE) { + permissions.add(PosixFilePermission.OTHERS_READ); + } + if ((octalFileMode & OTHERS_WRITE_FILEMODE) == OTHERS_WRITE_FILEMODE) { + permissions.add(PosixFilePermission.OTHERS_WRITE); + } + if ((octalFileMode & OTHERS_EXEC_FILEMODE) == OTHERS_EXEC_FILEMODE) { + permissions.add(PosixFilePermission.OTHERS_EXECUTE); + } + return permissions; + } +} diff --git a/settings.gradle b/settings.gradle index b3b191865..4e3449487 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,6 @@ plugins { id "com.gradle.enterprise" } - // Configuration of com.gradle.enterprise (build scan) plugin gradleEnterprise { buildScan { @@ -20,3 +19,5 @@ gradleEnterprise { //publishAlways() } } + +include 'jdkmanager' diff --git a/src/main/java/dev/jbang/Cache.java b/src/main/java/dev/jbang/Cache.java index 2747dd44b..8a931b441 100644 --- a/src/main/java/dev/jbang/Cache.java +++ b/src/main/java/dev/jbang/Cache.java @@ -5,8 +5,9 @@ import java.nio.file.Path; import dev.jbang.cli.ExitException; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkManager; +import dev.jbang.util.JavaUtil; import dev.jbang.util.Util; public class Cache { @@ -23,11 +24,12 @@ static void setupCache(Path dir) { public static void clearCache(CacheClass... classes) { for (CacheClass cc : classes) { Util.infoMsg("Clearing cache for " + cc.name()); - if (cc == CacheClass.jdks && Util.isWindows() && JdkManager.isCurrentJdkManaged()) { + JdkManager jdkMan = JavaUtil.defaultJdkManager(); + if (cc == CacheClass.jdks && Util.isWindows() && jdkMan.isCurrentJdkManaged()) { // We're running using a managed JDK on Windows so we can't just delete the // entire folder! - for (JdkProvider.Jdk jdk : JdkManager.listInstalledJdks()) { - JdkManager.uninstallJdk(jdk); + for (Jdk jdk : jdkMan.listInstalledJdks()) { + jdkMan.uninstallJdk(jdk); } } if (cc == CacheClass.deps) { diff --git a/src/main/java/dev/jbang/Main.java b/src/main/java/dev/jbang/Main.java index aa654c795..64cfd5662 100644 --- a/src/main/java/dev/jbang/Main.java +++ b/src/main/java/dev/jbang/Main.java @@ -1,7 +1,9 @@ package dev.jbang; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.logging.LogManager; import java.util.stream.Collectors; import dev.jbang.cli.JBang; @@ -10,6 +12,12 @@ public class Main { public static void main(String... args) { + try { + // Set up JUL logging so the output looks like JBang output + LogManager.getLogManager().readConfiguration(Main.class.getResourceAsStream("/logging.properties")); + } catch (IOException e) { + // Ignore + } CommandLine cli = JBang.getCommandLine(); args = handleDefaultRun(cli.getCommandSpec(), args); int exitcode = cli.execute(args); diff --git a/src/main/java/dev/jbang/Settings.java b/src/main/java/dev/jbang/Settings.java index fcfd1a97b..b5d2bff85 100644 --- a/src/main/java/dev/jbang/Settings.java +++ b/src/main/java/dev/jbang/Settings.java @@ -17,7 +17,7 @@ public class Settings { public static final String TRUSTED_SOURCES_JSON = "trusted-sources.json"; public static final String DEPENDENCY_CACHE_JSON = "dependency_cache.json"; - public static final String CURRENT_JDK = "currentjdk"; + public static final String DEFAULT_JDK = "currentjdk"; public static final String JBANG_DOT_DIR = ".jbang"; public static final String BIN_DIR = "bin"; public static final String EDITOR_DIR = "editor"; @@ -68,8 +68,8 @@ public static Path getConfigDir() { return getConfigDir(true); } - public static Path getCurrentJdkDir() { - return getConfigDir(true).resolve(CURRENT_JDK); + public static Path getDefaultJdkDir() { + return getConfigDir(true).resolve(DEFAULT_JDK); } public static Path getConfigBinDir() { diff --git a/src/main/java/dev/jbang/cli/Alias.java b/src/main/java/dev/jbang/cli/Alias.java index 4f2b1e2ac..6394c5a5b 100644 --- a/src/main/java/dev/jbang/cli/Alias.java +++ b/src/main/java/dev/jbang/cli/Alias.java @@ -149,7 +149,8 @@ ProjectBuilder createProjectBuilder() { .compileOptions(buildMixin.compileOptions) .nativeImage(nativeMixin.nativeImage) .nativeOptions(nativeMixin.nativeOptions) - .enablePreview(enablePreviewRequested); + .enablePreview(enablePreviewRequested) + .jdkManager(buildMixin.jdkProvidersMixin.getJdkManager()); Path cat = getCatalog(false); if (cat != null) { pb.catalog(cat.toFile()); diff --git a/src/main/java/dev/jbang/cli/App.java b/src/main/java/dev/jbang/cli/App.java index 3ff3ca910..1ac557cec 100644 --- a/src/main/java/dev/jbang/cli/App.java +++ b/src/main/java/dev/jbang/cli/App.java @@ -1,5 +1,7 @@ package dev.jbang.cli; +import static dev.jbang.util.JavaUtil.defaultJdkManager; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -21,8 +23,7 @@ import dev.jbang.Settings; import dev.jbang.catalog.CatalogUtil; import dev.jbang.dependencies.DependencyUtil; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; import dev.jbang.source.Project; import dev.jbang.source.ProjectBuilder; import dev.jbang.util.CommandBuffer; @@ -70,9 +71,6 @@ class AppInstall extends BaseCommand { @CommandLine.Mixin NativeMixin nativeMixin; - @CommandLine.Mixin - JdkProvidersMixin jdkProvidersMixin; - @CommandLine.Mixin RunMixin runMixin; @@ -120,7 +118,6 @@ private List collectRunOptions() { opts.addAll(buildMixin.opts()); opts.addAll(dependencyInfoMixin.opts()); opts.addAll(nativeMixin.opts()); - opts.addAll(jdkProvidersMixin.opts()); opts.addAll(runMixin.opts()); if (Boolean.TRUE.equals(enablePreviewRequested)) { opts.add("--enable-preview"); @@ -401,7 +398,7 @@ public static boolean needsSetup() { */ public static boolean guessWithJava() { boolean withJava; - JdkProvider.Jdk defJdk = JdkManager.getJdk(null, false); + Jdk defJdk = defaultJdkManager().getJdk(null, false); String javaHome = System.getenv("JAVA_HOME"); Path javacCmd = Util.searchPath("javac"); withJava = defJdk != null @@ -415,12 +412,12 @@ public static boolean guessWithJava() { public static int setup(boolean withJava, boolean force, boolean chatty) { Path jdkHome = null; if (withJava) { - JdkProvider.Jdk defJdk = JdkManager.getDefaultJdk(); + Jdk defJdk = defaultJdkManager().getDefaultJdk(); if (defJdk == null) { Util.infoMsg("No default JDK set, use 'jbang jdk default ' to set one."); return EXIT_UNEXPECTED_STATE; } - jdkHome = Settings.getCurrentJdkDir(); + jdkHome = Settings.getDefaultJdkDir(); } Path binDir = Settings.getConfigBinDir(); diff --git a/src/main/java/dev/jbang/cli/BaseBuildCommand.java b/src/main/java/dev/jbang/cli/BaseBuildCommand.java index 8aaafa53f..511862eef 100644 --- a/src/main/java/dev/jbang/cli/BaseBuildCommand.java +++ b/src/main/java/dev/jbang/cli/BaseBuildCommand.java @@ -23,9 +23,6 @@ public abstract class BaseBuildCommand extends BaseCommand { @CommandLine.Mixin NativeMixin nativeMixin; - @CommandLine.Mixin - JdkProvidersMixin jdkProvidersMixin; - @CommandLine.Option(names = { "--build-dir" }, description = "Use given directory for build results") Path buildDir; @@ -52,7 +49,8 @@ protected ProjectBuilder createBaseProjectBuilder() { .manifestOptions(buildMixin.manifestOptions) .nativeImage(nativeMixin.nativeImage) .nativeOptions(nativeMixin.nativeOptions) - .enablePreview(enablePreviewRequested); + .enablePreview(enablePreviewRequested) + .jdkManager(buildMixin.jdkProvidersMixin.getJdkManager()); // NB: Do not put `.mainClass(buildMixin.main)` here } diff --git a/src/main/java/dev/jbang/cli/Build.java b/src/main/java/dev/jbang/cli/Build.java index 98b8925ec..844e2879f 100644 --- a/src/main/java/dev/jbang/cli/Build.java +++ b/src/main/java/dev/jbang/cli/Build.java @@ -14,7 +14,6 @@ public class Build extends BaseBuildCommand { @Override public Integer doCall() throws IOException { scriptMixin.validate(); - jdkProvidersMixin.initJdkProviders(); ProjectBuilder pb = createProjectBuilderForBuild(); Project prj = pb.build(scriptMixin.scriptOrFile); @@ -24,7 +23,6 @@ public Integer doCall() throws IOException { } ProjectBuilder createProjectBuilderForBuild() { - return createBaseProjectBuilder() - .mainClass(buildMixin.main); + return createBaseProjectBuilder().mainClass(buildMixin.main); } } diff --git a/src/main/java/dev/jbang/cli/BuildMixin.java b/src/main/java/dev/jbang/cli/BuildMixin.java index e6677050b..3b4d2f7f4 100644 --- a/src/main/java/dev/jbang/cli/BuildMixin.java +++ b/src/main/java/dev/jbang/cli/BuildMixin.java @@ -4,11 +4,17 @@ import java.util.List; import java.util.Map; +import dev.jbang.jvm.Jdk; +import dev.jbang.source.Project; + import picocli.CommandLine; public class BuildMixin { public String javaVersion; + @CommandLine.Mixin + JdkProvidersMixin jdkProvidersMixin; + @CommandLine.Option(names = { "-j", "--java" }, description = "JDK version to use for running the script.") void setJavaVersion(String javaVersion) { @@ -33,6 +39,14 @@ void setJavaVersion(String javaVersion) { @CommandLine.Option(names = { "--manifest" }, parameterConsumer = KeyValueConsumer.class) public Map manifestOptions; + public Jdk getProjectJdk(Project project) { + Jdk jdk = project.projectJdk(); + if (javaVersion != null) { + jdk = jdkProvidersMixin.getJdkManager().getOrInstallJdk(javaVersion); + } + return jdk; + } + public List opts() { List opts = new ArrayList<>(); if (javaVersion != null) { @@ -59,6 +73,7 @@ public List opts() { opts.add(e.getKey() + "=" + e.getValue()); } } + opts.addAll(jdkProvidersMixin.opts()); return opts; } } diff --git a/src/main/java/dev/jbang/cli/Export.java b/src/main/java/dev/jbang/cli/Export.java index 15485802c..5102e41bb 100644 --- a/src/main/java/dev/jbang/cli/Export.java +++ b/src/main/java/dev/jbang/cli/Export.java @@ -89,7 +89,8 @@ protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) { .javaVersion(exportMixin.buildMixin.javaVersion) .mainClass(exportMixin.buildMixin.main) .moduleName(exportMixin.buildMixin.module) - .compileOptions(exportMixin.buildMixin.compileOptions); + .compileOptions(exportMixin.buildMixin.compileOptions) + .jdkManager(exportMixin.buildMixin.jdkProvidersMixin.getJdkManager()); } Path getJarOutputPath() { @@ -128,10 +129,8 @@ int apply(BuildContext ctx) throws IOException { String newPath = ctx.resolveClassPath().getManifestPath(); if (!newPath.isEmpty()) { Util.infoMsg("Updating jar..."); - String javaVersion = exportMixin.buildMixin.javaVersion != null - ? exportMixin.buildMixin.javaVersion - : prj.getJavaVersion(); - JarUtil.updateJar(outputPath, createManifest(newPath), prj.getMainClass(), javaVersion); + JarUtil.updateJar(outputPath, createManifest(newPath), prj.getMainClass(), + exportMixin.buildMixin.getProjectJdk(prj)); } Util.infoMsg("Exported to " + outputPath); @@ -179,10 +178,8 @@ int apply(BuildContext ctx) throws IOException { } Util.infoMsg("Updating jar..."); - String javaVersion = exportMixin.buildMixin.javaVersion != null - ? exportMixin.buildMixin.javaVersion - : prj.getJavaVersion(); - JarUtil.updateJar(outputPath, createManifest(newPath.toString()), prj.getMainClass(), javaVersion); + JarUtil.updateJar(outputPath, createManifest(newPath.toString()), prj.getMainClass(), + exportMixin.buildMixin.getProjectJdk(prj)); } Util.infoMsg("Exported to " + outputPath); return EXIT_OK; @@ -373,7 +370,8 @@ int apply(BuildContext ctx) throws IOException { Util.verboseMsg("Unpacking artifact: " + dep); UnpackUtil.unzip(dep.getFile(), tmpDir, false, null, ExportFatjar::handleExistingFile); } - JarUtil.createJar(outputPath, tmpDir, null, prj.getMainClass(), prj.getJavaVersion()); + JarUtil.createJar(outputPath, tmpDir, null, prj.getMainClass(), + exportMixin.buildMixin.getProjectJdk(prj)); } finally { Util.deletePath(tmpDir, true); } @@ -455,7 +453,7 @@ int apply(BuildContext ctx) throws IOException { } } - String jlinkCmd = JavaUtil.resolveInJavaHome("jlink", null); + String jlinkCmd = JavaUtil.resolveInJavaHome("jlink", prj.projectJdk()); String modMain = ModuleUtil.getModuleMain(prj); List cps = artifacts.stream().map(a -> a.getFile().toString()).collect(Collectors.toList()); List cp = new ArrayList<>(artifacts.size() + 1); diff --git a/src/main/java/dev/jbang/cli/Info.java b/src/main/java/dev/jbang/cli/Info.java index 6b95b45d0..df65f5786 100644 --- a/src/main/java/dev/jbang/cli/Info.java +++ b/src/main/java/dev/jbang/cli/Info.java @@ -19,8 +19,8 @@ import dev.jbang.dependencies.ArtifactInfo; import dev.jbang.dependencies.MavenRepo; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkManager; import dev.jbang.source.*; import dev.jbang.util.JavaUtil; import dev.jbang.util.ModuleUtil; @@ -117,8 +117,9 @@ public ScriptInfo(BuildContext ctx, boolean assureJdkInstalled) { requestedJavaVersion = prj.getJavaVersion(); try { - JdkProvider.Jdk jdk = assureJdkInstalled ? JdkManager.getOrInstallJdk(requestedJavaVersion) - : JdkManager.getJdk(requestedJavaVersion, false); + JdkManager jdkMan = JavaUtil.defaultJdkManager(); + Jdk jdk = assureJdkInstalled ? jdkMan.getOrInstallJdk(requestedJavaVersion) + : jdkMan.getJdk(requestedJavaVersion, false); if (jdk != null && jdk.isInstalled()) { availableJdkPath = jdk.getHome().toString(); } diff --git a/src/main/java/dev/jbang/cli/Jdk.java b/src/main/java/dev/jbang/cli/Jdk.java index 7c26c63ef..02e2caf09 100644 --- a/src/main/java/dev/jbang/cli/Jdk.java +++ b/src/main/java/dev/jbang/cli/Jdk.java @@ -12,8 +12,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.JdkManager; import dev.jbang.util.JavaUtil; import dev.jbang.util.Util; @@ -35,14 +34,14 @@ public Integer install( @CommandLine.Parameters(paramLabel = "versionOrId", index = "0", description = "The version or id to install", arity = "1") String versionOrId, @CommandLine.Parameters(paramLabel = "existingJdkPath", index = "1", description = "Pre installed JDK path", arity = "0..1") String path) throws IOException { - jdkProvidersMixin.initJdkProviders(); - JdkProvider.Jdk jdk = JdkManager.getInstalledJdk(versionOrId, true); + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk jdk = jdkMan.getInstalledJdk(versionOrId, true); if (force || jdk == null) { if (!Util.isNullOrBlankString(path)) { - JdkManager.linkToExistingJdk(path, Integer.parseInt(versionOrId)); + jdkMan.linkToExistingJdk(path, versionOrId); } else { if (jdk == null) { - jdk = JdkManager.getJdk(versionOrId, true); + jdk = jdkMan.getJdk(versionOrId, true); } jdk.install(); } @@ -61,15 +60,15 @@ public Integer list( "--show-details" }, description = "Shows detailed information for each JDK (only when format=text)") boolean details, @CommandLine.Option(names = { "--format" }, description = "Specify output format ('text' or 'json')") FormatMixin.Format format) { - jdkProvidersMixin.initJdkProviders(); - JdkProvider.Jdk defaultJdk = JdkManager.getDefaultJdk(); + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk defaultJdk = jdkMan.getDefaultJdk(); int defMajorVersion = defaultJdk != null ? defaultJdk.getMajorVersion() : 0; PrintStream out = System.out; - List jdks; + List jdks; if (available) { - jdks = JdkManager.listAvailableJdks(); + jdks = jdkMan.listAvailableJdks(); } else { - jdks = JdkManager.listInstalledJdks(); + jdks = jdkMan.listInstalledJdks(); } List jdkOuts = jdks .stream() .map(jdk -> new JdkOut(jdk.getId(), jdk.getVersion(), jdk.getProvider().name(), @@ -152,12 +151,12 @@ public int compareTo(JdkOut o) { @CommandLine.Command(name = "uninstall", description = "Uninstalls an existing JDK.") public Integer uninstall( @CommandLine.Parameters(paramLabel = "version", index = "0", description = "The version to install", arity = "1") String versionOrId) { - jdkProvidersMixin.initJdkProviders(); - JdkProvider.Jdk jdk = JdkManager.getInstalledJdk(versionOrId, true); + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk jdk = jdkMan.getInstalledJdk(versionOrId, true); if (jdk == null) { throw new ExitException(EXIT_INVALID_INPUT, "JDK " + versionOrId + " is not installed"); } - JdkManager.uninstallJdk(jdk); + jdkMan.uninstallJdk(jdk); Util.infoMsg("Uninstalled JDK:\n " + versionOrId); return EXIT_OK; } @@ -165,8 +164,8 @@ public Integer uninstall( @CommandLine.Command(name = "home", description = "Prints the folder where the given JDK is installed.") public Integer home( @CommandLine.Parameters(paramLabel = "versionOrId", index = "0", description = "The version of the JDK to select", arity = "0..1") String versionOrId) { - jdkProvidersMixin.initJdkProviders(); - Path home = JdkManager.getOrInstallJdk(versionOrId).getHome(); + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + Path home = jdkMan.getOrInstallJdk(versionOrId).getHome(); if (home != null) { String homeStr = Util.pathToString(home); System.out.println(homeStr); @@ -177,13 +176,13 @@ public Integer home( @CommandLine.Command(name = "java-env", description = "Prints out the environment variables needed to use the given JDK.") public Integer javaEnv( @CommandLine.Parameters(paramLabel = "versionOrId", index = "0", description = "The version of the JDK to select", arity = "0..1") String versionOrId) { - jdkProvidersMixin.initJdkProviders(); - JdkProvider.Jdk jdk = null; + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk jdk = null; if (versionOrId != null && JavaUtil.isRequestedVersion(versionOrId)) { - jdk = JdkManager.getJdk(versionOrId, true); + jdk = jdkMan.getJdk(versionOrId, true); } if (jdk == null || !jdk.isInstalled()) { - jdk = JdkManager.getOrInstallJdk(versionOrId); + jdk = jdkMan.getOrInstallJdk(versionOrId); } Path home = jdk.getHome(); if (home != null) { @@ -227,12 +226,16 @@ public Integer javaEnv( @CommandLine.Command(name = "default", description = "Sets the default JDK to be used by JBang.") public Integer defaultJdk( @CommandLine.Parameters(paramLabel = "version", index = "0", description = "The version of the JDK to select", arity = "0..1") String versionOrId) { - jdkProvidersMixin.initJdkProviders(); - JdkProvider.Jdk defjdk = JdkManager.getDefaultJdk(); + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + if (!jdkMan.hasDefaultProvider()) { + Util.warnMsg("Cannot perform operation, the 'default' provider was not found"); + return EXIT_INVALID_INPUT; + } + dev.jbang.jvm.Jdk defjdk = jdkMan.getDefaultJdk(); if (versionOrId != null) { - JdkProvider.Jdk jdk = JdkManager.getOrInstallJdk(versionOrId); + dev.jbang.jvm.Jdk jdk = jdkMan.getOrInstallJdk(versionOrId); if (defjdk == null || (!jdk.equals(defjdk) && !Objects.equals(jdk.getHome(), defjdk.getHome()))) { - JdkManager.setDefaultJdk(jdk); + jdkMan.setDefaultJdk(jdk); } else { Util.infoMsg("Default JDK already set to " + defjdk.getMajorVersion()); } diff --git a/src/main/java/dev/jbang/cli/JdkProvidersMixin.java b/src/main/java/dev/jbang/cli/JdkProvidersMixin.java index beb761338..f207e6785 100644 --- a/src/main/java/dev/jbang/cli/JdkProvidersMixin.java +++ b/src/main/java/dev/jbang/cli/JdkProvidersMixin.java @@ -3,7 +3,8 @@ import java.util.ArrayList; import java.util.List; -import dev.jbang.net.JdkManager; +import dev.jbang.jvm.JdkManager; +import dev.jbang.util.JavaUtil; import picocli.CommandLine; @@ -13,10 +14,13 @@ public class JdkProvidersMixin { "--jdk-providers" }, description = "Use the given providers to check for installed JDKs", split = ",", hidden = true) List jdkProviders; - protected void initJdkProviders() { - if (jdkProviders != null && !jdkProviders.isEmpty()) { - JdkManager.initProvidersByName(jdkProviders); + private JdkManager jdkMan; + + protected JdkManager getJdkManager() { + if (jdkMan == null) { + jdkMan = JavaUtil.defaultJdkManager(jdkProviders); } + return jdkMan; } public List opts() { diff --git a/src/main/java/dev/jbang/cli/Run.java b/src/main/java/dev/jbang/cli/Run.java index 9bfce0bf6..4e15d3037 100644 --- a/src/main/java/dev/jbang/cli/Run.java +++ b/src/main/java/dev/jbang/cli/Run.java @@ -51,7 +51,6 @@ private void rewriteScriptArguments() { public Integer doCall() throws IOException { requireScriptArgument(); rewriteScriptArguments(); - jdkProvidersMixin.initJdkProviders(); userParams = handleRemoteFiles(userParams); String scriptOrFile = scriptMixin.scriptOrFile; diff --git a/src/main/java/dev/jbang/dependencies/ModularClassPath.java b/src/main/java/dev/jbang/dependencies/ModularClassPath.java index c72989f3a..38295ec12 100644 --- a/src/main/java/dev/jbang/dependencies/ModularClassPath.java +++ b/src/main/java/dev/jbang/dependencies/ModularClassPath.java @@ -21,7 +21,7 @@ import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest; import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; import dev.jbang.util.Util; public class ModularClassPath { @@ -67,7 +67,7 @@ boolean hasJavaFX() { return javafx.get(); } - public List getAutoDectectedModuleArguments(@Nonnull JdkProvider.Jdk jdk) { + public List getAutoDectectedModuleArguments(@Nonnull Jdk jdk) { if (hasJavaFX() && supportsModules(jdk)) { List commandArguments = new ArrayList<>(); @@ -132,7 +132,7 @@ public List getAutoDectectedModuleArguments(@Nonnull JdkProvider.Jdk jdk } } - protected boolean supportsModules(JdkProvider.Jdk jdk) { + protected boolean supportsModules(Jdk jdk) { return jdk.getMajorVersion() >= 9; } diff --git a/src/main/java/dev/jbang/net/JdkManager.java b/src/main/java/dev/jbang/net/JdkManager.java deleted file mode 100644 index 27fbd3578..000000000 --- a/src/main/java/dev/jbang/net/JdkManager.java +++ /dev/null @@ -1,552 +0,0 @@ -package dev.jbang.net; - -import static dev.jbang.cli.BaseCommand.EXIT_INVALID_INPUT; -import static dev.jbang.cli.BaseCommand.EXIT_UNEXPECTED_STATE; - -import java.io.IOException; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.Settings; -import dev.jbang.cli.ExitException; -import dev.jbang.net.jdkproviders.*; -import dev.jbang.util.JavaUtil; -import dev.jbang.util.Util; - -public class JdkManager { - private static List providers = null; - - // TODO Don't hard-code this list - public static final String[] PROVIDERS_ALL = new String[] { "current", "default", "javahome", "path", "jbang", - "sdkman", "scoop", "linux" }; - public static final String[] PROVIDERS_DEFAULT = new String[] { "current", "default", "javahome", "path", "jbang" }; - - public static void initProvidersByName(String... providerNames) { - initProvidersByName(Arrays.asList(providerNames)); - } - - public static void initProvidersByName(List providerNames) { - if (providerNames.size() == 1 && "all".equals(providerNames.get(0))) { - initProvidersByName(PROVIDERS_ALL); - return; - } - // TODO Make providers know their names instead of hard-coding - providers = new ArrayList<>(); - for (String name : providerNames) { - JdkProvider provider; - switch (name) { - case "current": - provider = new CurrentJdkProvider(); - break; - case "default": - provider = new DefaultJdkProvider(); - break; - case "javahome": - provider = new JavaHomeJdkProvider(); - break; - case "path": - provider = new PathJdkProvider(); - break; - case "jbang": - provider = new JBangJdkProvider(); - break; - case "sdkman": - provider = new SdkmanJdkProvider(); - break; - case "scoop": - provider = new ScoopJdkProvider(); - break; - case "linux": - provider = new LinuxJdkProvider(); - break; - default: - Util.warnMsg("Unknown JDK provider: " + name); - continue; - } - if (provider.canUse()) { - providers.add(provider); - } - } - if (providers.isEmpty()) { - throw new ExitException(EXIT_INVALID_INPUT, "No providers could be initialized. Aborting."); - } - } - - public static void initProviders(List provs) { - providers = provs; - if (Util.isVerbose()) { - Util.verboseMsg("Using JDK provider(s): " + providers .stream() - .map(p -> p.getClass().getSimpleName()) - .collect(Collectors.joining(", "))); - } - } - - @Nonnull - private static List providers() { - if (providers == null) { - initProvidersByName(PROVIDERS_DEFAULT); - } - return providers; - } - - @Nonnull - private static List updatableProviders() { - return providers().stream().filter(JdkProvider::canUpdate).collect(Collectors.toList()); - } - - /** - * This method is like getJdk() but will make sure that the JDK - * being returned is actually installed. It will perform an installation if - * necessary. - * - * @param versionOrId A version pattern, id or null - * @return A Jdk object - * @throws ExitException If no JDK could be found at all or if one failed to - * install - */ - @Nonnull - public static JdkProvider.Jdk getOrInstallJdk(String versionOrId) { - if (versionOrId != null) { - if (JavaUtil.isRequestedVersion(versionOrId)) { - return getOrInstallJdkByVersion(JavaUtil.minRequestedVersion(versionOrId), - JavaUtil.isOpenVersion(versionOrId), false); - } else { - return getOrInstallJdkById(versionOrId, false); - } - } else { - return getOrInstallJdkByVersion(0, true, false); - } - } - - /** - * This method is like getJdkByVersion() but will make sure that - * the JDK being returned is actually installed. It will perform an installation - * if necessary. - * - * @param requestedVersion The (minimal) version to return, can be 0 - * @param openVersion Return newer version if exact is not available - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - * @throws ExitException If no JDK could be found at all or if one failed to - * install - */ - @Nonnull - private static JdkProvider.Jdk getOrInstallJdkByVersion(int requestedVersion, boolean openVersion, - boolean updatableOnly) { - Util.verboseMsg("Looking for JDK: " + requestedVersion); - JdkProvider.Jdk jdk = getJdkByVersion(requestedVersion, openVersion, updatableOnly); - if (jdk == null) { - if (requestedVersion > 0) { - throw new ExitException(EXIT_UNEXPECTED_STATE, - "No suitable JDK was found for requested version: " + requestedVersion); - } else { - throw new ExitException(EXIT_UNEXPECTED_STATE, "No suitable JDK was found"); - } - } - jdk = ensureInstalled(jdk); - - Util.verboseMsg("Using JDK: " + jdk); - - return jdk; - } - - /** - * This method is like getJdkByVersion() but will make sure that - * the JDK being returned is actually installed. It will perform an installation - * if necessary. - * - * @param requestedId The id of the JDK to return - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - * @throws ExitException If no JDK could be found at all or if one failed to - * install - */ - @Nonnull - private static JdkProvider.Jdk getOrInstallJdkById(@Nonnull String requestedId, boolean updatableOnly) { - Util.verboseMsg("Looking for JDK: " + requestedId); - JdkProvider.Jdk jdk = getJdkById(requestedId, updatableOnly); - if (jdk == null) { - throw new ExitException(EXIT_UNEXPECTED_STATE, - "No suitable JDK was found for requested id: " + requestedId); - } - jdk = ensureInstalled(jdk); - - Util.verboseMsg("Using JDK: " + jdk); - - return jdk; - } - - private static JdkProvider.Jdk ensureInstalled(JdkProvider.Jdk jdk) { - if (!jdk.isInstalled()) { - jdk = jdk.install(); - if (getDefaultJdk() == null) { - setDefaultJdk(jdk); - } - } - return jdk; - } - - /** - * Returns a Jdk object that matches the requested version from the - * list of currently installed JDKs or from the ones available for installation. - * The parameter is a string that either contains the actual (strict) major - * version of the JDK that should be returned, an open version terminated with a - * + sign to indicate that any later version is valid as well, or - * it is an id that will be matched against the ids of JDKs that are currently - * installed. If the requested version is null the "active" JDK - * will be returned, this is normally the JDK currently being used to run JBang - * itself. The method will return null if no installed or available - * JDK matches. NB: This method can return Jdk objects for JDKs - * that are currently _not_ installed. It will not cause any installs to be - * performed. See getOrInstallJdk() for that. - * - * @param versionOrId A version pattern, id or null - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - * @throws ExitException If no JDK could be found at all - */ - @Nullable - public static JdkProvider.Jdk getJdk(@Nullable String versionOrId, boolean updatableOnly) { - if (versionOrId != null) { - if (JavaUtil.isRequestedVersion(versionOrId)) { - return getJdkByVersion(JavaUtil.minRequestedVersion(versionOrId), JavaUtil.isOpenVersion(versionOrId), - updatableOnly); - } else { - return getJdkById(versionOrId, updatableOnly); - } - } else { - return getJdkByVersion(0, true, updatableOnly); - } - } - - /** - * Returns an Jdk object that matches the requested version from - * the list of currently installed JDKs or from the ones available for - * installation. The method will return null if no installed or - * available JDK matches. NB: This method can return Jdk objects - * for JDKs that are currently _not_ installed. It will not cause any installs - * to be performed. See getOrInstallJdkByVersion() for that. - * - * @param requestedVersion The (minimal) version to return, can be 0 - * @param openVersion Return newer version if exact is not available - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - * @throws ExitException If no JDK could be found at all - */ - @Nullable - private static JdkProvider.Jdk getJdkByVersion(int requestedVersion, boolean openVersion, boolean updatableOnly) { - JdkProvider.Jdk jdk = getInstalledJdkByVersion(requestedVersion, openVersion, updatableOnly); - if (jdk == null) { - if (requestedVersion > 0 && (requestedVersion >= Settings.getDefaultJavaVersion() || !openVersion)) { - jdk = getAvailableJdkByVersion(requestedVersion, false); - } else { - jdk = getJdkByVersion(Settings.getDefaultJavaVersion(), true, updatableOnly); - } - } - return jdk; - } - - /** - * Returns an Jdk object that matches the requested version from - * the list of currently installed JDKs or from the ones available for - * installation. The method will return null if no installed or - * available JDK matches. NB: This method can return Jdk objects - * for JDKs that are currently _not_ installed. It will not cause any installs - * to be performed. See getOrInstallJdkByVersion() for that. - * - * @param requestedId The id of the JDK to return - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - * @throws ExitException If no JDK could be found at all - */ - @Nullable - private static JdkProvider.Jdk getJdkById(@Nonnull String requestedId, boolean updatableOnly) { - JdkProvider.Jdk jdk = getInstalledJdkById(requestedId, updatableOnly); - if (jdk == null) { - jdk = getAvailableJdkById(requestedId); - } - return jdk; - } - - /** - * Returns an Jdk object for an installed JDK of the given version - * or id. Will return null if no JDK of that version or id is - * currently installed. - * - * @param versionOrId A version pattern, id or null - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - */ - @Nullable - public static JdkProvider.Jdk getInstalledJdk(String versionOrId, boolean updatableOnly) { - if (versionOrId != null) { - if (JavaUtil.isRequestedVersion(versionOrId)) { - return getInstalledJdkByVersion(JavaUtil.minRequestedVersion(versionOrId), - JavaUtil.isOpenVersion(versionOrId), updatableOnly); - } else { - return getInstalledJdkById(versionOrId, updatableOnly); - } - } else { - return getInstalledJdkByVersion(0, true, updatableOnly); - } - } - - /** - * Returns an Jdk object for an installed JDK of the given version. - * Will return null if no JDK of that version is currently - * installed. - * - * @param version The (major) version of the JDK to return - * @param openVersion Return newer version if exact is not available - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - */ - @Nullable - private static JdkProvider.Jdk getInstalledJdkByVersion(int version, boolean openVersion, boolean updatableOnly) { - return providers() .stream() - .filter(p -> !updatableOnly || p.canUpdate()) - .map(p -> p.getJdkByVersion(version, openVersion)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - /** - * Returns an Jdk object for an installed JDK with the given id. - * Will return null if no JDK with that id is currently installed. - * - * @param requestedId The id of the JDK to return - * @param updatableOnly Only return JDKs from updatable providers or not - * @return A Jdk object or null - */ - @Nullable - private static JdkProvider.Jdk getInstalledJdkById(String requestedId, boolean updatableOnly) { - return providers() .stream() - .filter(p -> !updatableOnly || p.canUpdate()) - .map(p -> p.getJdkById(requestedId)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - @Nonnull - private static JdkProvider.Jdk getAvailableJdkByVersion(int version, boolean openVersion) { - List jdks = getJdkByVersion(listAvailableJdks(), version, openVersion); - if (jdks.isEmpty()) { - throw new ExitException(EXIT_INVALID_INPUT, "JDK version is not available for installation: " + version - + "\n" - + "Use 'jbang jdk list --available' to see a list of JDKs available for installation"); - } - return jdks.get(0); - } - - @Nonnull - private static JdkProvider.Jdk getAvailableJdkById(String id) { - List jdks = getJdkById(listAvailableJdks(), id); - if (jdks.isEmpty()) { - throw new ExitException(EXIT_INVALID_INPUT, "JDK id is not available for installation: " + id - + "\n" - + "Use 'jbang jdk list --available --show-details' to see a list of JDKs available for installation"); - } - return jdks.get(0); - } - - public static void uninstallJdk(JdkProvider.Jdk jdk) { - JdkProvider.Jdk defaultJdk = getDefaultJdk(); - if (Util.isWindows()) { - // On Windows we have to check nobody is currently using the JDK or we could - // be causing all kinds of trouble - try { - Path jdkTmpDir = jdk.getHome() - .getParent() - .resolve("_delete_me_" + jdk.getHome().getFileName().toString()); - Files.move(jdk.getHome(), jdkTmpDir); - Files.move(jdkTmpDir, jdk.getHome()); - } catch (IOException ex) { - Util.warnMsg("Cannot uninstall JDK, it's being used: " + jdk); - return; - } - } - - boolean resetDefault = false; - if (defaultJdk != null) { - Path defHome = defaultJdk.getHome(); - try { - resetDefault = Files.isSameFile(defHome, jdk.getHome()); - } catch (IOException ex) { - Util.verboseMsg("Error while trying to reset default JDK", ex); - resetDefault = defHome.equals(jdk.getHome()); - } - } - - jdk.uninstall(); - - if (resetDefault) { - Optional newjdk = nextInstalledJdk(jdk.getMajorVersion(), true); - if (!newjdk.isPresent()) { - newjdk = prevInstalledJdk(jdk.getMajorVersion(), true); - } - if (newjdk.isPresent()) { - setDefaultJdk(newjdk.get()); - } else { - removeDefaultJdk(); - Util.infoMsg("Default JDK unset"); - } - } - } - - /** - * Links JBang JDK folder to an already existing JDK path with a link. It checks - * if the incoming version number is the same that the linked JDK has, if not an - * exception will be raised. - * - * @param path path to the pre-installed JDK. - * @param version requested version to link. - */ - public static void linkToExistingJdk(String path, int version) { - Path linkPath = JBangJdkProvider.getJdksPath().resolve(Integer.toString(version)); - Util.verboseMsg("Trying to link " + path + " to " + linkPath); - if (Files.exists(linkPath) || Files.isSymbolicLink(linkPath)) { - Util.verboseMsg("JBang managed JDK already exists, must be deleted to make sure linking works"); - Util.deletePath(linkPath, false); - } - Path linkedJdkPath = Paths.get(path); - if (!Files.isDirectory(linkedJdkPath)) { - throw new ExitException(EXIT_INVALID_INPUT, "Unable to resolve path as directory: " + path); - } - Optional ver = JavaUtil.resolveJavaVersionFromPath(linkedJdkPath); - if (ver.isPresent()) { - Integer linkedJdkVersion = ver.get(); - if (linkedJdkVersion == version) { - Util.mkdirs(linkPath.getParent()); - Util.createLink(linkPath, linkedJdkPath); - Util.infoMsg("JDK " + version + " has been linked to: " + linkedJdkPath); - } else { - throw new ExitException(EXIT_INVALID_INPUT, "Java version in given path: " + path - + " is " + linkedJdkVersion + " which does not match the requested version " + version + ""); - } - } else { - throw new ExitException(EXIT_INVALID_INPUT, "Unable to determine Java version in given path: " + path); - } - } - - /** - * Returns an installed JDK that matches the requested version or the next - * available version. Returns Optional.empty() if no matching JDK - * was found; - * - * @param minVersion the minimal version to return - * @param updatableOnly Only return JDKs from updatable providers or not - * @return an optional JDK - */ - private static Optional nextInstalledJdk(int minVersion, boolean updatableOnly) { - return listInstalledJdks() - .stream() - .filter(jdk -> !updatableOnly || jdk.getProvider().canUpdate()) - .filter(jdk -> jdk.getMajorVersion() >= minVersion) - .min(JdkProvider.Jdk::compareTo); - } - - /** - * Returns an installed JDK that matches the requested version or the previous - * available version. Returns Optional.empty() if no matching JDK - * was found; - * - * @param maxVersion the maximum version to return - * @param updatableOnly Only return JDKs from updatable providers or not - * @return an optional JDK - */ - private static Optional prevInstalledJdk(int maxVersion, boolean updatableOnly) { - return listInstalledJdks() - .stream() - .filter(jdk -> !updatableOnly || jdk.getProvider().canUpdate()) - .filter(jdk -> jdk.getMajorVersion() <= maxVersion) - .min(JdkProvider.Jdk::compareTo); - } - - public static List listAvailableJdks() { - return updatableProviders() .stream() - .flatMap(p -> p.listAvailable().stream()) - .collect(Collectors.toList()); - } - - public static List listInstalledJdks() { - return providers() .stream() - .flatMap(p -> p.listInstalled().stream()) - .sorted() - .collect(Collectors.toList()); - } - - @Nullable - public static JdkProvider.Jdk getDefaultJdk() { - return (new DefaultJdkProvider()).getJdkById("default"); - } - - public static void setDefaultJdk(JdkProvider.Jdk jdk) { - JdkProvider.Jdk defJdk = getDefaultJdk(); - if (jdk.isInstalled() && !jdk.equals(defJdk)) { - Path defaultJdk = getDefaultJdkPath(); - Path newDefaultJdk = defaultJdk.getParent().resolve(defaultJdk.getFileName() + ".new"); - Util.createLink(newDefaultJdk, jdk.getHome()); - removeJdk(defaultJdk); - try { - Files.move(newDefaultJdk, defaultJdk); - Util.infoMsg("Default JDK set to " + jdk); - } catch (IOException e) { - // Ignore - } - } - } - - public static void removeDefaultJdk() { - Path link = getDefaultJdkPath(); - removeJdk(link); - } - - private static void removeJdk(Path jdkPath) { - try { - Files.deleteIfExists(jdkPath); - } catch (DirectoryNotEmptyException e) { - Util.deletePath(jdkPath, true); - } catch (IOException e) { - // Ignore - } - } - - private static Path getDefaultJdkPath() { - return Settings.getCurrentJdkDir(); - } - - public static boolean isCurrentJdkManaged() { - Path home = JavaUtil.getJdkHome(); - return home != null && updatableProviders().stream().anyMatch(p -> p.getJdkByPath(home) != null); - } - - private static List getJdkByVersion(Collection jdks, int version, - boolean openVersion) { - Stream s = jdks.stream(); - if (openVersion) { - s = s.filter(jdk -> jdk.getMajorVersion() >= version); - } else { - s = s.filter(jdk -> jdk.getMajorVersion() == version); - } - return s.collect(Collectors.toList()); - } - - private static List getJdkById(Collection jdks, String id) { - return jdks.stream().filter(jdk -> jdk.getId().equals(id)).collect(Collectors.toList()); - } -} diff --git a/src/main/java/dev/jbang/net/JdkProvider.java b/src/main/java/dev/jbang/net/JdkProvider.java deleted file mode 100644 index 9a2e00715..000000000 --- a/src/main/java/dev/jbang/net/JdkProvider.java +++ /dev/null @@ -1,283 +0,0 @@ -package dev.jbang.net; - -import static dev.jbang.util.JavaUtil.resolveJavaVersionStringFromPath; - -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.util.JavaUtil; - -/** - * This interface must be implemented by providers that are able to give access - * to JDKs installed on the user's system. Some providers will also be able to - * manage those JDKs by installing and uninstalling them at the user's request. - * In those cases the canUpdate() should return true. - * - * The providers deal in JDK identifiers, not in versions. Those identifiers are - * specific to the implementation but should follow two important rules: 1. they - * must be unique across implementations 2. they must start with an integer - * specifying the main JDK version - */ -public interface JdkProvider { - - class Jdk implements Comparable { - @Nonnull - private final transient JdkProvider provider; - @Nonnull - private final String id; - @Nonnull - private final String version; - @Nullable - private final Path home; - - private Jdk(@Nonnull JdkProvider provider, @Nonnull String id, @Nullable Path home, @Nonnull String version) { - this.provider = provider; - this.id = id; - this.version = version; - this.home = home; - } - - @Nonnull - public JdkProvider getProvider() { - return provider; - } - - /** - * Returns the id that is used to uniquely identify this JDK across all - * providers - */ - @Nonnull - public String getId() { - return id; - } - - /** - * Returns the JDK's version - */ - public String getVersion() { - return version; - } - - /** - * The path to where the JDK is installed. Can be null which means - * the JDK isn't currently installed by that provider - */ - @Nullable - public Path getHome() { - return home; - } - - public int getMajorVersion() { - return JavaUtil.parseJavaVersion(getVersion()); - } - - public Jdk install() { - return provider.install(id); - } - - public void uninstall() { - provider.uninstall(id); - } - - public boolean isInstalled() { - return home != null; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Jdk jdk = (Jdk) o; - return id.equals(jdk.id) && Objects.equals(home, jdk.home); - } - - @Override - public int hashCode() { - return Objects.hash(home, id); - } - - @Override - public int compareTo(Jdk o) { - return Integer.compare(getMajorVersion(), o.getMajorVersion()); - } - - @Override - public String toString() { - return getMajorVersion() + " (" + version + ", " + id + ", " + home + ")"; - } - } - - default Jdk createJdk(@Nonnull String id, @Nullable Path home, @Nonnull String version) { - return new Jdk(this, id, home, version); - } - - default String name() { - String nm = getClass().getSimpleName(); - if (nm.endsWith("JdkProvider")) { - return nm.substring(0, nm.length() - 11).toLowerCase(); - } else { - return nm.toLowerCase(); - } - } - - /** - * For providers that can update this returns a set of JDKs that are available - * for installation. Providers might set the home field of the JDK - * objects if the respective JDK is currently installed on the user's system, - * but only if they can ensure that it's the exact same version, otherwise they - * should just leave the field null. - * - * @return List of Jdk objects - */ - @Nonnull - default List listAvailable() { - throw new UnsupportedOperationException("Listing available JDKs is not supported by " + getClass().getName()); - } - - /** - * Returns a set of JDKs that are currently installed on the user's system. - * - * @return List of Jdk objects, possibly empty - */ - @Nonnull - List listInstalled(); - - /** - * Determines if a JDK of the requested version is currently installed by this - * provider and if so returns its respective Jdk object, otherwise - * it returns null. If openVersion is set to true the - * method will also return the next installed version if the exact version was - * not found. - * - * @param version The specific JDK version to return - * @param openVersion Return newer version if exact is not available - * @return A Jdk object or null - */ - @Nullable - default Jdk getJdkByVersion(int version, boolean openVersion) { - List jdks = listInstalled(); - Jdk res; - if (openVersion) { - res = jdks .stream() - .sorted() - .filter(jdk -> jdk.getMajorVersion() >= version) - .findFirst() - .orElse(null); - } else { - res = jdks .stream() - .filter(jdk -> jdk.getMajorVersion() == version) - .findFirst() - .orElse(null); - } - return res; - } - - /** - * Determines if the given id refers to a JDK managed by this provider and if so - * returns its respective Jdk object, otherwise it returns - * null. - * - * @param id The id to look for - * @return A Jdk object or null - */ - @Nullable - Jdk getJdkById(@Nonnull String id); - - /** - * Determines if the given path belongs to a JDK managed by this provider and if - * so returns its respective Jdk object, otherwise it returns - * null. - * - * @param jdkPath The path to look for - * @return A Jdk object or null - */ - @Nullable - Jdk getJdkByPath(@Nonnull Path jdkPath); - - /** - * For providers that can update this installs the indicated JDK - * - * @param jdk The identifier of the JDK to install - * @return A Jdk object - * @throws UnsupportedOperationException if the provider can not update - */ - @Nonnull - default Jdk install(@Nonnull String jdk) { - throw new UnsupportedOperationException("Installing a JDK is not supported by " + getClass().getName()); - } - - /** - * Uninstalls the indicated JDK - * - * @param jdk The identifier of the JDK to install - * @throws UnsupportedOperationException if the provider can not update - */ - default void uninstall(@Nonnull String jdk) { - throw new UnsupportedOperationException("Uninstalling a JDK is not supported by " + getClass().getName()); - } - - /** - * Indicates if the provider can be used or not. This can perform sanity checks - * like the availability of certain package being installed on the system or - * even if the system is running a supported operating system. - * - * @return True if the provider can be used, false otherwise - */ - default boolean canUse() { - return true; - } - - /** - * Indicates if the provider is able to (un)install JDKs or not - * - * @return True if JDKs can be (un)installed, false otherwise - */ - default boolean canUpdate() { - return false; - } - - /** - * This is a special "dummy" provider that can be used to create - * Jdk objects for JDKs that don't seem to belong to any of the - * known providers but for which we still want an object to represent them. - */ - class UnknownJdkProvider implements JdkProvider { - private static final UnknownJdkProvider instance = new UnknownJdkProvider(); - - @Nonnull - @Override - public List listInstalled() { - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - Optional version = resolveJavaVersionStringFromPath(jdkPath); - if (version.isPresent()) { - return createJdk("unknown", jdkPath, version.get()); - } else { - return null; - } - } - - public static Jdk createJdk(Path jdkPath) { - return instance.getJdkByPath(jdkPath); - } - } - -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/BaseFoldersJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/BaseFoldersJdkProvider.java deleted file mode 100644 index 9426a14f0..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/BaseFoldersJdkProvider.java +++ /dev/null @@ -1,132 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.net.JdkProvider; -import dev.jbang.util.JavaUtil; -import dev.jbang.util.Util; - -public abstract class BaseFoldersJdkProvider implements JdkProvider { - @Nonnull - @Override - public List listInstalled() { - if (Files.isDirectory(getJdksRoot())) { - try (Stream jdkPaths = listJdkPaths()) { - return jdkPaths - .map(this::createJdk) - .filter(Objects::nonNull) - .sorted(Jdk::compareTo) - .collect(Collectors.toList()); - } catch (IOException e) { - Util.verboseMsg("Couldn't list installed JDKs", e); - } - } - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - if (isValidId(id)) { - try (Stream jdkPaths = listJdkPaths()) { - return jdkPaths - .filter(p -> jdkId(p.getFileName().toString()).equals(id)) - .map(this::createJdk) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } catch (IOException e) { - Util.verboseMsg("Couldn't list installed JDKs", e); - } - } - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - if (jdkPath.startsWith(getJdksRoot())) { - try (Stream jdkPaths = listJdkPaths()) { - return jdkPaths - .filter(jdkPath::startsWith) - .map(this::createJdk) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } catch (IOException e) { - Util.verboseMsg("Couldn't list installed JDKs", e); - } - } - return null; - } - - /** - * Returns a path to the requested JDK. This method should never return - * null and should return the path where the requested JDK is - * either currently installed or where it would be installed if it were - * available. This only needs to be implemented for providers that are - * updatable. - * - * @param jdk The identifier of the JDK to install - * @return A path to the requested JDK - */ - @Nonnull - protected Path getJdkPath(@Nonnull String jdk) { - return getJdksRoot().resolve(jdk); - } - - protected Predicate sameJdk(Path jdkRoot) { - Path release = jdkRoot.resolve("release"); - return (Path p) -> { - try { - return Files.isSameFile(p.resolve("release"), release); - } catch (IOException e) { - return false; - } - }; - } - - protected Stream listJdkPaths() throws IOException { - if (Files.isDirectory(getJdksRoot())) { - return Files.list(getJdksRoot()).filter(this::acceptFolder); - } - return Stream.empty(); - } - - @Nonnull - protected Path getJdksRoot() { - throw new UnsupportedOperationException("Getting the JDK root folder not supported by " + getClass().getName()); - } - - @Nullable - protected Jdk createJdk(Path home) { - String name = home.getFileName().toString(); - Optional version = JavaUtil.resolveJavaVersionStringFromPath(home); - if (version.isPresent() && acceptFolder(home)) { - return createJdk(jdkId(name), home, version.get()); - } - return null; - } - - protected boolean acceptFolder(Path jdkFolder) { - return Util.searchPath("javac", jdkFolder.resolve("bin").toString()) != null; - } - - protected boolean isValidId(String id) { - return id.endsWith("-" + name()); - } - - protected abstract String jdkId(String name); -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/CurrentJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/CurrentJdkProvider.java deleted file mode 100644 index 6fcae20a4..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/CurrentJdkProvider.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import static dev.jbang.util.JavaUtil.resolveJavaVersionStringFromPath; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.net.JdkProvider; -import dev.jbang.util.JavaUtil; - -/** - * This JDK provider returns the "current" JDK, which is the JDK that is - * currently being used to run JBang. - */ -public class CurrentJdkProvider implements JdkProvider { - @Nonnull - @Override - public List listInstalled() { - String jh = System.getProperty("java.home"); - if (jh != null) { - Path jdkHome = Paths.get(jh); - jdkHome = JavaUtil.jre2jdk(jdkHome); - Optional version = resolveJavaVersionStringFromPath(jdkHome); - if (version.isPresent()) { - String id = "current"; - return Collections.singletonList(createJdk(id, jdkHome, version.get())); - } - } - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - if (id.equals(name())) { - List l = listInstalled(); - if (!l.isEmpty()) { - return l.get(0); - } - } - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - List installed = listInstalled(); - Jdk def = !installed.isEmpty() ? installed.get(0) : null; - return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) ? def : null; - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/DefaultJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/DefaultJdkProvider.java deleted file mode 100644 index 4871b1275..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/DefaultJdkProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import static dev.jbang.util.JavaUtil.resolveJavaVersionStringFromPath; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.Settings; -import dev.jbang.net.JdkProvider; - -/** - * This JDK provider returns the "default" JDK if it was set (using - * jbang jdk default). - */ -public class DefaultJdkProvider implements JdkProvider { - @Nonnull - @Override - public List listInstalled() { - Path defaultDir = Settings.getCurrentJdkDir(); - if (Files.isDirectory(defaultDir)) { - Optional version = resolveJavaVersionStringFromPath(defaultDir); - if (version.isPresent()) { - String id = "default"; - return Collections.singletonList(createJdk(id, defaultDir, version.get())); - } - } - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - if (id.equals(name())) { - List l = listInstalled(); - if (!l.isEmpty()) { - return l.get(0); - } - } - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - List installed = listInstalled(); - Jdk def = !installed.isEmpty() ? installed.get(0) : null; - return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) ? def : null; - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/JBangJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/JBangJdkProvider.java deleted file mode 100644 index 8c73c86f8..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/JBangJdkProvider.java +++ /dev/null @@ -1,275 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import static dev.jbang.cli.BaseCommand.EXIT_UNEXPECTED_STATE; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.Cache; -import dev.jbang.Settings; -import dev.jbang.cli.ExitException; -import dev.jbang.net.JdkManager; -import dev.jbang.util.JavaUtil; -import dev.jbang.util.UnpackUtil; -import dev.jbang.util.Util; - -/** - * JBang's main JDK provider that can download and install the JDKs provided by - * the Foojay Disco API. They get installed in JBang's cache folder. - */ -public class JBangJdkProvider extends BaseFoldersJdkProvider { - private static final String FOOJAY_JDK_DOWNLOAD_URL = "https://api.foojay.io/disco/v3.0/directuris?"; - private static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; - - private static class JdkResult { - String java_version; - int major_version; - String release_status; - } - - private static class VersionsResponse { - List result; - } - - @Nonnull - @Override - public List listAvailable() { - try { - List result = new ArrayList<>(); - Consumer addJdk = version -> { - result.add(createJdk(jdkId(version), null, version)); - }; - String distro = Util.getVendor(); - if (distro == null) { - VersionsResponse res = Util.readJsonFromURL(getVersionsUrl(Util.getOS(), Util.getArch(), "temurin"), - VersionsResponse.class); - filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); - res = Util.readJsonFromURL(getVersionsUrl(Util.getOS(), Util.getArch(), "aoj"), VersionsResponse.class); - filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); - } else { - VersionsResponse res = Util.readJsonFromURL(getVersionsUrl(Util.getOS(), Util.getArch(), distro), - VersionsResponse.class); - filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); - } - result.sort(Jdk::compareTo); - return Collections.unmodifiableList(result); - } catch (IOException e) { - Util.errorMsg("Could not list available JDK's using foojay.io: " + e.getMessage(), e); - // Util.verboseMsg("Couldn't list available JDKs", e); - } - return Collections.emptyList(); - } - - // Filter out any EA releases for which a GA with - // the same major version exists - private List filterEA(List jdks) { - Set GAs = jdks - .stream() - .filter(jdk -> jdk.release_status.equals("ga")) - .map(jdk -> jdk.major_version) - .collect(Collectors.toSet()); - - JdkResult[] lastJdk = new JdkResult[] { null }; - return jdks - .stream() - .filter(jdk -> { - if (lastJdk[0] == null - || lastJdk[0].major_version != jdk.major_version - && (jdk.release_status.equals("ga") || !GAs.contains(jdk.major_version))) { - lastJdk[0] = jdk; - return true; - } else { - return false; - } - }) - .collect(Collectors.toList()); - } - - @Nullable - @Override - public Jdk getJdkByVersion(int version, boolean openVersion) { - Path jdk = getJdksRoot().resolve(Integer.toString(version)); - if (Files.isDirectory(jdk)) { - return createJdk(jdk); - } else if (openVersion) { - return super.getJdkByVersion(version, true); - } - return null; - } - - @Nonnull - @Override - public Jdk install(@Nonnull String jdk) { - int version = jdkVersion(jdk); - Util.infoMsg("Downloading JDK " + version + ". Be patient, this can take several minutes..."); - String url = getDownloadUrl(version, Util.getOS(), Util.getArch(), Util.getVendor()); - Util.verboseMsg("Downloading " + url); - Path jdkDir = getJdkPath(jdk); - Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); - Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); - Util.deletePath(jdkTmpDir, false); - Util.deletePath(jdkOldDir, false); - try { - Path jdkPkg = Util.downloadAndCacheFile(url); - Util.infoMsg("Installing JDK " + version + "..."); - Util.verboseMsg("Unpacking to " + jdkDir); - UnpackUtil.unpackJdk(jdkPkg, jdkTmpDir); - if (Files.isDirectory(jdkDir)) { - Files.move(jdkDir, jdkOldDir); - } else if (Files.isSymbolicLink(jdkDir)) { - // This means we have a broken/invalid link - Util.deletePath(jdkDir, true); - } - Files.move(jdkTmpDir, jdkDir); - Util.deletePath(jdkOldDir, false); - Optional fullVersion = JavaUtil.resolveJavaVersionStringFromPath(jdkDir); - if (!fullVersion.isPresent()) { - throw new ExitException(EXIT_UNEXPECTED_STATE, "Cannot obtain version of recently installed JDK"); - } - return createJdk(jdk, jdkDir, fullVersion.get()); - } catch (Exception e) { - Util.deletePath(jdkTmpDir, true); - if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { - try { - Files.move(jdkOldDir, jdkDir); - } catch (IOException ex) { - // Ignore - } - } - String msg = "Required Java version not possible to download or install."; - Jdk defjdk = JdkManager.getJdk(null, false); - if (defjdk != null) { - msg += " You can run with '--java " + defjdk.getMajorVersion() - + "' to force using the default installed Java."; - } - Util.errorMsg(msg); - throw new ExitException(EXIT_UNEXPECTED_STATE, - "Unable to download or install JDK version " + version, e); - } - } - - @Override - public void uninstall(@Nonnull String jdk) { - Path jdkDir = getJdkPath(jdk); - Util.deletePath(jdkDir, false); - } - - @Nonnull - @Override - protected Path getJdkPath(@Nonnull String jdk) { - return getJdksPath().resolve(Integer.toString(jdkVersion(jdk))); - } - - @Override - public boolean canUpdate() { - return true; - } - - private static String getDownloadUrl(int version, Util.OS os, Util.Arch arch, String distro) { - return FOOJAY_JDK_DOWNLOAD_URL + getUrlParams(version, os, arch, distro); - } - - private static String getVersionsUrl(Util.OS os, Util.Arch arch, String distro) { - return FOOJAY_JDK_VERSIONS_URL + getUrlParams(null, os, arch, distro); - } - - private static String getUrlParams(Integer version, Util.OS os, Util.Arch arch, String distro) { - Map params = new HashMap<>(); - if (version != null) { - params.put("version", String.valueOf(version)); - } - - if (distro == null) { - if (version == null || version == 8 || version == 11 || version >= 17) { - distro = "temurin"; - } else { - distro = "aoj"; - } - } - params.put("distro", distro); - - String archiveType; - if (os == Util.OS.windows) { - archiveType = "zip"; - } else { - archiveType = "tar.gz"; - } - params.put("archive_type", archiveType); - - params.put("architecture", arch.name()); - params.put("package_type", "jdk"); - params.put("operating_system", os.name()); - - if (os == Util.OS.windows) { - params.put("libc_type", "c_std_lib"); - } else if (os == Util.OS.mac) { - params.put("libc_type", "libc"); - } else { - params.put("libc_type", "glibc"); - } - - params.put("javafx_bundled", "false"); - params.put("latest", "available"); - params.put("release_status", "ga,ea"); - params.put("directly_downloadable", "true"); - - return urlEncodeUTF8(params); - } - - static String urlEncodeUTF8(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append(String.format("%s=%s", - urlEncodeUTF8(entry.getKey().toString()), - urlEncodeUTF8(entry.getValue().toString()))); - } - return sb.toString(); - } - - static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } - - @Nonnull - @Override - protected Path getJdksRoot() { - return getJdksPath(); - } - - public static Path getJdksPath() { - return Settings.getCacheDir(Cache.CacheClass.jdks); - } - - @Nonnull - @Override - protected String jdkId(String name) { - int majorVersion = JavaUtil.parseJavaVersion(name); - return majorVersion + "-jbang"; - } - - private static int jdkVersion(String jdk) { - return JavaUtil.parseJavaVersion(jdk); - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/JavaHomeJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/JavaHomeJdkProvider.java deleted file mode 100644 index f9ddde209..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/JavaHomeJdkProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import static dev.jbang.util.JavaUtil.resolveJavaVersionStringFromPath; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.net.JdkProvider; -import dev.jbang.util.JavaUtil; - -/** - * This JDK provider detects if a JDK is already available on the system by - * looking at JAVA_HOME environment variable. - */ -public class JavaHomeJdkProvider implements JdkProvider { - @Nonnull - @Override - public List listInstalled() { - Path jdkHome = JavaUtil.getJdkHome(); - if (jdkHome != null && Files.isDirectory(jdkHome)) { - Optional version = resolveJavaVersionStringFromPath(jdkHome); - if (version.isPresent()) { - String id = "javahome"; - return Collections.singletonList(createJdk(id, jdkHome, version.get())); - } - } - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - if (id.equals(name())) { - List l = listInstalled(); - if (!l.isEmpty()) { - return l.get(0); - } - } - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - List installed = listInstalled(); - Jdk def = !installed.isEmpty() ? installed.get(0) : null; - return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) ? def : null; - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/PathJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/PathJdkProvider.java deleted file mode 100644 index c9ae2605b..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/PathJdkProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import static dev.jbang.util.JavaUtil.resolveJavaVersionStringFromPath; - -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.net.JdkProvider; -import dev.jbang.util.Util; - -/** - * This JDK provider detects if a JDK is already available on the system by - * first looking at the user's PATH. - */ -public class PathJdkProvider implements JdkProvider { - @Nonnull - @Override - public List listInstalled() { - Path jdkHome = null; - Path javac = Util.searchPath("javac"); - if (javac != null) { - javac = javac.toAbsolutePath(); - jdkHome = javac.getParent().getParent(); - } - if (jdkHome != null) { - Optional version = resolveJavaVersionStringFromPath(jdkHome); - if (version.isPresent()) { - String id = "path"; - return Collections.singletonList(createJdk(id, jdkHome, version.get())); - } - } - return Collections.emptyList(); - } - - @Nullable - @Override - public Jdk getJdkById(@Nonnull String id) { - if (id.equals(name())) { - List l = listInstalled(); - if (!l.isEmpty()) { - return l.get(0); - } - } - return null; - } - - @Nullable - @Override - public Jdk getJdkByPath(@Nonnull Path jdkPath) { - List installed = listInstalled(); - Jdk def = !installed.isEmpty() ? installed.get(0) : null; - return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) ? def : null; - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/ScoopJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/ScoopJdkProvider.java deleted file mode 100644 index ac6e6817f..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/ScoopJdkProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.jbang.net.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 javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.jbang.cli.BaseCommand; -import dev.jbang.cli.ExitException; -import dev.jbang.util.Util; - -/** - * This JDK provider detects any JDKs that have been installed using the Scoop - * package manager. Windows only. - */ -public class ScoopJdkProvider extends BaseFoldersJdkProvider { - private static final Path SCOOP_APPS = Paths.get(System.getProperty("user.home")).resolve("scoop/apps"); - - @Nonnull - @Override - protected Stream listJdkPaths() throws IOException { - if (Files.isDirectory(getJdksRoot())) { - return Files.list(getJdksRoot()) - .filter(p -> p.getFileName().startsWith("openjdk")) - .map(p -> p.resolve("current")); - } - return Stream.empty(); - } - - @Override - protected String jdkId(String name) { - return name + "-scoop"; - } - - @Nullable - @Override - protected Jdk createJdk(Path home) { - try { - // Try to resolve any links - home = home.toRealPath(); - } catch (IOException e) { - throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, "Couldn't resolve 'current' link: " + home, e); - } - return super.createJdk(home); - } - - @Override - public boolean canUse() { - return Util.isWindows() && Files.isDirectory(SCOOP_APPS); - } -} diff --git a/src/main/java/dev/jbang/net/jdkproviders/SdkmanJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/SdkmanJdkProvider.java deleted file mode 100644 index d25c82451..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/SdkmanJdkProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.jbang.net.jdkproviders; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * This JDK provider detects any JDKs that have been installed using the SDKMAN - * package manager. - */ -public class SdkmanJdkProvider extends BaseFoldersJdkProvider { - private static final Path JDKS_ROOT = Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); - - @Nonnull - @Override - protected Path getJdksRoot() { - return JDKS_ROOT; - } - - @Nullable - @Override - protected String jdkId(String name) { - return name + "-sdkman"; - } - - @Override - public boolean canUse() { - return Files.isDirectory(JDKS_ROOT); - } -} diff --git a/src/main/java/dev/jbang/source/AppBuilder.java b/src/main/java/dev/jbang/source/AppBuilder.java index 5771c255c..55bbfea9a 100644 --- a/src/main/java/dev/jbang/source/AppBuilder.java +++ b/src/main/java/dev/jbang/source/AppBuilder.java @@ -63,7 +63,7 @@ public CmdGeneratorBuilder build() throws IOException { Util.verboseMsg("Building as previously built jar found but it or its dependencies not up-to-date."); } else if (jarProject.getJavaVersion() == null) { Util.verboseMsg("Building as previously built jar found but it has incomplete meta data."); - } else if (JavaUtil.javaVersion(requestedJavaVersion) < JavaUtil.minRequestedVersion( + } else if (project.projectJdk().getMajorVersion() < JavaUtil.minRequestedVersion( jarProject.getJavaVersion())) { Util.verboseMsg( String.format( diff --git a/src/main/java/dev/jbang/source/Project.java b/src/main/java/dev/jbang/source/Project.java index 8c708f122..fcbdd0ee8 100644 --- a/src/main/java/dev/jbang/source/Project.java +++ b/src/main/java/dev/jbang/source/Project.java @@ -12,6 +12,8 @@ import dev.jbang.dependencies.DependencyResolver; import dev.jbang.dependencies.MavenRepo; +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkManager; import dev.jbang.util.ModuleUtil; import dev.jbang.util.Util; @@ -41,10 +43,13 @@ public class Project { private boolean nativeImage; private boolean enablePreviewRequested; + private JdkManager jdkManager; + private final List subProjects = new ArrayList<>(); // Cached values private String stableId; + private Jdk projectJdk; public static final String ATTR_PREMAIN_CLASS = "Premain-Class"; public static final String ATTR_AGENT_CLASS = "Agent-Class"; @@ -233,6 +238,10 @@ public void setMainSource(Source mainSource) { this.mainSource = mainSource; } + public void setJdkManager(JdkManager jdkManager) { + this.jdkManager = jdkManager; + } + protected String getStableId() { if (stableId == null) { Stream sss = mainSourceSet.getStableIdInfo(); @@ -250,6 +259,20 @@ protected void updateDependencyResolver(DependencyResolver resolver) { getMainSourceSet().updateDependencyResolver(resolver); } + public JdkManager projectJdkManager() { + if (jdkManager == null) { + throw new IllegalStateException("No JdkManager set"); + } + return jdkManager; + } + + public Jdk projectJdk() { + if (projectJdk == null) { + projectJdk = projectJdkManager().getOrInstallJdk(getJavaVersion()); + } + return projectJdk; + } + /** * Returns a Builder that can be used to turn this * Project into executable code. diff --git a/src/main/java/dev/jbang/source/ProjectBuilder.java b/src/main/java/dev/jbang/source/ProjectBuilder.java index a947f32dd..c027df232 100644 --- a/src/main/java/dev/jbang/source/ProjectBuilder.java +++ b/src/main/java/dev/jbang/source/ProjectBuilder.java @@ -24,6 +24,7 @@ import dev.jbang.cli.BaseCommand; import dev.jbang.cli.ExitException; import dev.jbang.dependencies.*; +import dev.jbang.jvm.JdkManager; import dev.jbang.source.buildsteps.JarBuildStep; import dev.jbang.source.resolvers.*; import dev.jbang.source.sources.JavaSource; @@ -54,6 +55,7 @@ public class ProjectBuilder { private Boolean nativeImage; private String javaVersion; private Boolean enablePreview; + private JdkManager jdkManager; // Cached values private Properties contextProperties; @@ -187,6 +189,11 @@ public ProjectBuilder catalog(File catalogFile) { return this; } + public ProjectBuilder jdkManager(JdkManager jdkManager) { + this.jdkManager = jdkManager; + return this; + } + private Properties getContextProperties() { if (contextProperties == null) { contextProperties = getContextProperties(properties); @@ -432,6 +439,11 @@ private Project updateProject(Project prj) { if (enablePreview != null) { prj.setEnablePreviewRequested(enablePreview); } + if (jdkManager != null) { + prj.setJdkManager(jdkManager); + } else { + prj.setJdkManager(JavaUtil.defaultJdkManager()); + } return prj; } diff --git a/src/main/java/dev/jbang/source/buildsteps/CompileBuildStep.java b/src/main/java/dev/jbang/source/buildsteps/CompileBuildStep.java index 1012a961d..096e860ab 100644 --- a/src/main/java/dev/jbang/source/buildsteps/CompileBuildStep.java +++ b/src/main/java/dev/jbang/source/buildsteps/CompileBuildStep.java @@ -23,12 +23,12 @@ import dev.jbang.cli.BaseCommand; import dev.jbang.cli.ExitException; import dev.jbang.dependencies.MavenCoordinate; +import dev.jbang.jvm.Jdk; import dev.jbang.source.BuildContext; import dev.jbang.source.Builder; import dev.jbang.source.Project; import dev.jbang.source.ResourceRef; import dev.jbang.util.CommandBuffer; -import dev.jbang.util.JavaUtil; import dev.jbang.util.ModuleUtil; import dev.jbang.util.TemplateEngine; import dev.jbang.util.Util; @@ -58,12 +58,14 @@ public Project build() throws IOException { protected Project compile() throws IOException { Project project = ctx.getProject(); + Jdk jdk = project.projectJdk(); String requestedJavaVersion = project.getJavaVersion(); if (requestedJavaVersion == null && project.getModuleName().isPresent() - && JavaUtil.javaVersion(null) < 9) { + && jdk.getMajorVersion() < 9) { // Make sure we use at least Java 9 when dealing with modules requestedJavaVersion = "9+"; + jdk = project.projectJdkManager().getOrInstallJdk(requestedJavaVersion); } Path compileDir = ctx.getCompileDir(); @@ -72,7 +74,7 @@ protected Project compile() throws IOException { if (project.enablePreview()) { optionList.add("--enable-preview"); optionList.add("-source"); - optionList.add("" + JavaUtil.javaVersion(requestedJavaVersion)); + optionList.add("" + jdk.getMajorVersion()); } optionList.addAll(project.getMainSourceSet().getCompileOptions()); String path = ctx.resolveClassPath().getClassPath(); diff --git a/src/main/java/dev/jbang/source/buildsteps/JarBuildStep.java b/src/main/java/dev/jbang/source/buildsteps/JarBuildStep.java index e128c6279..86750083a 100644 --- a/src/main/java/dev/jbang/source/buildsteps/JarBuildStep.java +++ b/src/main/java/dev/jbang/source/buildsteps/JarBuildStep.java @@ -12,7 +12,6 @@ import dev.jbang.source.Builder; import dev.jbang.source.Project; import dev.jbang.util.JarUtil; -import dev.jbang.util.JavaUtil; /** * This class takes a Project and the result from a previous @@ -42,13 +41,13 @@ public static void createJar(Project prj, Path compileDir, Path jarFile) throws prj.getManifestAttributes().forEach((k, v) -> manifest.getMainAttributes().putValue(k, v)); - int buildJdk = JavaUtil.javaVersion(prj.getJavaVersion()); + int buildJdk = prj.projectJdk().getMajorVersion(); if (buildJdk > 0) { String val = buildJdk >= 9 ? Integer.toString(buildJdk) : "1." + buildJdk; manifest.getMainAttributes().putValue(ATTR_BUILD_JDK, val); } - JarUtil.createJar(jarFile, compileDir, manifest, prj.getMainClass(), prj.getJavaVersion()); + JarUtil.createJar(jarFile, compileDir, manifest, prj.getMainClass(), prj.projectJdk()); if (AppBuilder.keepClasses()) { // In the case the "keep classes" option is specified we write diff --git a/src/main/java/dev/jbang/source/buildsteps/NativeBuildStep.java b/src/main/java/dev/jbang/source/buildsteps/NativeBuildStep.java index 42e39ede9..3aaa58888 100644 --- a/src/main/java/dev/jbang/source/buildsteps/NativeBuildStep.java +++ b/src/main/java/dev/jbang/source/buildsteps/NativeBuildStep.java @@ -9,6 +9,7 @@ import java.util.List; import dev.jbang.cli.ExitException; +import dev.jbang.jvm.Jdk; import dev.jbang.source.BuildContext; import dev.jbang.source.Builder; import dev.jbang.source.Project; @@ -31,7 +32,7 @@ public NativeBuildStep(BuildContext ctx) { public Project build() throws IOException { List optionList = new ArrayList<>(); Project project = ctx.getProject(); - optionList.add(resolveInGraalVMHome("native-image", project.getJavaVersion())); + optionList.add(resolveInGraalVMHome("native-image", project.projectJdk())); optionList.add("-H:+ReportExceptionStackTraces"); optionList.add("--enable-https"); @@ -92,12 +93,12 @@ protected void runNativeBuilder(List optionList) throws IOException { } } - private static String resolveInGraalVMHome(String cmd, String requestedVersion) { + private static String resolveInGraalVMHome(String cmd, Jdk jdk) { String newcmd = resolveInEnv("GRAALVM_HOME", cmd); if (newcmd.equals(cmd) && !new File(newcmd).exists()) { - return JavaUtil.resolveInJavaHome(cmd, requestedVersion); + return JavaUtil.resolveInJavaHome(cmd, jdk); } else { return newcmd; } diff --git a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java index fdbda91d1..15b657365 100644 --- a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java +++ b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java @@ -9,8 +9,7 @@ import dev.jbang.Settings; import dev.jbang.cli.BaseCommand; import dev.jbang.cli.ExitException; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; import dev.jbang.source.*; import dev.jbang.util.CommandBuffer; import dev.jbang.util.JavaUtil; @@ -78,9 +77,8 @@ protected List generateCommandLineList() throws IOException { List optionalArgs = new ArrayList<>(); - String requestedJavaVersion = project.getJavaVersion(); - JdkProvider.Jdk jdk = JdkManager.getOrInstallJdk(requestedJavaVersion); - String javacmd = JavaUtil.resolveInJavaHome("java", requestedJavaVersion); + Jdk jdk = project.projectJdk(); + String javacmd = JavaUtil.resolveInJavaHome("java", jdk); addPropertyFlags(project.getProperties(), "-D", optionalArgs); diff --git a/src/main/java/dev/jbang/source/generators/JshCmdGenerator.java b/src/main/java/dev/jbang/source/generators/JshCmdGenerator.java index 4b6b57b3e..847ac6c00 100644 --- a/src/main/java/dev/jbang/source/generators/JshCmdGenerator.java +++ b/src/main/java/dev/jbang/source/generators/JshCmdGenerator.java @@ -13,7 +13,7 @@ import org.apache.commons.text.StringEscapeUtils; -import dev.jbang.net.JdkManager; +import dev.jbang.jvm.Jdk; import dev.jbang.source.*; import dev.jbang.util.JavaUtil; import dev.jbang.util.Util; @@ -55,8 +55,8 @@ protected List generateCommandLineList() throws IOException { List optionalArgs = new ArrayList<>(); - String requestedJavaVersion = project.getJavaVersion(); - String javacmd = JavaUtil.resolveInJavaHome("jshell", requestedJavaVersion); + Jdk jdk = project.projectJdk(); + String javacmd = JavaUtil.resolveInJavaHome("jshell", jdk); // NB: See https://github.com/jbangdev/jbang/issues/992 for the reasons why we // use the -J flags below @@ -115,8 +115,7 @@ protected List generateCommandLineList() throws IOException { fullArgs.addAll(jshellOpts(project.getRuntimeOptions())); fullArgs.addAll(jshellOpts(runtimeOptions)); fullArgs.addAll(ctx .resolveClassPath() - .getAutoDectectedModuleArguments( - JdkManager.getOrInstallJdk(requestedJavaVersion))); + .getAutoDectectedModuleArguments(jdk)); fullArgs.addAll(optionalArgs); if (project.isJShell()) { diff --git a/src/main/java/dev/jbang/source/sources/GroovySource.java b/src/main/java/dev/jbang/source/sources/GroovySource.java index c8944463f..9e967d686 100644 --- a/src/main/java/dev/jbang/source/sources/GroovySource.java +++ b/src/main/java/dev/jbang/source/sources/GroovySource.java @@ -11,7 +11,6 @@ import org.jboss.jandex.ClassInfo; import dev.jbang.net.GroovyManager; -import dev.jbang.net.JdkManager; import dev.jbang.source.*; import dev.jbang.source.AppBuilder; import dev.jbang.source.buildsteps.CompileBuildStep; @@ -95,7 +94,9 @@ protected void runCompiler(ProcessBuilder processBuilder) throws IOException { if (project.getMainSource() instanceof GroovySource) { processBuilder .environment() .put("JAVA_HOME", - JdkManager.getOrInstallJdk(project.getJavaVersion()).getHome().toString()); + project .projectJdk() + .getHome() + .toString()); processBuilder.environment().remove("GROOVY_HOME"); } super.runCompiler(processBuilder); diff --git a/src/main/java/dev/jbang/source/sources/JavaSource.java b/src/main/java/dev/jbang/source/sources/JavaSource.java index 6ad4275fe..bf8927eb3 100644 --- a/src/main/java/dev/jbang/source/sources/JavaSource.java +++ b/src/main/java/dev/jbang/source/sources/JavaSource.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.function.Function; +import dev.jbang.jvm.Jdk; import dev.jbang.source.*; import dev.jbang.source.AppBuilder; import dev.jbang.source.buildsteps.CompileBuildStep; @@ -61,7 +62,9 @@ public JavaCompileBuildStep() { @Override protected String getCompilerBinary(String requestedJavaVersion) { - return resolveInJavaHome("javac", requestedJavaVersion); + Project prj = ctx.getProject(); + Jdk jdk = prj.projectJdkManager().getOrInstallJdk(requestedJavaVersion); + return resolveInJavaHome("javac", jdk); } @Override diff --git a/src/main/java/dev/jbang/spi/IntegrationManager.java b/src/main/java/dev/jbang/spi/IntegrationManager.java index ced078b72..7a7c9de2d 100644 --- a/src/main/java/dev/jbang/spi/IntegrationManager.java +++ b/src/main/java/dev/jbang/spi/IntegrationManager.java @@ -35,6 +35,7 @@ import dev.jbang.cli.ExitException; import dev.jbang.dependencies.ArtifactInfo; import dev.jbang.dependencies.MavenRepo; +import dev.jbang.jvm.Jdk; import dev.jbang.source.BuildContext; import dev.jbang.source.Project; import dev.jbang.source.Source; @@ -97,7 +98,7 @@ public static IntegrationResult runIntegrations(BuildContext ctx) { IntegrationResult ir = requestedJavaVersion == null || JavaUtil.satisfiesRequestedVersion( requestedJavaVersion, JavaUtil.getCurrentMajorJavaVersion()) ? runIntegrationEmbedded(input, integrationCl) - : runIntegrationExternal(input, prj.getProperties(), requestedJavaVersion); + : runIntegrationExternal(input, prj.getProperties(), prj.projectJdk()); result = result.merged(ir); } } catch (ClassNotFoundException e) { @@ -216,13 +217,13 @@ public void write(int b) { private static IntegrationResult runIntegrationExternal(IntegrationInput input, Map properties, - String requestedJavaVersion) + Jdk jdk) throws Exception { Gson parser = gsonb.create(); Util.infoMsg("Running external post build for " + input.integrationClassName); List args = new ArrayList<>(); - args.add(resolveInJavaHome("java", requestedJavaVersion)); // TODO + args.add(resolveInJavaHome("java", jdk)); // TODO for (Map.Entry entry : properties.entrySet()) { args.add("-D" + entry.getKey() + "=" + entry.getValue()); } diff --git a/src/main/java/dev/jbang/util/JBangFormatter.java b/src/main/java/dev/jbang/util/JBangFormatter.java new file mode 100644 index 000000000..a06188bf0 --- /dev/null +++ b/src/main/java/dev/jbang/util/JBangFormatter.java @@ -0,0 +1,55 @@ +package dev.jbang.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * This JUL Formatter is used to format JUL log messages in such a way that they + * look similar to JBang log messages. + */ +public class JBangFormatter extends Formatter { + private final Date dat = new Date(); + + @Override + public synchronized String format(LogRecord record) { + dat.setTime(record.getMillis()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + String format = "[jbang] "; + if (Util.isVerbose()) { + format += "[%1$tF %1$tT] "; + } + if (record.getLevel().intValue() > Level.INFO.intValue()) { + format += "[%4$s] "; + } + format += "%5$s%n"; + return String.format(format, + dat, + source, + record.getLoggerName(), + record.getLevel().getLocalizedName(), + message, + throwable); + } +} \ No newline at end of file diff --git a/src/main/java/dev/jbang/util/JarUtil.java b/src/main/java/dev/jbang/util/JarUtil.java index 7a92e748e..b0c35eed9 100644 --- a/src/main/java/dev/jbang/util/JarUtil.java +++ b/src/main/java/dev/jbang/util/JarUtil.java @@ -11,23 +11,24 @@ import java.util.jar.Manifest; import dev.jbang.cli.ExitException; +import dev.jbang.jvm.Jdk; public final class JarUtil { private JarUtil() { } - public static void createJar(Path jar, Path src, Manifest manifest, String mainClass, String requestedJavaVersion) + public static void createJar(Path jar, Path src, Manifest manifest, String mainClass, Jdk jdk) throws IOException { - runJarCommand(jar, "c", src, manifest, mainClass, requestedJavaVersion); + runJarCommand(jar, "c", src, manifest, mainClass, jdk); } - public static void updateJar(Path jar, Manifest manifest, String mainClass, String requestedJavaVersion) + public static void updateJar(Path jar, Manifest manifest, String mainClass, Jdk jdk) throws IOException { - runJarCommand(jar, "u", null, manifest, mainClass, requestedJavaVersion); + runJarCommand(jar, "u", null, manifest, mainClass, jdk); } - private static void runJarCommand(Path jar, String action, Path src, Manifest manifest, String mainClass, - String requestedJavaVersion) throws IOException { + private static void runJarCommand(Path jar, String action, Path src, Manifest manifest, String mainClass, Jdk jdk) + throws IOException { assert (action.equals("c") || action.equals("u")); List optionList = new ArrayList<>(); Path tmpManifest = null; @@ -56,7 +57,7 @@ private static void runJarCommand(Path jar, String action, Path src, Manifest ma optionList.add(src.toAbsolutePath().toString()); optionList.add("."); } - runJarCommand(optionList, requestedJavaVersion); + runJarCommand(optionList, jdk); } finally { if (tmpManifest != null) { Util.deletePath(tmpManifest, true); @@ -64,8 +65,8 @@ private static void runJarCommand(Path jar, String action, Path src, Manifest ma } } - private static void runJarCommand(List arguments, String requestedJavaVersion) throws IOException { - arguments.add(0, resolveInJavaHome("jar", requestedJavaVersion)); + private static void runJarCommand(List arguments, Jdk jdk) throws IOException { + arguments.add(0, resolveInJavaHome("jar", jdk)); Util.verboseMsg("Package: " + String.join(" ", arguments)); String out = Util.runCommand(arguments.toArray(new String[] {})); if (out == null) { diff --git a/src/main/java/dev/jbang/util/JavaUtil.java b/src/main/java/dev/jbang/util/JavaUtil.java index c1e92facd..03819ad9e 100644 --- a/src/main/java/dev/jbang/util/JavaUtil.java +++ b/src/main/java/dev/jbang/util/JavaUtil.java @@ -6,34 +6,104 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Comparator; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.Cache; +import dev.jbang.Settings; +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkManager; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.jdkproviders.*; public class JavaUtil { - private static Integer javaVersion; + @Nonnull + public static JdkManager defaultJdkManager(String... names) { + return (new JdkManBuilder()).provider(names).build(); + } - /** - * Returns the actual Java version that's going to be used. It either returns - * the value of the expected version if it is supplied or the version of the JDK - * available in the environment (either JAVA_HOME or what's available on the - * PATH) - * - * @param requestedVersion The Java version requested by the user - * @return The Java version that will be used - */ - public static int javaVersion(String requestedVersion) { - JdkProvider.Jdk jdk = JdkManager.getOrInstallJdk(requestedVersion); - return jdk.getMajorVersion(); + @Nonnull + public static JdkManager defaultJdkManager(List names) { + return (new JdkManBuilder()).provider(names).build(); + } + + public static class JdkManBuilder extends JdkManager.Builder { + private final List providerNames = new ArrayList<>(); + + public static final List PROVIDERS_ALL = Collections.unmodifiableList(Arrays.asList(new String[] { + "current", "default", "javahome", "path", "linked", "jbang", "linux", "sdkman", "scoop" })); + public static final List PROVIDERS_DEFAULT = Collections.unmodifiableList(Arrays.asList(new String[] { + "current", "default", "javahome", "path", "linked", "jbang" })); + + public JdkManager.Builder provider(String... names) { + return provider(names != null ? Arrays.asList(names) : null); + } + + public JdkManager.Builder provider(List names) { + if (names != null) { + for (String providerName : names) { + if (PROVIDERS_ALL.contains(providerName)) { + providerNames.add(providerName); + } + } + } + return this; + } + + public JdkManager build() { + if (providerNames.isEmpty() && providers.isEmpty()) { + provider(PROVIDERS_DEFAULT); + } + for (String providerName : providerNames) { + JdkProvider provider = createProvider(providerName); + if (provider != null && provider.canUse()) { + provider(provider); + } + } + return super.build(); + } + + private JdkProvider createProvider(String providerName) { + JdkProvider provider; + switch (providerName) { + case "current": + provider = new CurrentJdkProvider(); + break; + case "default": + provider = new DefaultJdkProvider(Settings.getDefaultJdkDir()); + break; + case "javahome": + provider = new JavaHomeJdkProvider(); + break; + case "jbang": + provider = new FoojayJdkProvider(Settings.getCacheDir(Cache.CacheClass.jdks)); + break; + case "linked": + provider = new LinkedJdkProvider(Settings.getCacheDir(Cache.CacheClass.jdks)); + break; + case "linux": + provider = new LinuxJdkProvider(); + break; + case "path": + provider = new PathJdkProvider(); + break; + case "scoop": + provider = new ScoopJdkProvider(); + break; + case "sdkman": + provider = new SdkmanJdkProvider(); + break; + default: + Util.warnMsg("Unknown JDK provider: " + providerName); + return null; + } + return provider; + } } /** @@ -117,8 +187,8 @@ public static int getCurrentMajorJavaVersion() { return parseJavaVersion(System.getProperty("java.version")); } - public static String resolveInJavaHome(@Nonnull String cmd, @Nullable String requestedVersion) { - Path jdkHome = JdkManager.getOrInstallJdk(requestedVersion).getHome(); + public static String resolveInJavaHome(@Nonnull String cmd, @Nonnull Jdk jdk) { + Path jdkHome = jdk.getHome(); if (jdkHome != null) { if (Util.isWindows()) { cmd = cmd + ".exe"; diff --git a/src/main/java/dev/jbang/util/Util.java b/src/main/java/dev/jbang/util/Util.java index 20e5d8264..42eda8935 100644 --- a/src/main/java/dev/jbang/util/Util.java +++ b/src/main/java/dev/jbang/util/Util.java @@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.logging.LogManager; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -104,6 +105,9 @@ public static void setVerbose(boolean verbose) { if (verbose) { setQuiet(false); } + LogManager .getLogManager() + .getLogger("") + .setLevel(verbose ? java.util.logging.Level.FINE : java.util.logging.Level.INFO); } public static boolean isVerbose() { @@ -115,6 +119,9 @@ public static void setQuiet(boolean quiet) { if (quiet) { setVerbose(false); } + LogManager .getLogManager() + .getLogger("") + .setLevel(quiet ? java.util.logging.Level.WARNING : java.util.logging.Level.INFO); } public static boolean isQuiet() { @@ -1453,8 +1460,8 @@ public static boolean deletePath(Path path, boolean quiet) { try { if (Files.isDirectory(path)) { verboseMsg("Deleting folder " + path); - Files .walk(path) - .sorted(Comparator.reverseOrder()) + try (Stream s = Files.walk(path)) { + s .sorted(Comparator.reverseOrder()) .forEach(f -> { try { Files.delete(f); @@ -1462,6 +1469,7 @@ public static boolean deletePath(Path path, boolean quiet) { err[0] = e; } }); + } } else if (Files.exists(path)) { verboseMsg("Deleting file " + path); Files.delete(path); diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties new file mode 100644 index 000000000..141c0cdc9 --- /dev/null +++ b/src/main/resources/logging.properties @@ -0,0 +1,4 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=dev.jbang.util.JBangFormatter diff --git a/src/test/java/dev/jbang/BaseTest.java b/src/test/java/dev/jbang/BaseTest.java index 333a2310e..c20433a83 100644 --- a/src/test/java/dev/jbang/BaseTest.java +++ b/src/test/java/dev/jbang/BaseTest.java @@ -10,8 +10,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.concurrent.Callable; import java.util.function.Function; +import java.util.logging.ConsoleHandler; +import java.util.logging.LogManager; +import java.util.logging.Logger; import org.apache.commons.io.output.ByteArrayOutputStream; import org.junit.Rule; @@ -61,6 +65,19 @@ void initEnv(@TempDir Path tempPath) throws IOException { @BeforeAll static void init() throws URISyntaxException, IOException { + try { + // The default ConsoleHandler for logging doesn't like us changing + // System.err out from under it, so we remove it and add our own + LogManager lm = LogManager.getLogManager(); + lm.readConfiguration(BaseTest.class.getResourceAsStream("/logging.properties")); + Logger rl = lm.getLogger(""); + Arrays .stream(rl.getHandlers()) + .filter(h -> h instanceof ConsoleHandler) + .forEach(rl::removeHandler); + rl.addHandler(new JBangHandler()); + } catch (IOException e) { + // Ignore + } mavenTempDir = Files.createTempDirectory("jbang_tests_maven"); jdksTempDir = Files.createTempDirectory("jbang_tests_jdks"); URL examplesUrl = BaseTest.class.getClassLoader().getResource(EXAMPLES_FOLDER); @@ -85,7 +102,8 @@ static void cleanup() { public Path jbangTempDir; public Path cwdDir; - protected CaptureResult checkedRun(Function commandRunner, String... args) throws Exception { + protected CaptureResult checkedRun(Function commandRunner, String... args) + throws Exception { CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs(args); while (pr.subcommand() != null) { pr = pr.subcommand(); diff --git a/src/test/java/dev/jbang/JBangHandler.java b/src/test/java/dev/jbang/JBangHandler.java new file mode 100644 index 000000000..da1058123 --- /dev/null +++ b/src/test/java/dev/jbang/JBangHandler.java @@ -0,0 +1,36 @@ +package dev.jbang; + +import java.io.OutputStream; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + +import dev.jbang.util.JBangFormatter; + +public class JBangHandler extends StreamHandler { + private OutputStream currentOut; + + public JBangHandler() { + super(System.err, new JBangFormatter()); + currentOut = System.err; + } + + @Override + public void publish(LogRecord record) { + updateStream(); + super.publish(record); + flush(); + } + + @Override + public void close() { + updateStream(); + flush(); + } + + private void updateStream() { + if (currentOut != System.err) { + setOutputStream(System.err); + currentOut = System.err; + } + } +} diff --git a/src/test/java/dev/jbang/cli/TestJdk.java b/src/test/java/dev/jbang/cli/TestJdk.java index 7da3d3227..0cee66a90 100644 --- a/src/test/java/dev/jbang/cli/TestJdk.java +++ b/src/test/java/dev/jbang/cli/TestJdk.java @@ -5,8 +5,7 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import java.io.File; import java.io.IOException; @@ -21,8 +20,8 @@ import org.junit.jupiter.api.io.TempDir; import dev.jbang.BaseTest; +import dev.jbang.Cache; import dev.jbang.Settings; -import dev.jbang.net.jdkproviders.JBangJdkProvider; import dev.jbang.util.Util; import picocli.CommandLine; @@ -38,7 +37,7 @@ void initJdk() { @Test void testNoJdksInstalled() throws Exception { - CaptureResult result = checkedRun(jdk -> jdk.list(false, false, FormatMixin.Format.text)); + CaptureResult result = checkedRun(jdk -> jdk.list(false, false, FormatMixin.Format.text)); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), equalTo("No JDKs installed\n")); @@ -46,10 +45,10 @@ void testNoJdksInstalled() throws Exception { @Test void testHasJdksInstalled() throws Exception { - final Path jdkPath = JBangJdkProvider.getJdksPath(); + final Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks); Arrays.asList(11, 12, 13).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.list(false, false, FormatMixin.Format.text)); + CaptureResult result = checkedRun(jdk -> jdk.list(false, false, FormatMixin.Format.text)); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), @@ -65,8 +64,8 @@ void testHasJdksInstalledWithJavaHome() throws Exception { initMockJdkDir(jdkPath, "13.0.7"); environmentVariables.set("JAVA_HOME", jdkPath.toString()); - CaptureResult result = checkedRun((Jdk jdk) -> jdk.list(false, false, FormatMixin.Format.text), - "jdk", "--jdk-providers", "javahome,jbang"); + CaptureResult result = checkedRun((Jdk jdk) -> jdk.list(false, false, FormatMixin.Format.text), + "jdk", "--jdk-providers", "default,javahome,jbang"); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), @@ -77,7 +76,7 @@ void testHasJdksInstalledWithJavaHome() throws Exception { void testDefault() throws Exception { Arrays.asList(11, 12, 13).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.defaultJdk("12")); + CaptureResult result = checkedRun(jdk -> jdk.defaultJdk("12")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), startsWith("[jbang] Default JDK set to 12")); @@ -92,7 +91,7 @@ void testDefault() throws Exception { void testDefaultPlus() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.defaultJdk("16+")); + CaptureResult result = checkedRun(jdk -> jdk.defaultJdk("16+")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), startsWith("[jbang] Default JDK set to 17")); @@ -107,7 +106,7 @@ void testDefaultPlus() throws Exception { void testHome() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.home(null)); + CaptureResult result = checkedRun(jdk -> jdk.home(null)); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), endsWith(File.separator + "currentjdk\n")); @@ -117,7 +116,7 @@ void testHome() throws Exception { void testHomeDefault() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.home("default")); + CaptureResult result = checkedRun(jdk -> jdk.home("default")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), endsWith(File.separator + "currentjdk\n")); @@ -127,7 +126,7 @@ void testHomeDefault() throws Exception { void testHomeWithVersion() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.home("17")); + CaptureResult result = checkedRun(jdk -> jdk.home("17")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), endsWith("cache" + File.separator + "jdks" + File.separator + "17\n")); @@ -137,7 +136,7 @@ void testHomeWithVersion() throws Exception { void testHomePlus() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.home("16+")); + CaptureResult result = checkedRun(jdk -> jdk.home("16+")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), endsWith("cache" + File.separator + "jdks" + File.separator + "17\n")); @@ -147,7 +146,7 @@ void testHomePlus() throws Exception { void testJavaEnv() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv(null)); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv(null)); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), @@ -169,7 +168,7 @@ void testJavaEnv() throws Exception { void testJavaEnvDefault() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv("default")); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv("default")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), containsString(File.separator + "currentjdk")); @@ -179,7 +178,7 @@ void testJavaEnvDefault() throws Exception { void testJavaEnvWithVersion() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv("17")); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv("17")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), containsString("cache" + File.separator + "jdks" + File.separator + "17")); @@ -189,7 +188,7 @@ void testJavaEnvWithVersion() throws Exception { void testJavaEnvWithDefaultVersion() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv("11")); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv("11")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), containsString("cache" + File.separator + "jdks" + File.separator + "11")); @@ -199,7 +198,7 @@ void testJavaEnvWithDefaultVersion() throws Exception { void testJavaRuntimeVersion() throws Exception { Arrays.asList(21).forEach(this::createMockJdkRuntime); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv("21")); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv("21")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), containsString("cache" + File.separator + "jdks" + File.separator + "21")); @@ -209,7 +208,7 @@ void testJavaRuntimeVersion() throws Exception { void testJavaEnvPlus() throws Exception { Arrays.asList(11, 14, 17).forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> jdk.javaEnv("16+")); + CaptureResult result = checkedRun(jdk -> jdk.javaEnv("16+")); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedOut(), containsString("cache" + File.separator + "jdks" + File.separator + "17")); @@ -224,8 +223,8 @@ void testDefaultWithJavaHome() throws Exception { initMockJdkDir(jdkPath, "12.0.7"); environmentVariables.set("JAVA_HOME", jdkPath.toString()); - CaptureResult result = checkedRun((Jdk jdk) -> jdk.defaultJdk("12"), "jdk", "--jdk-providers", - "javahome,jbang"); + CaptureResult result = checkedRun((Jdk jdk) -> jdk.defaultJdk("12"), "jdk", "--jdk-providers", + "default,javahome,jbang"); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), startsWith("[jbang] Default JDK set to 12")); @@ -242,7 +241,7 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenPathIsInvalid() { try { jdk.install(true, "11", "/non-existent-path"); } catch (Exception e) { - assertTrue(e instanceof ExitException); + assertInstanceOf(ExitException.class, e); assertEquals("Unable to resolve path as directory: /non-existent-path", e.getMessage()); } return null; @@ -253,10 +252,10 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenPathIsInvalid() { void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionDoesNotExist(@TempDir File javaDir) throws Exception { initMockJdkDir(javaDir.toPath(), "11.0.14"); - final Path jdkPath = JBangJdkProvider.getJdksPath(); + final Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks); jdkPath.toFile().mkdir(); - CaptureResult result = checkedRun(jdk -> { + CaptureResult result = checkedRun(jdk -> { try { return jdk.install(false, "11", javaDir.toPath().toString()); } catch (IOException e) { @@ -267,7 +266,7 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionDoesNotExi assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), - equalTo("[jbang] JDK 11 has been linked to: " + javaDir.toPath().toString() + "\n")); + equalTo("[jbang] JDK 11 has been linked to: " + javaDir.toPath() + "\n")); assertTrue(Util.isLink(jdkPath.resolve("11"))); System.err.println("ASSERT: " + javaDir.toPath() + " - " + jdkPath.resolve("11").toRealPath()); assertTrue(Files.isSameFile(javaDir.toPath(), jdkPath.resolve("11").toRealPath())); @@ -277,11 +276,11 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionDoesNotExi void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionExistsAndInstallIsForced( @TempDir File javaDir) throws Exception { initMockJdkDir(javaDir.toPath(), "11.0.14"); - final Path jdkPath = JBangJdkProvider.getJdksPath(); + final Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks); Arrays .asList(11) .forEach(this::createMockJdk); - CaptureResult result = checkedRun(jdk -> { + CaptureResult result = checkedRun(jdk -> { try { return jdk.install(true, "11", javaDir.toPath().toString()); } catch (IOException e) { @@ -305,7 +304,7 @@ void testJdkInstallWithLinkingToExistingJdkPathWithDifferentVersion(@TempDir Fil try { jdk.install(true, "13", javaDir.toPath().toString()); } catch (Exception e) { - assertTrue(e instanceof ExitException); + assertInstanceOf(ExitException.class, e); assertEquals("Java version in given path: " + javaDir.toPath() + " is " + 11 + " which does not match the requested version " + 13 + "", e.getMessage()); } @@ -322,7 +321,7 @@ void testJdkInstallWithLinkingToExistingJdkPathWithNoVersion(@TempDir File javaD try { jdk.install(true, "13", javaDir.toPath().toString()); } catch (Exception e) { - assertTrue(e instanceof ExitException); + assertInstanceOf(ExitException.class, e); assertEquals("Unable to determine Java version in given path: " + javaDir.toPath(), e.getMessage()); } return null; @@ -336,9 +335,9 @@ void testJdkInstallWithLinkingToExistingBrokenLink( Path jdkOk = javaDir.toPath().resolve("14ok"); initMockJdkDir(jdkBroken, "11.0.14-broken"); initMockJdkDir(jdkOk, "11.0.14-ok"); - final Path jdkPath = JBangJdkProvider.getJdksPath(); + final Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks); - CaptureResult result = checkedRun(jdk -> { + CaptureResult result = checkedRun(jdk -> { try { return jdk.install(true, "11", jdkBroken.toString()); } catch (IOException e) { @@ -372,7 +371,7 @@ void testExistingJdkUninstall() throws Exception { int jdkVersion = 14; createMockJdk(jdkVersion); - CaptureResult result = checkedRun(jdk -> jdk.uninstall(Integer.toString(jdkVersion))); + CaptureResult result = checkedRun(jdk -> jdk.uninstall(Integer.toString(jdkVersion))); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), @@ -386,12 +385,11 @@ void testExistingJdkUninstallWithJavaHome() throws Exception { int jdkVersion = 14; createMockJdk(jdkVersion); - Path jdkPath = JBangJdkProvider.getJdksPath().resolve("14"); + Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks).resolve("14"); environmentVariables.set("JAVA_HOME", jdkPath.toString()); - CaptureResult result = checkedRun((Jdk jdk) -> jdk.uninstall(Integer.toString(jdkVersion)), "jdk", - "--jdk-providers", - "javahome,jbang"); + CaptureResult result = checkedRun((Jdk jdk) -> jdk.uninstall(Integer.toString(jdkVersion)), "jdk", + "--jdk-providers", "default,javahome,jbang"); assertThat(result.result, equalTo(SUCCESS_EXIT)); assertThat(result.normalizedErr(), @@ -406,15 +404,15 @@ void testNonExistingJdkUninstall() throws IOException { try { jdk.uninstall("16"); } catch (Exception e) { - assertTrue(e instanceof ExitException); + assertInstanceOf(ExitException.class, e); assertEquals("JDK 16 is not installed", e.getMessage()); } return null; }); } - private CaptureResult checkedRun(Function commandRunner) throws Exception { - return checkedRun(commandRunner, "jdk", "--jdk-providers", "default,jbang"); + private CaptureResult checkedRun(Function commandRunner) throws Exception { + return checkedRun(commandRunner, "jdk", "--jdk-providers", "default,jbang,linked"); } private void checkedRunWithException(Function commandRunner) { @@ -434,9 +432,9 @@ private void createMockJdkRuntime(int jdkVersion) { } private void createMockJdk(int jdkVersion, BiConsumer init) { - Path jdkPath = JBangJdkProvider.getJdksPath().resolve(String.valueOf(jdkVersion)); + Path jdkPath = Settings.getCacheDir(Cache.CacheClass.jdks).resolve(String.valueOf(jdkVersion)); init.accept(jdkPath, jdkVersion + ".0.7"); - Path link = Settings.getCurrentJdkDir(); + Path link = Settings.getDefaultJdkDir(); if (!Files.exists(link)) { Util.createLink(link, jdkPath); } diff --git a/src/test/java/dev/jbang/cli/TestRun.java b/src/test/java/dev/jbang/cli/TestRun.java index 4b8924ff0..177745f20 100644 --- a/src/test/java/dev/jbang/cli/TestRun.java +++ b/src/test/java/dev/jbang/cli/TestRun.java @@ -5,6 +5,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static dev.jbang.source.Project.ATTR_AGENT_CLASS; import static dev.jbang.source.Project.ATTR_PREMAIN_CLASS; +import static dev.jbang.util.JavaUtil.defaultJdkManager; import static dev.jbang.util.Util.writeString; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.nullValue; @@ -73,7 +74,6 @@ import dev.jbang.catalog.Alias; import dev.jbang.catalog.Catalog; import dev.jbang.catalog.CatalogUtil; -import dev.jbang.net.JdkManager; import dev.jbang.net.TrustedSources; import dev.jbang.source.BuildContext; import dev.jbang.source.Builder; @@ -2292,7 +2292,7 @@ void testReposWorksWithFresh() throws IOException { @Test void testForceJavaVersion() throws IOException { - int v = JdkManager.getJdk(null, false).getMajorVersion(); + int v = defaultJdkManager().getJdk(null, false).getMajorVersion(); String arg = examplesTestFolder.resolve("java4321.java").toAbsolutePath().toString(); CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", "--java", "" + v, arg); Run run = (Run) pr.subcommand().commandSpec().userObject(); diff --git a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java index ac64602f4..f6408de86 100644 --- a/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java +++ b/src/test/java/dev/jbang/dependencies/DependencyResolverTest.java @@ -1,6 +1,7 @@ package dev.jbang.dependencies; import static dev.jbang.dependencies.DependencyUtil.toMavenRepo; +import static dev.jbang.util.JavaUtil.defaultJdkManager; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -22,16 +23,17 @@ import java.util.Properties; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import dev.jbang.BaseTest; import dev.jbang.Settings; -import dev.jbang.net.JdkManager; -import dev.jbang.net.JdkProvider; +import dev.jbang.jvm.Jdk; import dev.jbang.util.PropertiesValueResolver; import dev.jbang.util.Util; +@Disabled class DependencyResolverTest extends BaseTest { @Test @@ -189,12 +191,12 @@ void testResolveJavaModules() throws IOException { DependencyUtil .resolveDependencies(deps, Collections.emptyList(), false, false, false, true, false) .getArtifacts()) { @Override - protected boolean supportsModules(JdkProvider.Jdk jdk) { + protected boolean supportsModules(Jdk jdk) { return true; } }; - List ma = cp.getAutoDectectedModuleArguments(JdkManager.getOrInstallJdk(null)); + List ma = cp.getAutoDectectedModuleArguments(defaultJdkManager().getOrInstallJdk(null)); assertThat(ma, hasItem("--module-path"));