diff --git a/build.gradle b/build.gradle index 0a443de16..45d6d483d 100644 --- a/build.gradle +++ b/build.gradle @@ -18,20 +18,85 @@ plugins { id 'maven-publish' } -javadoc { - options.encoding = 'UTF-8' - //remove this to see all the missing tags/parameters. - options.addStringOption('Xdoclint:none', '-quiet') -} +allprojects { + //apply plugin: "java" + apply plugin: "com.diffplug.spotless" + + sourceCompatibility = '8' + targetCompatibility = '8' + + spotless { + lineEndings 'UNIX' + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + targetExclude 'CONTRIBUTORS.md', 'src/main/scripts/container/README.md', 'build/**/*', 'out/**/*' + // all-contributor bot adds non-indented code + trimTrailingWhitespace() + indentWithTabs(4) // or spaces. Takes an integer argument if you don't like 4 + endWithNewline() + } + java { + importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' + removeUnusedImports() + eclipse().configFile new File(rootProject.projectDir, "misc/eclipse_formatting_nowrap.xml") + targetExclude 'build/**/*' + } + format 'xml', { + targetExclude 'build/test-results', fileTree('.idea') + target '**/*.xml', '**/*.nuspec' + } + } -repositories { - mavenCentral() - //maven { url 'https://jitpack.io' } -} + javadoc { + options.encoding = 'UTF-8' + //remove this to see all the missing tags/parameters. + options.addStringOption('Xdoclint:none', '-quiet') + } + + repositories { + mavenCentral() + //maven { url 'https://jitpack.io' } + } -java { - withJavadocJar() - withSourcesJar() + java { + withJavadocJar() + withSourcesJar() + } + + test { + useJUnitPlatform() + jvmArgs = [ + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ] + //testLogging.showStandardStreams = true + + /*testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + }*/ + //timeout.set(Duration.ofSeconds(60)) + + jacoco { + enabled = false + } + } + + jacoco { + toolVersion = '0.8.7' + } + + jacocoTestReport { + afterEvaluate { + executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec") + } + + reports { + html.required = true + xml.required = true + csv.required = false + } + } } publishing { @@ -77,6 +142,22 @@ publishing { url = layout.buildDirectory.dir('staging-deploy') } } + + // to enable reproducible builds + tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true + } + + compileJava { + options.encoding = 'UTF-8' + options.compilerArgs << "-Xlint:unchecked" + } + + compileTestJava { + options.encoding = 'UTF-8' + options.compilerArgs << "-Xlint:unchecked" + } } sourceSets { @@ -90,6 +171,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 +193,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" @@ -126,12 +210,6 @@ buildConfig { buildConfigField('String', 'VERSION', provider { "\"${project.version}\"" }) } -// to enable reproducible builds -tasks.withType(AbstractArchiveTask) { - preserveFileTimestamps = false - reproducibleFileOrder = true -} - sonarqube { properties { property "sonar.projectKey", "jbangdev_jbang" @@ -140,28 +218,6 @@ sonarqube { } } -spotless { - lineEndings 'UNIX' - format 'misc', { - target '**/*.gradle', '**/*.md', '**/.gitignore' - targetExclude 'CONTRIBUTORS.md', 'src/main/scripts/container/README.md', 'build/**/*', 'out/**/*' - // all-contributor bot adds non-indented code - trimTrailingWhitespace() - indentWithTabs(4) // or spaces. Takes an integer argument if you don't like 4 - endWithNewline() - } - java { - importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' - removeUnusedImports() - eclipse().configFile "misc/eclipse_formatting_nowrap.xml" - targetExclude 'build/**/*' - } - format 'xml', { - targetExclude 'build/test-results', fileTree('.idea') - target '**/*.xml', '**/*.nuspec' - } -} - task versionTxt() { doLast { new File(project.buildDir, "tmp/version.txt").text = project.version @@ -218,16 +274,6 @@ jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } -compileJava { - options.encoding = 'UTF-8' - options.compilerArgs << "-Xlint:unchecked" -} - -compileTestJava { - options.encoding = 'UTF-8' - options.compilerArgs << "-Xlint:unchecked" -} - compileJava9Java { sourceCompatibility = 9 targetCompatibility = 9 @@ -251,42 +297,6 @@ shadowJar { archiveFileName = "${archiveBaseName.get()}.${archiveExtension.get()}" } -test { - useJUnitPlatform() - jvmArgs = [ - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED" - ] - //testLogging.showStandardStreams = true - - /*testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - }*/ - //timeout.set(Duration.ofSeconds(60)) - - jacoco { - enabled = false - } -} - -jacoco { - toolVersion = '0.8.7' -} - -jacocoTestReport { - afterEvaluate { - executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec") - } - - reports { - html.required = true - xml.required = true - csv.required = false - } -} - - task karateExecute(type: JavaExec) { classpath = sourceSets.test.runtimeClasspath mainClass = System.properties.getProperty('mainClass') @@ -435,5 +445,3 @@ tasks.named("spotlessXml").configure { dependsOn("chocolatey") } tasks.named("spotlessXml").configure { dependsOn("copyITests") } group = "dev.jbang" -sourceCompatibility = '8' -targetCompatibility = '8' diff --git a/itests/javaversion.feature b/itests/javaversion.feature index f040d397c..3068e7aae 100644 --- a/itests/javaversion.feature +++ b/itests/javaversion.feature @@ -2,9 +2,9 @@ Feature: java version control Scenario: java run non existent //java When command('jbang --verbose java4321.java') - Then match err contains "JDK version is not available for installation: 4321" + Then match err contains "No suitable JDK was found for requested version: 4321" Scenario: java run with explicit java 8 When command('jbang --verbose --java 8 java4321.java') - Then match err !contains "JDK version is not available for installation: 4321" \ No newline at end of file + Then match err !contains "No suitable JDK was found for requested version: 4321" \ No newline at end of file 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..e6efbbebc --- /dev/null +++ b/jdkmanager/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'java' +} + +group = 'dev.jbang.jvm' +version = parent.version + +sourceCompatibility = '8' +targetCompatibility = '8' + +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' + testImplementation "org.hamcrest:hamcrest-library:2.2" + testImplementation "com.github.stefanbirkner:system-rules:1.17.2" +} + +test { + useJUnitPlatform() + jvmArgs = [ + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ] +} + + +publishing { + publications { + jdkmanager(MavenPublication) { + groupId = 'dev.jbang' + artifactId = 'jdkmanager' + + from components.java + + pom { + name = 'JBang JDK Manager' + description = 'Library for managing JDK installations' + url = 'https://jbang.dev' + inceptionYear = '2025' + licenses { + license { + name = 'MIT' + url = 'https://github.com/jbangdev/jbang/blob/main/LICENSE' + } + } + developers { + developer { + id = 'maxandersen' + name = 'Max Rydahl Andersen' + } + developer { + id = 'quintesse' + name = 'Tako Schotanus' + } + } + scm { + connection = 'scm:git:https://github.com/jbangdev/jbang' + developerConnection = 'scm:git:https://github.com/jbangdev/jbang' + url = 'http://github.com/jbangdev/jbang' + } + } + } + } +} 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..9200d50a3 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/Jdk.java @@ -0,0 +1,174 @@ +package dev.jbang.jvm; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.util.JavaUtils; + +public interface Jdk extends Comparable { + @NonNull + JdkProvider provider(); + + @NonNull + String id(); + + @NonNull + String version(); + + @NonNull + Path home(); + + int majorVersion(); + + @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 provider() { + return provider; + } + + /** + * Returns the id that is used to uniquely identify this JDK across all + * providers + */ + @Override + @NonNull + public String id() { + return id; + } + + /** Returns the JDK's version */ + @Override + @NonNull + public String version() { + 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 + @NonNull + public Path home() { + if (home == null) { + throw new IllegalStateException("Trying to retrieve home folder for uninstalled JDK"); + } + return home; + } + + @Override + public int majorVersion() { + return JavaUtils.parseJavaVersion(version()); + } + + @Override + @NonNull + public Jdk install() { + if (!provider.canUpdate()) { + throw new UnsupportedOperationException("Installing a JDK is not supported by " + provider); + } + 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(majorVersion(), o.majorVersion()); + } + + @Override + public String toString() { + return majorVersion() + " (" + version + ", " + id + ", " + home + ")"; + } + } + + class Predicates { + public static final Predicate all = provider -> true; + + public static Predicate exactVersion(int version) { + return jdk -> jdk.majorVersion() == version; + } + + public static Predicate openVersion(int version) { + return jdk -> jdk.majorVersion() >= version; + } + + public static Predicate forVersion(String version) { + int v = JavaUtils.parseJavaVersion(version); + return forVersion(v, JavaUtils.isOpenVersion(version)); + } + + public static Predicate forVersion(int version, boolean openVersion) { + return openVersion ? openVersion(version) : exactVersion(version); + } + + public static Predicate id(String id) { + return jdk -> jdk.id().equals(id); + } + + public static Predicate path(Path jdkPath) { + return jdk -> jdk.isInstalled() && jdkPath.startsWith(jdk.home()); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/JdkDiscovery.java b/jdkmanager/src/main/java/dev/jbang/jvm/JdkDiscovery.java new file mode 100644 index 000000000..70c280214 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkDiscovery.java @@ -0,0 +1,60 @@ +package dev.jbang.jvm; + +import static dev.jbang.jvm.util.FileUtils.deleteOnExit; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This interface gives JDK providers the ability to be discovered and + * instantiated by code that doesn't know about the specific API the provider + * implements. See {@link JdkProviders} for a possible implementation of the + * discovery mechanism. + */ +public interface JdkDiscovery { + @NonNull + String name(); + + @Nullable + JdkProvider create(Config config); + + class Config { + @NonNull + public final Path installPath; + @NonNull + public final Path cachePath; + @NonNull + public final Map properties; + + public Config(@NonNull Path installPaths) { + this(installPaths, null, null); + } + + public Config(@NonNull Path installPath, @Nullable Path cachePath, @Nullable Map properties) { + this.installPath = installPath; + if (cachePath == null) { + try { + this.cachePath = deleteOnExit(Files.createTempDirectory("jdk-provider-cache")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + this.cachePath = cachePath; + } + this.properties = new HashMap<>(); + if (properties != null) { + this.properties.putAll(properties); + } + } + + public Config copy() { + return new Config(installPath, cachePath, properties); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/JdkInstaller.java b/jdkmanager/src/main/java/dev/jbang/jvm/JdkInstaller.java new file mode 100644 index 000000000..6f6ccf871 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkInstaller.java @@ -0,0 +1,73 @@ +package dev.jbang.jvm; + +import java.nio.file.Path; +import java.util.List; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * This interface must be implemented by installers that are able to install + * JDKs on the user's system. They should be able to install and uninstall them + * at the user's request. + */ +public interface JdkInstaller { + + /** + * This method returns a set of JDKs that are available for installation. + * Implementations 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 installer 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 installer-specific + * matching logic. NB: In special cases, depending on the installer, 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.getJdkBy(listAvailable().stream(), Jdk.Predicates.id(idOrToken)).orElse(null); + } + + /** + * Installs the indicated JDK + * + * @param jdk The Jdk object of the JDK to install + * @param installDir The path where the JDK should be installed + * @return A Jdk object + * @throws UnsupportedOperationException if the provider can not update + */ + @NonNull + default Jdk install(@NonNull Jdk jdk, Path installDir) { + 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()); + } +} 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..84d53e7bd --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkManager.java @@ -0,0 +1,556 @@ +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.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.jdkproviders.*; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; + +public class JdkManager { + public static final int DEFAULT_JAVA_VERSION = 21; + private static final Logger LOGGER = Logger.getLogger(JdkManager.class.getName()); + + private final List providers; + private final int defaultJavaVersion; + + private final JdkProvider defaultProvider; + + public static JdkManager create() { + Path installPath = JBangJdkProvider.getJBangJdkDir(); + JdkDiscovery.Config cfg = new JdkDiscovery.Config(installPath); + return builder() + .providers(JdkProviders.instance().all(cfg)) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + protected final List providers = new ArrayList<>(); + protected int defaultJavaVersion = DEFAULT_JAVA_VERSION; + + protected Builder() { + } + + public Builder providers(JdkProvider... provs) { + return providers(Arrays.asList(provs)); + } + + public Builder providers(List provs) { + providers.addAll(provs); + return this; + } + + public Builder defaultJavaVersion(int defaultJavaVersion) { + if (defaultJavaVersion < 1) { + throw new IllegalArgumentException("Default Java version must be at least 1"); + } + 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) { + assert defaultJavaVersion > 0; + this.providers = Collections.unmodifiableList(providers); + this.defaultJavaVersion = defaultJavaVersion; + this.defaultProvider = provider("default"); + for (JdkProvider provider : providers) { + provider.manager(this); + } + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine( + "Using JDK provider(s): " + + providers .stream() + .map(p -> p.getClass().getSimpleName()) + .collect(Collectors.joining(", "))); + } + } + + @NonNull + private Stream providers(Predicate providerFilter) { + return providers.stream().filter(providerFilter); + } + + @Nullable + private JdkProvider provider(String name) { + return providers(JdkProvider.Predicates.name(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), + JdkProvider.Predicates.all); + } else { + return getOrInstallJdkById(versionOrId, JdkProvider.Predicates.all); + } + } else { + return getOrInstallJdkByVersion(0, true, JdkProvider.Predicates.all); + } + } + + /** + * 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 providerFilter Only return JDKs from providers that match the filter + * @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, @NonNull Predicate providerFilter) { + LOGGER.log(Level.FINE, "Looking for JDK: {0}", requestedVersion); + Jdk jdk = getJdkByVersion(requestedVersion, openVersion, providerFilter); + 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 providerFilter Only return JDKs from providers that match the filter + * @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, @NonNull Predicate providerFilter) { + LOGGER.log(Level.FINE, "Looking for JDK: {0}", requestedId); + Jdk jdk = getJdkById(requestedId, providerFilter); + 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 the + * app 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 + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + public Jdk getJdk(@Nullable String versionOrId) { + return getJdk(versionOrId, JdkProvider.Predicates.all); + } + + /** + * 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 the + * app 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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + public Jdk getJdk(@Nullable String versionOrId, @NonNull Predicate providerFilter) { + if (versionOrId != null) { + if (JavaUtils.isRequestedVersion(versionOrId)) { + return getJdkByVersion( + JavaUtils.minRequestedVersion(versionOrId), + JavaUtils.isOpenVersion(versionOrId), + providerFilter); + } else { + return getJdkById(versionOrId, providerFilter); + } + } else { + return getJdkByVersion(0, true, providerFilter); + } + } + + /** + * 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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + @Nullable + private Jdk getJdkByVersion(int requestedVersion, boolean openVersion, + @NonNull Predicate providerFilter) { + Jdk jdk = getInstalledJdkByVersion(requestedVersion, openVersion, providerFilter); + if (jdk == null) { + if (requestedVersion > 0 + && (requestedVersion >= defaultJavaVersion || !openVersion)) { + jdk = getAvailableJdkByVersion(requestedVersion, openVersion); + } else { + jdk = getJdkByVersion(defaultJavaVersion, openVersion, providerFilter); + if (jdk == null) { + // If we can't find the default version or higher, + // we'll just find the highest version available + jdk = prevAvailableJdk(defaultJavaVersion).orElse(null); + } + } + } + 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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + * @throws IllegalArgumentException If no JDK could be found at all + */ + private @Nullable Jdk getJdkById(@NonNull String requestedId, @NonNull Predicate providerFilter) { + Jdk jdk = getInstalledJdkById(requestedId, providerFilter); + 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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + */ + @Nullable + public Jdk getInstalledJdk(String versionOrId, @NonNull Predicate providerFilter) { + if (versionOrId != null) { + if (JavaUtils.isRequestedVersion(versionOrId)) { + return getInstalledJdkByVersion( + JavaUtils.minRequestedVersion(versionOrId), + JavaUtils.isOpenVersion(versionOrId), + providerFilter); + } else { + return getInstalledJdkById(versionOrId, providerFilter); + } + } else { + return getInstalledJdkByVersion(0, true, providerFilter); + } + } + + /** + * 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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + */ + @Nullable + private Jdk getInstalledJdkByVersion(int version, boolean openVersion, + @NonNull Predicate providerFilter) { + return providers(providerFilter) + .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 providerFilter Only return JDKs from providers that match the filter + * @return A Jdk object or null + */ + @Nullable + private Jdk getInstalledJdkById(String requestedId, @NonNull Predicate providerFilter) { + return providers(providerFilter) + .map(p -> p.getInstalledById(requestedId)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @Nullable + private Jdk getAvailableJdkByVersion(int version, boolean openVersion) { + Optional jdk = getJdkBy(listAvailableJdks().stream(), Jdk.Predicates.forVersion(version, openVersion)); + return jdk.orElse(null); + } + + @Nullable + private Jdk getAvailableJdkById(String id) { + Optional jdk = getJdkBy(listAvailableJdks().stream(), Jdk.Predicates.id(id)); + return jdk.orElse(null); + } + + 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.home() + .getParent() + .resolve("_delete_me_" + jdk.home().getFileName().toString()); + Files.move(jdk.home(), jdkTmpDir); + Files.move(jdkTmpDir, jdk.home()); + } 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.home(); + try { + resetDefault = Files.isSameFile(defHome, jdk.home()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Error while trying to reset default JDK", ex); + resetDefault = defHome.equals(jdk.home()); + } + } + + if (jdk.isInstalled()) { + FileUtils.deletePath(jdk.home()); + LOGGER.log( + Level.INFO, + "JDK {0} has been uninstalled", + new Object[] { jdk.id() }); + } + + if (resetDefault) { + Optional newjdk = nextInstalledJdk(jdk.majorVersion(), JdkProvider.Predicates.canUpdate); + if (!newjdk.isPresent()) { + newjdk = prevInstalledJdk(jdk.majorVersion(), JdkProvider.Predicates.canUpdate); + } + if (newjdk.isPresent()) { + setDefaultJdk(newjdk.get()); + } else { + removeDefaultJdk(); + LOGGER.log(Level.INFO, "Default JDK unset"); + } + } + } + + /** + * Links a 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 jdkPath path to the pre-installed JDK. + * @param id id for the new JDK. + */ + public void linkToExistingJdk(Path jdkPath, String id) { + JdkProvider linked = provider(LinkedJdkProvider.Discovery.PROVIDER_ID); + if (linked == null) { + return; + } + if (!Files.isDirectory(jdkPath)) { + throw new IllegalArgumentException("Unable to resolve path as directory: " + jdkPath); + } + Jdk linkedJdk = linked.getAvailableByIdOrToken(id + "@" + jdkPath); + if (linkedJdk == null) { + throw new IllegalArgumentException("Unable to create link to JDK in path: " + jdkPath); + } + LOGGER.log(Level.FINE, "Linking JDK: {0} to {1}", new Object[] { id, jdkPath }); + 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 providerFilter Only return JDKs from providers that match the filter + * @return an optional JDK + */ + private Optional nextInstalledJdk(int minVersion, @NonNull Predicate providerFilter) { + return listInstalledJdks(providerFilter) + .filter(jdk -> jdk.majorVersion() >= 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 providerFilter Only return JDKs from providers that match the filter + * @return an optional JDK + */ + private Optional prevInstalledJdk(int maxVersion, @NonNull Predicate providerFilter) { + return listInstalledJdks(providerFilter) + .filter(jdk -> jdk.majorVersion() <= maxVersion) + .max(Jdk::compareTo); + } + + /** + * Returns an available 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 + * @return an optional JDK + */ + private Optional prevAvailableJdk(int maxVersion) { + return listAvailableJdks() + .stream() + .filter(jdk -> jdk.majorVersion() <= maxVersion) + .max(Jdk::compareTo); + } + + public List listAvailableJdks() { + return providers(JdkProvider.Predicates.canUpdate) + .flatMap(p -> p.listAvailable().stream()) + .collect(Collectors.toList()); + } + + public List listInstalledJdks() { + return listInstalledJdks(JdkProvider.Predicates.all).sorted().collect(Collectors.toList()); + } + + private Stream listInstalledJdks(Predicate providerFilter) { + return providers(providerFilter).flatMap(p -> p.listInstalled().stream()); + } + + public boolean hasDefaultProvider() { + return defaultProvider != null; + } + + @Nullable + public Jdk getDefaultJdk() { + return hasDefaultProvider() ? defaultProvider.getInstalledById(DefaultJdkProvider.Discovery.PROVIDER_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.Discovery.PROVIDER_ID, jdk.home(), + jdk.version()); + 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 providers(JdkProvider.Predicates.canUpdate).anyMatch(p -> p.getInstalledByPath(currentJdk) != null); + } + + @NonNull + static Optional getJdkBy(@NonNull Stream jdks, @NonNull Predicate jdkFilter) { + return jdks.filter(jdkFilter).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..b463655ea --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkProvider.java @@ -0,0 +1,202 @@ +package dev.jbang.jvm; + +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; + +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 an important rule: they must + * be unique across implementations. + */ +public interface JdkProvider { + + default Jdk createJdk(@NonNull String id, @Nullable Path home, @NonNull String version) { + return new Jdk.Default(this, id, home, version); + } + + @NonNull + JdkManager manager(); + + void manager(@NonNull JdkManager manager); + + /** + * Returns the name of the provider. This name should be unique across all + * providers and consist only of lowercase letters and numbers. + * + * @return The name of the provider + */ + @NonNull + String name(); + + /** + * Returns a description of the provider. + * + * @return The description of the provider + */ + @NonNull + String description(); + + /** + * 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 .getJdkBy(listInstalled().stream(), Jdk.Predicates.forVersion(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) { + if (isValidId(id)) { + return JdkManager.getJdkBy(listInstalled().stream(), Jdk.Predicates.id(id)).orElse(null); + } + return 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.getJdkBy(listInstalled().stream(), Jdk.Predicates.path(jdkPath)).orElse(null); + } + + /** + * Determines if the given id is a valid JDK id for this provider. + * + * @param id The id to validate + * @return True if the id is valid, false otherwise + */ + default boolean isValidId(@NonNull String id) { + return name().equals(id); + } + + /** + * 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. If true the + * provider must also implement the JdkInstaller interface. + * + * @return True if JDKs can be (un)installed, false otherwise + */ + default boolean canUpdate() { + return false; + } + + /** + * This method returns a set of JDKs that are available for installation. + * Implementations 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.getJdkBy(listAvailable().stream(), Jdk.Predicates.id(idOrToken)).orElse(null); + } + + /** + * 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) { + if (!canUpdate()) { + throw new UnsupportedOperationException( + "Uninstalling a JDK is not supported by " + getClass().getName()); + } + manager().uninstallJdk(jdk); + } + + class Predicates { + public static final Predicate all = provider -> true; + public static final Predicate canUpdate = JdkProvider::canUpdate; + + public static Predicate name(String name) { + return provider -> provider.name().equalsIgnoreCase(name); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/JdkProviders.java b/jdkmanager/src/main/java/dev/jbang/jvm/JdkProviders.java new file mode 100644 index 000000000..33d19f706 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/JdkProviders.java @@ -0,0 +1,130 @@ +package dev.jbang.jvm; + +import java.nio.file.Paths; +import java.util.*; +import java.util.function.BiFunction; + +public class JdkProviders { + private List discoveries; + + private static final JdkProviders INSTANCE = new JdkProviders(); + + private JdkProviders() { + } + + public static JdkProviders instance() { + return INSTANCE; + } + + /** + * Returns an ordered list of names of the minimal set of providers that can be + * used to find JDKs already available in the user's environment. This does + * specifically not include providers that can install JDKs, nor does it include + * 3rd party or platform providers. + * + * @return a list of provider names + */ + public List minimalNames() { + return Arrays.asList("current", "javahome", "path"); + } + + /** + * Returns an ordered list of names of a basic set of providers that are needed + * to deliver proper functionality for the JBang tool. TODO: Remove this once + * JBang defaults to all() + * + * @return a list of provider names + */ + public List basicNames() { + return Arrays.asList("current", "default", "javahome", "path", "linked", "jbang"); + } + + /** + * Returns an ordered list of names of all available providers. The list will + * always start with the names returned by {@link #basicNames()} and then + * followed by the names of any remaining providers. + * + * @return a list of provider names + */ + public List allNames() { + LinkedHashSet names = new LinkedHashSet<>(basicNames()); + ArrayList sorted = new ArrayList<>(); + for (JdkDiscovery discovery : discoveries()) { + sorted.add(discovery.name()); + } + names.addAll(sorted); + return new ArrayList<>(names); + } + + public List minimal() { + JdkDiscovery.Config cfg = new JdkDiscovery.Config(Paths.get("")); + return parseNames(cfg, minimalNames().toArray(new String[0])); + } + + public List basic(JdkDiscovery.Config config) { + return parseNames(config, basicNames().toArray(new String[0])); + } + + public List all(JdkDiscovery.Config config) { + return parseNames(config, allNames().toArray(new String[0])); + } + + public List parseNames(JdkDiscovery.Config config, String names) { + return parseNames(config, names.split(",")); + } + + public List parseNames(JdkDiscovery.Config config, String... names) { + ArrayList providers = new ArrayList<>(); + if (names != null) { + for (String nameAndConfig : names) { + JdkProvider provider = parseName(config, nameAndConfig); + if (provider != null) { + providers.add(provider); + } + } + } + return providers; + } + + public JdkProvider parseName(JdkDiscovery.Config config, String nameAndConfig) { + return parseName(config, nameAndConfig, this::byName); + } + + JdkProvider parseName(JdkDiscovery.Config config, String nameAndConfig, + BiFunction action) { + String[] parts = nameAndConfig.split(";"); + String name = parts[0]; + JdkDiscovery.Config providerConfig = config.copy(); + for (int i = 1; i < parts.length; i++) { + String[] keyValue = parts[i].split("="); + if (keyValue.length == 2) { + providerConfig.properties.put(keyValue[0], keyValue[1]); + } + } + return action.apply(name, providerConfig); + } + + public JdkProvider byName(String name, JdkDiscovery.Config config) { + for (JdkDiscovery discovery : discoveries()) { + if (discovery.name().equals(name)) { + JdkProvider provider = discovery.create(config); + if (provider != null) { + return provider; + } + } + } + return null; + } + + private synchronized List discoveries() { + if (discoveries == null) { + ServiceLoader loader = ServiceLoader.load(JdkDiscovery.class); + discoveries = new ArrayList<>(); + for (JdkDiscovery discovery : loader) { + discoveries.add(discovery); + } + discoveries.sort(Comparator.comparing(JdkDiscovery::name)); + } + return discoveries; + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkinstallers/FoojayJdkInstaller.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkinstallers/FoojayJdkInstaller.java new file mode 100644 index 000000000..7ae0ccbde --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkinstallers/FoojayJdkInstaller.java @@ -0,0 +1,270 @@ +package dev.jbang.jvm.jdkinstallers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +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 org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkInstaller; +import dev.jbang.jvm.util.*; + +/** + * JVM's main JDK installer that can download and install the JDKs provided by + * the Foojay Disco API. + */ +public class FoojayJdkInstaller implements JdkInstaller { + protected final JdkFactory jdkFactory; + protected RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); + + protected static final String FOOJAY_JDK_DOWNLOAD_URL = "https://api.foojay.io/disco/v3.0/directuris?"; + protected static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; + + private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); + + private static class JdkResult { + String java_version; + int major_version; + String release_status; + } + + private static class VersionsResponse { + List result; + } + + public FoojayJdkInstaller(JdkFactory jdkFactory) { + this.jdkFactory = jdkFactory; + } + + public FoojayJdkInstaller remoteAccessProvider(RemoteAccessProvider remoteAccessProvider) { + this.remoteAccessProvider = remoteAccessProvider; + return this; + } + + @NonNull + @Override + public List listAvailable() { + try { + Set result = new LinkedHashSet<>(); + Consumer addJdk = version -> { + result.add(jdkFactory.createJdk(jdkFactory.jdkId(version), null, version)); + }; + String distro = getDistro(); + if (distro == null) { + VersionsResponse res = readJsonFromUrl( + getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), "temurin,aoj")); + filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); + } else { + VersionsResponse res = readJsonFromUrl( + getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), distro)); + filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); + } + // result.sort(Jdk::compareTo); + return Collections.unmodifiableList(new ArrayList<>(result)); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + } + return Collections.emptyList(); + } + + private VersionsResponse readJsonFromUrl(String url) throws IOException { + return remoteAccessProvider.resultFromUrl(url, is -> { + try (InputStream ignored = is) { + Gson parser = new GsonBuilder().create(); + return parser.fromJson(new InputStreamReader(is), VersionsResponse.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + // 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()); + } + + @NonNull + @Override + public Jdk install(@NonNull Jdk jdk, Path jdkDir) { + int version = jdkVersion(jdk.id()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = getDownloadUrl(version, OsUtils.getOS(), OsUtils.getArch(), getDistro()); + LOGGER.log(Level.FINE, "Downloading {0}", url); + 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 = remoteAccessProvider.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 jdkFactory.createJdk(jdk.id(), 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) { + if (jdk.isInstalled()) { + FileUtils.deletePath(jdk.home()); + } + } + + 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); + } + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + // TODO refactor + private static String getDistro() { + return null; + } + + public interface JdkFactory { + String jdkId(String name); + + Jdk createJdk(@NonNull String id, @Nullable Path home, @NonNull String version); + } +} 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..6a2d704d6 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseFoldersJdkProvider.java @@ -0,0 +1,142 @@ +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 org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; + +public abstract class BaseFoldersJdkProvider extends BaseJdkProvider { + protected final Path jdksRoot; + + private static final Logger LOGGER = Logger.getLogger(BaseFoldersJdkProvider.class.getName()); + + protected BaseFoldersJdkProvider(Path jdksRoot) { + this.jdksRoot = jdksRoot; + } + + @Override + @NonNull + public String name() { + String nm = getClass().getSimpleName(); + if (nm.endsWith("JdkProvider")) { + return nm.substring(0, nm.length() - 11).toLowerCase(); + } else { + return nm.toLowerCase(); + } + } + + @Override + public boolean canUse() { + return Files.isDirectory(jdksRoot) || canUpdate(); + } + + @Override + public @Nullable Jdk getAvailableByIdOrToken(String idOrToken) { + if (isValidId(idOrToken) && super.canUpdate()) { + return 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) + .collect(Collectors.toList()); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); + } + } + return Collections.emptyList(); + } + + @Nullable + @Override + public Jdk getInstalledById(@NonNull String id) { + return getInstalledByPath(getJdkPath(id)); + } + + @Nullable + @Override + public Jdk getInstalledByPath(@NonNull Path jdkPath) { + if (jdkPath.startsWith(jdksRoot) && Files.isDirectory(jdkPath) && acceptFolder(jdkPath)) { + return createJdk(jdkPath); + } + 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 String jdkId(String name) { + return name + "-" + name(); + } + + protected boolean acceptFolder(Path jdkFolder) { + return OsUtils.searchPath("javac", jdkFolder.resolve("bin").toString()) != null; + } + + @Override + public boolean isValidId(@NonNull String id) { + return id.endsWith("-" + name()); + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseJdkProvider.java new file mode 100644 index 000000000..dfe2292d6 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseJdkProvider.java @@ -0,0 +1,19 @@ +package dev.jbang.jvm.jdkproviders; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.JdkManager; +import dev.jbang.jvm.JdkProvider; + +public abstract class BaseJdkProvider implements JdkProvider { + protected JdkManager manager; + + @Override + public @NonNull JdkManager manager() { + return manager; + } + + public void manager(@NonNull JdkManager manager) { + this.manager = manager; + } +} 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..766e61cd1 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/CurrentJdkProvider.java @@ -0,0 +1,59 @@ +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 org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; + +/** + * This JDK provider returns the "current" JDK, which is the JDK that is being + * used to run the current application. + */ +public class CurrentJdkProvider extends BaseJdkProvider { + + @Override + public @NonNull String name() { + return Discovery.PROVIDER_ID; + } + + @Override + public @NonNull String description() { + return "The JDK that is being used to run the current application."; + } + + @Override + public @NonNull 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(Discovery.PROVIDER_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "current"; + + @Override + public @NonNull String name() { + return PROVIDER_ID; + } + + @Override + public @NonNull JdkProvider create(Config config) { + return new CurrentJdkProvider(); + } + } +} 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..e120111fe --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/DefaultJdkProvider.java @@ -0,0 +1,86 @@ +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 org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; + +/** + * This JDK provider returns the "default" JDK if it was set. This is not a JDK + * in itself but a link to a JDK that was set as the default. The path that is + * configured for this default JDK should be stable and unchanging so it can be + * added to the user's PATH. + */ +public class DefaultJdkProvider extends BaseJdkProvider { + @NonNull + protected final Path defaultJdkLink; + + public DefaultJdkProvider(@NonNull Path defaultJdkLink) { + this.defaultJdkLink = defaultJdkLink; + } + + @Override + @NonNull + public String name() { + return Discovery.PROVIDER_ID; + } + + @Override + public @NonNull String description() { + return "The JDK that is set as the default JDK."; + } + + @NonNull + @Override + public List listInstalled() { + if (Files.isDirectory(defaultJdkLink)) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(defaultJdkLink); + if (version.isPresent()) { + return Collections.singletonList(createJdk(Discovery.PROVIDER_ID, defaultJdkLink, version.get())); + } + } + return Collections.emptyList(); + } + + @Override + public @NonNull Jdk install(@NonNull Jdk jdk) { + Jdk defJdk = getInstalledById(Discovery.PROVIDER_ID); + if (defJdk != null && defJdk.isInstalled() && !jdk.equals(defJdk)) { + uninstall(defJdk); + } + // Remove anything that might be in the way + FileUtils.deletePath(defaultJdkLink); + // Now create the new link + FileUtils.createLink(defaultJdkLink, jdk.home()); + return defJdk; + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + FileUtils.deletePath(defaultJdkLink); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "default"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new DefaultJdkProvider(config.installPath.resolve(PROVIDER_ID)); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JBangJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JBangJdkProvider.java new file mode 100644 index 000000000..b23cd9491 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JBangJdkProvider.java @@ -0,0 +1,170 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.*; +import dev.jbang.jvm.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.jvm.util.*; + +/** + * JBang's main JDK provider that (by default) can download and install the JDKs + * provided by the Foojay Disco API. They get installed in the user's JBang + * folder. + */ +public class JBangJdkProvider extends BaseFoldersJdkProvider implements FoojayJdkInstaller.JdkFactory { + protected JdkInstaller jdkInstaller; + + public JBangJdkProvider() { + this(getJBangJdkDir()); + } + + public JBangJdkProvider(Path jdksRoot) { + super(jdksRoot); + jdkInstaller = new FoojayJdkInstaller(this); + } + + @Override + public @NonNull String description() { + return "The JDKs managed by JBang."; + } + + public JBangJdkProvider installer(JdkInstaller jdkInstaller) { + this.jdkInstaller = jdkInstaller; + return this; + } + + @NonNull + @Override + public List listAvailable() { + return jdkInstaller.listAvailable(); + } + + @Override + public @Nullable Jdk getAvailableByIdOrToken(String idOrToken) { + return jdkInstaller.getAvailableByIdOrToken(idOrToken); + } + + @NonNull + @Override + public Jdk install(@NonNull Jdk jdk) { + return jdkInstaller.install(jdk, getJdkPath(jdk.id())); + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + super.uninstall(jdk); + jdkInstaller.uninstall(jdk); + } + + @Override + public Jdk createJdk(@NonNull String id, @Nullable Path home, @NonNull String version) { + return super.createJdk(id, home, version); + } + + @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, openVersion); + } + return null; + } + + @Override + public boolean canUpdate() { + return true; + } + + @NonNull + public Path getJdksPath() { + return jdksRoot; + } + + @NonNull + @Override + public String jdkId(String name) { + int majorVersion = JavaUtils.parseJavaVersion(name); + return super.jdkId(Integer.toString(majorVersion)); + } + + @Override + public boolean isValidId(@NonNull String id) { + return JavaUtils.parseToInt(id, 0) > 0; + } + + @NonNull + @Override + protected Path getJdkPath(@NonNull String jdk) { + return getJdksPath().resolve(Integer.toString(jdkVersion(jdk))); + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + @Override + protected boolean acceptFolder(Path jdkFolder) { + return isValidId(jdkFolder.getFileName().toString()) + && super.acceptFolder(jdkFolder) + && !FileUtils.isLink(jdkFolder); + } + + public static Path getJBangJdkDir() { + Path dir; + String v = System.getenv("JBANG_CACHE_DIR_JDKS"); + if (v != null) { + dir = Paths.get(v); + } else { + dir = getJBangCacheDir().resolve("jdks"); + } + return dir; + } + + private static Path getJBangCacheDir() { + Path dir; + String v = System.getenv("JBANG_CACHE_DIR"); + if (v != null) { + dir = Paths.get(v); + } else { + dir = getJBangConfigDir().resolve("cache"); + } + return dir; + } + + private static Path getJBangConfigDir() { + Path dir; + String jd = System.getenv("JBANG_DIR"); + if (jd != null) { + dir = Paths.get(jd); + } else { + dir = Paths.get(System.getProperty("user.home")).resolve(".jbang"); + } + return dir; + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "jbang"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new JBangJdkProvider(config.installPath); + // TODO make RAP configurable + // .remoteAccessProvider(RemoteAccessProvider.createDefaultRemoteAccessProvider(config.cachePath)); + } + } +} 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..4e8d21212 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JavaHomeJdkProvider.java @@ -0,0 +1,60 @@ +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 org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; + +/** + * This JDK provider detects if a JDK is already available on the system by + * looking at JAVA_HOME environment variable. + */ +public class JavaHomeJdkProvider extends BaseJdkProvider { + + @Override + @NonNull + public String name() { + return Discovery.PROVIDER_ID; + } + + @Override + public @NonNull String description() { + return "The JDK pointed to by the JAVA_HOME environment variable."; + } + + @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(Discovery.PROVIDER_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "javahome"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new JavaHomeJdkProvider(); + } + } +} 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..b936e72da --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinkedJdkProvider.java @@ -0,0 +1,155 @@ +package dev.jbang.jvm.jdkproviders; + +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; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.FileUtils; +import dev.jbang.jvm.util.JavaUtils; + +/** + * This JDK provider returns JDKs that are not managed or found by any of the + * providers but that are available on the user's system and that the user has + * explicitly told we can use. Each of those JDKs is represented by a symbolic + * link to the actual JDK folder. + */ +public class LinkedJdkProvider extends BaseFoldersJdkProvider { + private static final Logger LOGGER = Logger.getLogger(LinkedJdkProvider.class.getName()); + + public LinkedJdkProvider(Path jdksRoot) { + super(jdksRoot); + } + + @Override + public @NonNull String description() { + return "Any unmanaged JDKs that the user has linked to."; + } + + @Override + public boolean canUse() { + return true; + } + + @Override + public @Nullable Jdk getAvailableByIdOrToken(String idOrToken) { + String[] parts = idOrToken.split("@", 2); + if (parts.length == 2 && isValidId(parts[0]) && 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 isValidId(jdkFolder.getFileName().toString()) + && 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.id().split("@", 2); + if (parts.length != 2 || !isValidPath(parts[1])) { + throw new IllegalStateException("Invalid linked Jdk id: " + jdk.id()); + } + 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.home()); + LOGGER.log( + Level.INFO, + "JDK {0} has been uninstalled", + new Object[] { jdk.id() }); + } + } + + // TODO remove these 3 methods when switching to the new folder structure + @NonNull + @Override + public String jdkId(String name) { + int majorVersion = JavaUtils.parseJavaVersion(name); + return Integer.toString(majorVersion); + } + + @Override + public boolean isValidId(@NonNull String id) { + return JavaUtils.parseToInt(id, 0) > 0; + } + + @NonNull + @Override + protected Path getJdkPath(@NonNull String jdk) { + return jdksRoot.resolve(Integer.toString(JavaUtils.parseToInt(jdk, 0))); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "linked"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new LinkedJdkProvider(config.installPath); + } + } +} diff --git a/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java new file mode 100644 index 000000000..3a425a28c --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/LinuxJdkProvider.java @@ -0,0 +1,70 @@ +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 org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.FileUtils; + +/** + * This JDK provider is intended to detects JDKs that have been installed in + * standard location of the users linux distro. + *

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

+ * If need different behavior per linux distro its intended this provider will + * adjust based on identified distro. + * + */ +public class LinuxJdkProvider extends BaseFoldersJdkProvider { + protected static final Path JDKS_ROOT = Paths.get("/usr/lib/jvm"); + + public LinuxJdkProvider() { + super(JDKS_ROOT); + } + + @Override + public @NonNull String description() { + return "The JDKs installed in the standard location of a Linux distro."; + } + + @Override + protected boolean acceptFolder(Path jdkFolder) { + return super.acceptFolder(jdkFolder) && !isSameFolderLink(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(); + try { + if (FileUtils.isLink(absFolder)) { + Path realPath = absFolder.toRealPath(); + return Files.isSameFile(absFolder.getParent(), realPath.getParent()); + } + } catch (IOException e) { + /* ignore */ + } + return false; + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "linux"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new LinuxJdkProvider(); + } + } +} 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..a37137499 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/PathJdkProvider.java @@ -0,0 +1,65 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.JavaUtils; +import dev.jbang.jvm.util.OsUtils; + +/** + * This JDK provider detects if a JDK is already available on the system by + * first looking at the user's PATH. + */ +public class PathJdkProvider extends BaseJdkProvider { + + @Override + @NonNull + public String name() { + return Discovery.PROVIDER_ID; + } + + @Override + public @NonNull String description() { + return "The JDK pointed to by the PATH environment variable."; + } + + @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(Discovery.PROVIDER_ID, jdkHome, version.get())); + } + } + return Collections.emptyList(); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "path"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new PathJdkProvider(); + } + } +} 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..86a0814b8 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/ScoopJdkProvider.java @@ -0,0 +1,74 @@ +package dev.jbang.jvm.jdkproviders; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.jvm.Jdk; +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; +import dev.jbang.jvm.util.OsUtils; + +/** + * 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); + } + + @Override + public @NonNull String description() { + return "The JDKs installed using the Scoop package manager."; + } + + @NonNull + @Override + protected Stream listJdkPaths() throws IOException { + return super.listJdkPaths().map(p -> p.resolve("current")); + } + + @Override + protected boolean acceptFolder(Path jdkFolder) { + return jdkFolder.getFileName().startsWith("openjdk") && super.acceptFolder(jdkFolder); + } + + @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(); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "scoop"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new ScoopJdkProvider(); + } + } +} 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..917a8803b --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/SdkmanJdkProvider.java @@ -0,0 +1,41 @@ +package dev.jbang.jvm.jdkproviders; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.jvm.JdkDiscovery; +import dev.jbang.jvm.JdkProvider; + +/** + * 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); + } + + @Override + public @NonNull String description() { + return "The JDKs installed using the SDKMAN package manager."; + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "sdkman"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(Config config) { + return new SdkmanJdkProvider(); + } + } +} 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..dd8928be8 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileHttpCacheStorage.java @@ -0,0 +1,74 @@ +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..066e603df --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/FileUtils.java @@ -0,0 +1,133 @@ +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); + } else { + LOGGER.log(Level.FINE, "Link already exists {0}, aborting", new Object[] { link }); + } + } + + private static boolean createSymbolicLink(Path link, Path target) { + try { + if (!Files.exists(link) && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) { + // We automatically remove broken links + deletePath(link); + } + 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) : ""; + } + + public static Path deleteOnExit(Path path) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> deletePath(path))); + return path; + } +} 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..9c94525df --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/JavaUtils.java @@ -0,0 +1,137 @@ +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) { + String[] nums = version.split("[-.+]"); + String num = nums.length > 1 && nums[0].equals("1") ? nums[1] : nums[0]; + return parseToInt(num, 0); + } + return 0; + } + + public static int parseToInt(String number, int defaultValue) { + if (number != null) { + try { + return Integer.parseInt(number); + } catch (NumberFormatException ex) { + // Ignore + } + } + return defaultValue; + } + + 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..708fa2dbe --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/NetUtils.java @@ -0,0 +1,119 @@ +package dev.jbang.jvm.util; + +import java.io.IOException; +import java.io.InputStream; +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 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 T resultFromUrl(String url, Function streamToObject) throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return resultFromUrl(builder, url, streamToObject); + } + + public static T resultFromUrl(HttpClientBuilder builder, String url, Function streamToObject) + throws IOException { + return requestUrl(builder, url, + mimetypeChecker("application/json") + .andThen(NetUtils::responseStreamer) + .andThen(streamToObject)); + } + + 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 Function mimetypeChecker(String expectedMimeType) { + return response -> { + String mimeType = ContentType.getOrDefault(response.getEntity()).getMimeType(); + if (expectedMimeType != null && !mimeType.equals(expectedMimeType)) { + throw new RuntimeException("Unexpected MIME type: " + mimeType); + } + return response; + }; + } + + private static InputStream responseStreamer(HttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + return entity.getContent(); + } 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..b53201cce --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/OsUtils.java @@ -0,0 +1,179 @@ +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/RemoteAccessProvider.java b/jdkmanager/src/main/java/dev/jbang/jvm/util/RemoteAccessProvider.java new file mode 100644 index 000000000..041024643 --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/RemoteAccessProvider.java @@ -0,0 +1,35 @@ +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.util.function.Function; + +public interface RemoteAccessProvider { + + Path downloadFromUrl(String url) throws IOException; + + default T resultFromUrl(String url, Function streamToObject) throws IOException { + Path file = downloadFromUrl(url); + try (InputStream is = Files.newInputStream(file)) { + return streamToObject.apply(is); + } + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider() { + return new DefaultRemoteAccessProvider(); + } + + class DefaultRemoteAccessProvider implements RemoteAccessProvider { + @Override + public Path downloadFromUrl(String url) throws IOException { + return NetUtils.downloadFromUrl(url); + } + + @Override + public T resultFromUrl(String url, Function streamToObject) throws IOException { + return NetUtils.resultFromUrl(url, streamToObject); + } + } +} 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..fc0c8b3de --- /dev/null +++ b/jdkmanager/src/main/java/dev/jbang/jvm/util/UnpackUtils.java @@ -0,0 +1,218 @@ +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/jdkmanager/src/main/resources/META-INF/services/dev.jbang.jvm.JdkDiscovery b/jdkmanager/src/main/resources/META-INF/services/dev.jbang.jvm.JdkDiscovery new file mode 100644 index 000000000..5b7082502 --- /dev/null +++ b/jdkmanager/src/main/resources/META-INF/services/dev.jbang.jvm.JdkDiscovery @@ -0,0 +1,9 @@ +dev.jbang.jvm.jdkproviders.CurrentJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.DefaultJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.JavaHomeJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.PathJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.LinkedJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.JBangJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.LinuxJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.ScoopJdkProvider$Discovery +dev.jbang.jvm.jdkproviders.SdkmanJdkProvider$Discovery diff --git a/jdkmanager/src/test/java/dev/jbang/jvm/BaseTest.java b/jdkmanager/src/test/java/dev/jbang/jvm/BaseTest.java new file mode 100644 index 000000000..19956e9a5 --- /dev/null +++ b/jdkmanager/src/test/java/dev/jbang/jvm/BaseTest.java @@ -0,0 +1,216 @@ +package dev.jbang.jvm; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.logging.*; +import java.util.stream.Collectors; + +import org.jspecify.annotations.NonNull; +import org.junit.Rule; +import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.jvm.jdkproviders.BaseFoldersJdkProvider; +import dev.jbang.jvm.util.FileUtils; + +public class BaseTest { + protected JdkDiscovery.Config config; + + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @BeforeAll + protected static void initAll() { + // Force the logging level to FINE + Logger root = LogManager.getLogManager().getLogger(""); + root.setLevel(Level.FINE); + Handler consoleHandler = null; + for (Handler handler : root.getHandlers()) { + if (handler instanceof ConsoleHandler) { + consoleHandler = handler; + break; + } + } + if (consoleHandler == null) { + consoleHandler = new ConsoleHandler(); + root.addHandler(new ConsoleHandler()); + } + consoleHandler.setLevel(java.util.logging.Level.FINE); + } + + @BeforeEach + protected void initEnv(@TempDir Path tempPath) throws IOException { + config = new JdkDiscovery.Config(tempPath.resolve("jdks"), null, null); + } + + protected JdkManager jdkManager() { + return jdkManager("default", "linked", "jbang"); + } + + protected JdkManager jdkManager(String... providerNames) { + return JdkManager .builder() + .providers(JdkProviders.instance().parseNames(config, providerNames)) + .build(); + } + + protected JdkManager dummyJdkManager(int... providerNames) { + return JdkManager .builder() + .providers(new DummyJdkProvider(providerNames)) + .build(); + } + + protected Path createMockJdk(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdkRuntime(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDirRuntime); + } + + protected Path createMockJdk(int jdkVersion, BiConsumer init) { + Path jdkPath = config.installPath.resolve(String.valueOf(jdkVersion)); + init.accept(jdkPath, jdkVersion + ".0.7"); + Path link = config.installPath.resolve("default"); + if (!Files.exists(link)) { + FileUtils.createLink(link, jdkPath); + } + return jdkPath; + } + + protected void initMockJdkDirRuntime(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_RUNTIME_VERSION"); + } + + protected void initMockJdkDir(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_VERSION"); + } + + protected void initMockJdkDir(Path jdkPath, String version, String key) { + try { + Files.createDirectories(jdkPath); + Path jdkBinPath = jdkPath.resolve("bin"); + Files.createDirectories(jdkBinPath); + String rawJavaVersion = key + "=\"" + version + "\""; + Path release = jdkPath.resolve("release"); + Path javacPath = jdkBinPath.resolve("javac"); + writeString(javacPath, "dummy"); + javacPath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("javac.exe"), "dummy"); + writeString(release, rawJavaVersion); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void writeString(Path toPath, String scriptText) throws IOException { + Files.write(toPath, scriptText.getBytes()); + } + + static class JdkProviderWrapper implements JdkProvider { + protected final JdkProvider provider; + + JdkProviderWrapper(JdkProvider provider) { + this.provider = provider; + } + + @Override + public @NonNull JdkManager manager() { + return provider.manager(); + } + + @Override + public void manager(@NonNull JdkManager manager) { + provider.manager(manager); + } + + @Override + public @NonNull String name() { + return provider.name(); + } + + @Override + public @NonNull String description() { + return provider.description(); + } + + @Override + public @NonNull List listInstalled() { + return provider.listInstalled(); + } + + @Override + public Jdk getInstalledById(@NonNull String id) { + return provider.getInstalledById(id); + } + + @Override + public Jdk getInstalledByPath(@NonNull Path jdkPath) { + return provider.getInstalledByPath(jdkPath); + } + + @Override + public @NonNull List listAvailable() { + return provider.listAvailable(); + } + + @Override + public Jdk getAvailableByIdOrToken(String idOrToken) { + return provider.getAvailableByIdOrToken(idOrToken); + } + + @Override + public @NonNull Jdk install(@NonNull Jdk jdk) { + return provider.install(jdk); + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + provider.uninstall(jdk); + } + } + + class DummyJdkProvider extends BaseFoldersJdkProvider { + protected final int[] versions; + + @Override + public @NonNull String description() { + return "Dummy JDK provider"; + } + + protected DummyJdkProvider(int... versions) { + super(config.installPath); + this.versions = versions; + } + + @Override + public @NonNull List listAvailable() { + return Arrays .stream(versions) + .mapToObj(v -> createJdk(v + "-dummy", null, v + ".0.7")) + .collect(Collectors.toList()); + } + + @Override + public @NonNull Jdk install(@NonNull Jdk jdk) { + Path jdkPath = createMockJdk(jdk.majorVersion()); + return createJdk(jdk.id(), jdkPath, jdk.version()); + } + + @Override + public void uninstall(@NonNull Jdk jdk) { + if (jdk.isInstalled()) { + FileUtils.deletePath(jdk.home()); + } + } + + @Override + public boolean canUpdate() { + return true; + } + } +} diff --git a/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkManager.java b/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkManager.java new file mode 100644 index 000000000..9f2d16981 --- /dev/null +++ b/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkManager.java @@ -0,0 +1,248 @@ +package dev.jbang.jvm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import dev.jbang.jvm.jdkproviders.DefaultJdkProvider; +import dev.jbang.jvm.jdkproviders.JavaHomeJdkProvider; +import dev.jbang.jvm.jdkproviders.LinkedJdkProvider; +import dev.jbang.jvm.jdkproviders.PathJdkProvider; +import dev.jbang.jvm.util.FileUtils; + +public class TestJdkManager extends BaseTest { + @Test + void testNoJdksInstalled() { + assertThat(jdkManager().listInstalledJdks(), is(empty())); + } + + @Test + void testHasJdksInstalled() { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(4)); + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), + containsInAnyOrder(11, 11, 12, 13)); + assertThat(jdks.stream().map(Jdk::version).collect(Collectors.toList()), + containsInAnyOrder("11.0.7", "11.0.7", "12.0.7", "13.0.7")); + } + + @Test + void testHasJdksInstalledWithJavaHome() { + Arrays.asList(11, 12).forEach(this::createMockJdk); + + Path jdkPath = config.cachePath.resolve("jdk13"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "13.0.7"); + environmentVariables.set("JAVA_HOME", jdkPath.toString()); + + List jdks = jdkManager("default", "javahome", "jbang").listInstalledJdks(); + assertThat(jdks, hasSize(4)); + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), + containsInAnyOrder(11, 11, 12, 13)); + assertThat(jdks.stream().map(Jdk::version).collect(Collectors.toList()), + containsInAnyOrder("11.0.7", "11.0.7", "12.0.7", "13.0.7")); + } + + @Test + void testHasJdksInstalledAllProvider() { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + List jdks = jdkManager(JdkProviders.instance().allNames().toArray(new String[] {})).listInstalledJdks(); + assertThat(jdks, hasSize(greaterThanOrEqualTo(5))); + } + + @Test + void testDefault() { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + JdkManager jm = jdkManager(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(11)); + jm.setDefaultJdk(jm.getJdk("12")); + assertThat(jm.getDefaultJdk().majorVersion(), is(12)); + } + + @Test + void testDefaultPlus() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + JdkManager jm = jdkManager(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(11)); + jm.setDefaultJdk(jm.getJdk("16+")); + assertThat(jm.getDefaultJdk().majorVersion(), is(17)); + } + + @Test + void testHomeDir() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + Path home = jdkManager().getOrInstallJdk(null).home(); + assertThat(home.toString(), endsWith(File.separator + "default")); + } + + @Test + void testDefaultHomeDir() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + Path home = jdkManager().getOrInstallJdk("default").home(); + assertThat(home.toString(), endsWith(File.separator + "default")); + } + + @Test + void testDefaultUninstallNext() { + Arrays.asList(14, 11, 17).forEach(this::createMockJdk); + JdkManager jm = jdkManager(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(14)); + jm.getJdk("14", JdkProvider.Predicates.canUpdate).uninstall(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(17)); + } + + @Test + void testDefaultUninstallPrev() { + Arrays.asList(17, 11, 14).forEach(this::createMockJdk); + JdkManager jm = jdkManager(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(17)); + jm.getJdk("17", JdkProvider.Predicates.canUpdate).uninstall(); + assertThat(jm.getDefaultJdk(), not(nullValue())); + assertThat(jm.getDefaultJdk().majorVersion(), is(14)); + } + + @Test + void testVersionHomeDir() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + Path home = jdkManager().getOrInstallJdk("17").home(); + assertThat(home.toString(), endsWith(File.separator + "17")); + } + + @Test + void testVersionPlusHomeDir() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + Path home = jdkManager().getOrInstallJdk("16+").home(); + assertThat(home.toString(), endsWith(File.separator + "17")); + } + + @Test + void testJavaHome() throws IOException { + Arrays.asList(11, 13).forEach(this::createMockJdk); + + Path jdkPath = config.cachePath.resolve("jdk12"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "12.0.7"); + environmentVariables.set("JAVA_HOME", jdkPath.toString()); + + JdkManager jm = jdkManager("javahome", "jbang"); + Jdk jdk = jm.getOrInstallJdk("12"); + assertThat(jdk.provider(), instanceOf(JavaHomeJdkProvider.class)); + assertThat(jdk.home().toString(), endsWith(File.separator + "jdk12")); + } + + @Test + void testDefaultWithJavaHome() throws IOException { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + + Path jdkPath = config.cachePath.resolve("jdk12"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "12.0.7"); + environmentVariables.set("JAVA_HOME", jdkPath.toString()); + + JdkManager jm = jdkManager("default", "javahome", "jbang"); + jm.setDefaultJdk(jm.getJdk("12")); + Jdk jdk = jm.getOrInstallJdk("12"); + assertThat(jdk.provider(), instanceOf(DefaultJdkProvider.class)); + assertThat(jdk.home().toString(), endsWith(File.separator + "default")); + assertThat(jdk.home().toRealPath().toString(), endsWith(File.separator + "jdk12")); + } + + @Test + void testPath() throws IOException { + Arrays.asList(11, 13).forEach(this::createMockJdk); + + Path jdkPath = config.cachePath.resolve("jdk12"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "12.0.7"); + environmentVariables.set("PATH", jdkPath.resolve("bin") + File.pathSeparator + System.getenv("PATH")); + + JdkManager jm = jdkManager("path", "jbang"); + Jdk jdk = jm.getOrInstallJdk("12"); + assertThat(jdk.provider(), instanceOf(PathJdkProvider.class)); + assertThat(jdk.home().toString(), endsWith(File.separator + "jdk12")); + } + + @Test + void testLinkToExistingJdkPath() { + Path jdkPath = config.cachePath.resolve("jdk12"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "12.0.7"); + + jdkManager().linkToExistingJdk(jdkPath, "12"); + + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(1)); + assertThat(jdks.get(0).provider(), instanceOf(LinkedJdkProvider.class)); + } + + @Test + void testLinkToInvalidJdkPath() { + try { + jdkManager().linkToExistingJdk(Paths.get("/invalid-path"), "11"); + assertThat("Should have thrown an exception", false); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), + startsWith("Unable to resolve path as directory: " + File.separator + "invalid-path")); + } + } + + @Test + void testProviderOrder() { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + + Path jdkPath = config.cachePath.resolve("jdk12"); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, "12.0.7"); + environmentVariables.set("JAVA_HOME", jdkPath.toString()); + + JdkManager jm = jdkManager("javahome", "jbang"); + Jdk jdk = jm.getOrInstallJdk(null); + assertThat(jdk.provider(), instanceOf(JavaHomeJdkProvider.class)); + } + + @Test + void testInstallVersion() { + Path home = dummyJdkManager(11, 14, 17).getOrInstallJdk("17").home(); + assertThat(home.toString(), endsWith(File.separator + "17")); + assertThat(home.resolve("release").toFile().exists(), is(true)); + } + + @Test + void testInstallVersionFail() { + try { + Path home = dummyJdkManager(11, 14, 17).getOrInstallJdk("15").home(); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("No suitable JDK was found for requested version: 15")); + } + } + + @Test + void testInstallVersionPlus() { + Path home = dummyJdkManager(11, 14, 17).getOrInstallJdk("15+").home(); + assertThat(home.toString(), endsWith(File.separator + "17")); + assertThat(home.resolve("release").toFile().exists(), is(true)); + } + + @Test + void testInstallDefaultVersion() { + Jdk jdk = dummyJdkManager(8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24) + .getOrInstallJdk( + null); + assertThat(jdk.majorVersion(), is(JdkManager.DEFAULT_JAVA_VERSION)); + } +} diff --git a/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkProviders.java b/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkProviders.java new file mode 100644 index 000000000..c429dc56b --- /dev/null +++ b/jdkmanager/src/test/java/dev/jbang/jvm/TestJdkProviders.java @@ -0,0 +1,78 @@ +package dev.jbang.jvm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import org.junit.jupiter.api.Test; + +import dev.jbang.jvm.jdkproviders.*; + +public class TestJdkProviders extends BaseTest { + @Test + void testMinimalNames() { + assertThat(JdkProviders.instance().minimalNames(), + contains("current", "javahome", "path")); + } + + @Test + void testBasicNames() { + assertThat(JdkProviders.instance().basicNames(), + contains("current", "default", "javahome", "path", "linked", "jbang")); + } + + @Test + void testAllNames() { + assertThat(JdkProviders.instance().allNames(), + contains("current", "default", "javahome", "path", "linked", "jbang", "linux", "scoop", "sdkman")); + } + + @Test + void testMinimal() { + assertThat(JdkProviders.instance().minimal(), contains(instanceOf(CurrentJdkProvider.class), + instanceOf(JavaHomeJdkProvider.class), instanceOf(PathJdkProvider.class))); + } + + @Test + void testBasic() { + assertThat(JdkProviders.instance().basic(config), contains(instanceOf(CurrentJdkProvider.class), + instanceOf(DefaultJdkProvider.class), instanceOf(JavaHomeJdkProvider.class), + instanceOf(PathJdkProvider.class), instanceOf(LinkedJdkProvider.class), + instanceOf(JBangJdkProvider.class))); + } + + @Test + void testAll() { + assertThat(JdkProviders.instance().all(config), contains(instanceOf(CurrentJdkProvider.class), + instanceOf(DefaultJdkProvider.class), instanceOf(JavaHomeJdkProvider.class), + instanceOf(PathJdkProvider.class), instanceOf(LinkedJdkProvider.class), + instanceOf(JBangJdkProvider.class), instanceOf(LinuxJdkProvider.class), + instanceOf(ScoopJdkProvider.class), instanceOf(SdkmanJdkProvider.class))); + } + + @Test + void testParseNames() { + String names = "current,default,javahome,path,linked,jbang,linux,scoop,sdkman"; + assertThat(JdkProviders.instance().parseNames(config, names), contains(instanceOf(CurrentJdkProvider.class), + instanceOf(DefaultJdkProvider.class), instanceOf(JavaHomeJdkProvider.class), + instanceOf(PathJdkProvider.class), instanceOf(LinkedJdkProvider.class), + instanceOf(JBangJdkProvider.class), instanceOf(LinuxJdkProvider.class), + instanceOf(ScoopJdkProvider.class), instanceOf(SdkmanJdkProvider.class))); + } + + @Test + void testParseNameWithConfig() { + String name = "jbang;aap=noot;mies=wim"; + assertThat(JdkProviders.instance().parseName(config, name, (prov, config) -> { + assertThat(prov, equalTo("jbang")); + assertThat(config.properties, hasEntry("aap", "noot")); + assertThat(config.properties, hasEntry("mies", "wim")); + return new JBangJdkProvider(config.installPath); + }), + is(instanceOf(JBangJdkProvider.class))); + } + + @Test + void testByName() { + assertThat(JdkProviders.instance().byName("jbang", config), is(instanceOf(JBangJdkProvider.class))); + } +} 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 13092dc59..5a16c4c37 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() { .nativeImage(nativeMixin.nativeImage) .nativeOptions(nativeMixin.nativeOptions) .integrations(buildMixin.integrations) - .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..74f0b0653 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); 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 6bdc1f321..709b8029f 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; @@ -53,7 +50,8 @@ protected ProjectBuilder createBaseProjectBuilder() { .nativeImage(nativeMixin.nativeImage) .nativeOptions(nativeMixin.nativeOptions) .integrations(buildMixin.integrations) - .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 d279e3931..af5814211 100644 --- a/src/main/java/dev/jbang/cli/BuildMixin.java +++ b/src/main/java/dev/jbang/cli/BuildMixin.java @@ -4,12 +4,18 @@ import java.util.List; import java.util.Map; +import dev.jbang.jvm.Jdk; +import dev.jbang.source.Project; + import picocli.CommandLine; import picocli.CommandLine.Option; 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) { @@ -38,6 +44,14 @@ void setJavaVersion(String javaVersion) { "--integrations" }, description = "Enable integration execution (default: true)", negatable = true) public Boolean integrations; + 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) { @@ -69,6 +83,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..ff8ce2032 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,10 +117,11 @@ 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); if (jdk != null && jdk.isInstalled()) { - availableJdkPath = jdk.getHome().toString(); + availableJdkPath = jdk.home().toString(); } } catch (ExitException e) { // Ignore diff --git a/src/main/java/dev/jbang/cli/Jdk.java b/src/main/java/dev/jbang/cli/Jdk.java index 7c26c63ef..01023bb12 100644 --- a/src/main/java/dev/jbang/cli/Jdk.java +++ b/src/main/java/dev/jbang/cli/Jdk.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.PrintStream; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; @@ -12,8 +13,8 @@ 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.jvm.JdkProvider; import dev.jbang.util.JavaUtil; import dev.jbang.util.Util; @@ -35,14 +36,17 @@ 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, JdkProvider.Predicates.canUpdate); if (force || jdk == null) { if (!Util.isNullOrBlankString(path)) { - JdkManager.linkToExistingJdk(path, Integer.parseInt(versionOrId)); + jdkMan.linkToExistingJdk(Paths.get(path), versionOrId); } else { if (jdk == null) { - jdk = JdkManager.getJdk(versionOrId, true); + jdk = jdkMan.getJdk(versionOrId, JdkProvider.Predicates.canUpdate); + if (jdk == null) { + throw new IllegalArgumentException("JDK is not available for installation: " + versionOrId); + } } jdk.install(); } @@ -61,21 +65,21 @@ 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(); - int defMajorVersion = defaultJdk != null ? defaultJdk.getMajorVersion() : 0; + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk defaultJdk = jdkMan.getDefaultJdk(); + int defMajorVersion = defaultJdk != null ? defaultJdk.majorVersion() : 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(), - jdk.getHome(), + .map(jdk -> new JdkOut(jdk.id(), jdk.version(), jdk.provider().name(), + jdk.home(), details ? jdk.equals(defaultJdk) - : jdk.getMajorVersion() == defMajorVersion)) + : jdk.majorVersion() == defMajorVersion)) .collect(Collectors.toList()); if (!details) { // Only keep a list of unique major versions @@ -152,12 +156,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, JdkProvider.Predicates.canUpdate); 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,9 +169,10 @@ 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(); - if (home != null) { + JdkManager jdkMan = jdkProvidersMixin.getJdkManager(); + dev.jbang.jvm.Jdk jdk = jdkMan.getOrInstallJdk(versionOrId); + if (jdk.isInstalled()) { + Path home = jdk.home(); String homeStr = Util.pathToString(home); System.out.println(homeStr); } @@ -177,16 +182,16 @@ 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, JdkProvider.Predicates.canUpdate); } if (jdk == null || !jdk.isInstalled()) { - jdk = JdkManager.getOrInstallJdk(versionOrId); + jdk = jdkMan.getOrInstallJdk(versionOrId); } - Path home = jdk.getHome(); - if (home != null) { + if (jdk.isInstalled()) { + Path home = jdk.home(); String homeStr = Util.pathToString(home); String homeOsStr = Util.pathToOsString(home); PrintStream out = System.out; @@ -227,20 +232,24 @@ 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); - if (defjdk == null || (!jdk.equals(defjdk) && !Objects.equals(jdk.getHome(), defjdk.getHome()))) { - JdkManager.setDefaultJdk(jdk); + dev.jbang.jvm.Jdk jdk = jdkMan.getOrInstallJdk(versionOrId); + if (defjdk == null || (!jdk.equals(defjdk) && !Objects.equals(jdk.home(), defjdk.home()))) { + jdkMan.setDefaultJdk(jdk); } else { - Util.infoMsg("Default JDK already set to " + defjdk.getMajorVersion()); + Util.infoMsg("Default JDK already set to " + defjdk.majorVersion()); } } else { if (defjdk == null) { Util.infoMsg("No default JDK set, use 'jbang jdk default ' to set one."); } else { - Util.infoMsg("Default JDK is currently set to " + defjdk.getMajorVersion()); + Util.infoMsg("Default JDK is currently set to " + defjdk.majorVersion()); } } return EXIT_OK; 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..78dd2cc5b 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,8 +132,8 @@ public List getAutoDectectedModuleArguments(@Nonnull JdkProvider.Jdk jdk } } - protected boolean supportsModules(JdkProvider.Jdk jdk) { - return jdk.getMajorVersion() >= 9; + protected boolean supportsModules(Jdk jdk) { + return jdk.majorVersion() >= 9; } public List getArtifacts() { 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/LinuxJdkProvider.java b/src/main/java/dev/jbang/net/jdkproviders/LinuxJdkProvider.java deleted file mode 100644 index c8dd49b84..000000000 --- a/src/main/java/dev/jbang/net/jdkproviders/LinuxJdkProvider.java +++ /dev/null @@ -1,59 +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 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. - * - * For now just using `/usr/lib/jvm` as apparently fedora, debian, ubuntu and - * centos/rhel use it. - * - * If need different behavior per linux distro its intended this provider will - * adjust based on identified distro. - * - */ -public class LinuxJdkProvider extends BaseFoldersJdkProvider { - 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); - } - - @Override - protected boolean acceptFolder(Path jdkFolder) { - return super.acceptFolder(jdkFolder) && !isSameFolderSymLink(jdkFolder); - } - - // Returns true if a path is a symlink to an entry in the same folder - private boolean isSameFolderSymLink(Path jdkFolder) { - Path absFolder = jdkFolder.toAbsolutePath(); - if (Files.isSymbolicLink(absFolder)) { - try { - Path realPath = absFolder.toRealPath(); - return Files.isSameFile(absFolder.getParent(), realPath.getParent()); - } catch (IOException e) { - /* ignore */ } - } - return false; - } -} 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 f33ebf582..7a3fc1842 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().majorVersion() < 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 04bbd1fe5..65ebcdbb1 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; @@ -42,10 +44,13 @@ public class Project { private boolean integrations = true; 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"; @@ -242,6 +247,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(); @@ -259,6 +268,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 d082d99d2..20910fa29 100644 --- a/src/main/java/dev/jbang/source/ProjectBuilder.java +++ b/src/main/java/dev/jbang/source/ProjectBuilder.java @@ -37,6 +37,7 @@ import dev.jbang.dependencies.MavenCoordinate; import dev.jbang.dependencies.MavenRepo; import dev.jbang.dependencies.ModularClassPath; +import dev.jbang.jvm.JdkManager; import dev.jbang.source.buildsteps.JarBuildStep; import dev.jbang.source.resolvers.AliasResourceResolver; import dev.jbang.source.resolvers.ClasspathResourceResolver; @@ -76,6 +77,7 @@ public class ProjectBuilder { private Boolean integrations; private String javaVersion; private Boolean enablePreview; + private JdkManager jdkManager; // Cached values private Properties contextProperties; @@ -214,6 +216,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); @@ -380,12 +387,8 @@ public Project build(Source src) { return updateProject(updateProjectMain(src, prj, getResourceResolver())); } - /** + /* * Imports settings from jar MANIFEST.MF, pom.xml and more - * - * @param prj - * @param importModuleName - * @return */ private Project importJarMetadata(Project prj, boolean importModuleName) { Path jar = prj.getResourceRef().getFile(); @@ -412,8 +415,7 @@ private Project importJarMetadata(Project prj, boolean importModuleName) { // we pass exports/opens into the project... // TODO: this does mean we can't separate from user specified options and jar - // origined ones - // but not sure if needed? + // origined ones, but not sure if needed? // https://openjdk.org/jeps/261#Breaking-encapsulation String exports = attrs.getValue("Add-Exports"); if (exports != null) { @@ -484,6 +486,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..c551cb459 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.majorVersion() < 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.majorVersion()); } 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..fddde92e0 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().majorVersion(); 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 069f54c36..e5cc261a7 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,11 +77,10 @@ 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); - if (jdk.getMajorVersion() > 9) { + if (jdk.majorVersion() > 9) { String opens = ctx.getProject().getManifestAttributes().get("Add-Opens"); if (opens != null) { for (String val : opens.split(" ")) { @@ -158,7 +156,7 @@ protected List generateCommandLineList() throws IOException { } if (classDataSharing || project.enableCDS()) { - if (jdk.getMajorVersion() >= 13) { + if (jdk.majorVersion() >= 13) { Path cdsJsa = ctx.getJsaFile().toAbsolutePath(); if (Files.exists(cdsJsa)) { Util.verboseMsg("CDS: Using shared archive classes from " + cdsJsa); 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..cb7d95194 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() + .home() + .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..0971f61ab 100644 --- a/src/main/java/dev/jbang/util/JavaUtil.java +++ b/src/main/java/dev/jbang/util/JavaUtil.java @@ -6,34 +6,94 @@ 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.*; +import dev.jbang.jvm.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.jvm.jdkproviders.*; +import dev.jbang.jvm.util.RemoteAccessProvider; 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 = JdkProviders.instance().allNames(); + public static final List PROVIDERS_DEFAULT = JdkProviders.instance().basicNames(); + + 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()) { + providers(provider); + } + } + return super.build(); + } + + private JdkProvider createProvider(String providerName) { + JdkProvider provider; + switch (providerName) { + case "default": + provider = new DefaultJdkProvider(Settings.getDefaultJdkDir()); + break; + case "jbang": + JBangJdkProvider p = new JBangJdkProvider(); + p.installer(new FoojayJdkInstaller(p).remoteAccessProvider(new JBangRemoteAccessProvider())); + provider = p; + break; + default: + JdkDiscovery.Config cfg = new JdkDiscovery.Config(Settings.getCacheDir(Cache.CacheClass.jdks)); + provider = JdkProviders.instance().byName(providerName, cfg); + if (provider == null) { + Util.warnMsg("Unknown JDK provider: " + providerName); + } + break; + } + return provider; + } + } + + static class JBangRemoteAccessProvider implements RemoteAccessProvider { + @Override + public Path downloadFromUrl(String url) throws IOException { + return Util.downloadAndCacheFile(url); + } } /** @@ -117,9 +177,9 @@ 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(); - if (jdkHome != null) { + public static String resolveInJavaHome(@Nonnull String cmd, @Nonnull Jdk jdk) { + if (jdk.isInstalled()) { + Path jdkHome = jdk.home(); 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 a10b05324..9f3e68620 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; @@ -61,7 +62,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; @@ -2280,7 +2280,7 @@ void testReposWorksWithFresh() throws IOException { @Test void testForceJavaVersion() throws IOException { - int v = JdkManager.getJdk(null, false).getMajorVersion(); + int v = defaultJdkManager().getJdk(null).majorVersion(); 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"));