See More

// spotless:off Dependencies for JBang //DEPS dev.jbang:devkitman:0.4.2 //DEPS org.yaml:snakeyaml:2.4 //DEPS info.picocli:picocli:4.7.7 //DEPS de.vandermeer:asciitable:0.3.2 //SOURCES Jvm.java util/Version.java // spotless:on package org.codejive.jvm; import de.vandermeer.asciitable.AsciiTable; import de.vandermeer.asciitable.CWC_LongestLine; import dev.jbang.devkitman.*; import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; import dev.jbang.devkitman.util.OsUtils; import java.io.File; 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.util.*; import java.util.concurrent.Callable; import org.codejive.jvm.util.Version; import picocli.CommandLine; import picocli.CommandLine.*; /** Main class for the jvm command line tool. */ @Command( name = "jvm", versionProvider = Version.class, description = "Command line tool for installing and managing Java versions", subcommands = { Main.ListInstalled.class, Main.ListAvailable.class, Main.ListProviders.class, Main.Install.class, Main.Uninstall.class, Main.Default.class, Main.Link.class, Main.Unlink.class, Main.Env.class, Main.Run.class }) public class Main { static boolean verbose = false; static boolean quiet = false; @Option( names = {"-h", "--help"}, description = "Show this help message and exit.", usageHelp = true, scope = CommandLine.ScopeType.INHERIT) boolean showHelp; @Option( names = {"-q", "--quiet"}, description = "We will be quiet, only print when error occurs.", scope = CommandLine.ScopeType.INHERIT) public void setQuiet(boolean quiet) { Main.quiet = quiet; } @Option( names = {"-v", "--verbose"}, description = "Enable verbose output for debugging", scope = CommandLine.ScopeType.INHERIT) public void setVerbose(boolean verbose) { Main.verbose = verbose; } @Option( names = {"-V", "--version"}, description = "Show application version.", versionHelp = true) boolean showVersion; abstract static class CmdBase implements Callable {} @Command( name = "list", aliases = {"l"}, description = "List the installed Java versions") static class ListInstalled extends CmdBase { @Override public Integer call() { JdkManager manager = createJdkManager(); manager.getOrInstallJdk("11+"); List jdks = manager.listInstalledJdks(); jdks.sort(Comparator.naturalOrder().reversed()); AsciiTable at = new AsciiTable(); CWC_LongestLine cwc = new CWC_LongestLine(); at.getRenderer().setCWC(cwc); at.addRule(); at.addRow("V", "Version", "Id", "Provider", "Home"); at.addRule(); for (Jdk.InstalledJdk jdk : jdks) { at.addRow( jdk.majorVersion(), jdk.version(), jdk.id(), jdk.provider().name(), jdk.home().toString()); } at.addRule(); System.out.println(at.render()); return 0; } } @Command( name = "list-available", aliases = {"la"}, description = "List the Java versions available for installation") static class ListAvailable extends CmdBase { @Override public Integer call() { System.err.println("Retrieving available Java versions, this can take a moment..."); JdkManager manager = createJdkManager(); List jdks = manager.listAvailableJdks(); jdks.sort(Comparator.naturalOrder().reversed()); AsciiTable at = new AsciiTable(); CWC_LongestLine cwc = new CWC_LongestLine(); at.getRenderer().setCWC(cwc); at.addRule(); at.addRow("V", "Version", "Id", "Provider"); at.addRule(); for (Jdk.AvailableJdk jdk : jdks) { at.addRow(jdk.majorVersion(), jdk.version(), jdk.id(), jdk.provider().name()); } at.addRule(); System.out.println(at.render()); return 0; } } @Command( name = "list-providers", aliases = {"lp"}, description = "List the available JDK providers that can be used for '--providers' options") static class ListProviders extends CmdBase { @Override public Integer call() { Path installPath = JBangJdkProvider.getJBangJdkDir(); JdkDiscovery.Config cfg = new JdkDiscovery.Config(installPath); List providers = JdkProviders.instance().all(cfg); AsciiTable at = new AsciiTable(); CWC_LongestLine cwc = new CWC_LongestLine(); at.getRenderer().setCWC(cwc); at.addRule(); at.addRow("Provider", "Active", "Description"); at.addRule(); for (JdkProvider prov : providers) { at.addRow(prov.name(), prov.canUse(), prov.description()); } at.addRule(); System.out.println(at.render()); return 0; } } @Command( name = "install", aliases = {"i"}, description = "Install a Java JDK or JRE") static class Install extends CmdBase { @Option( names = {"-f", "--force"}, description = "Force installation even when already installed") boolean force; @Mixin JavaVersionParamMixin javaVersionParamMixin; @Override public Integer call() { String versionOrId = javaVersionParamMixin.getVersionOrId(quiet); JdkManager jdkMan = createJdkManager(); Jdk jdk = jdkMan.getInstalledJdk(versionOrId, JdkProvider.Predicates.canUpdate); if (!force && jdk != null) { if (!quiet) { System.err.println("Java version is already installed: " + jdk); System.err.println("Use --force to install anyway"); } return 1; } if (jdk == null) { jdk = jdkMan.getJdk(versionOrId, JdkProvider.Predicates.canUpdate); if (jdk == null) { if (!quiet) { System.err.println( "Java version is not available for installation: " + versionOrId); } return 2; } } if (jdk.isInstalled()) { ((Jdk.AvailableJdk) jdk).install(); } if (!quiet) { System.err.println("Successfully installed Java version " + versionOrId); } return 0; } } @Command( name = "uninstall", aliases = {"u"}, description = "Uninstall a Java JDK or JRE") static class Uninstall extends CmdBase { private String versionOrId; @Parameters(paramLabel = "version", description = "Java version to use", arity = "0..1") void setJavaVersion(String versionOrId) { this.versionOrId = versionOrId != null ? validateVersionOrId(versionOrId) : null; } @Override public Integer call() { JdkManager jdkMan = createJdkManager(); Jdk.InstalledJdk jdk = jdkMan.getInstalledJdk(versionOrId, JdkProvider.Predicates.canUpdate); if (jdk == null) { System.err.println("Java version not installed: " + versionOrId); return 1; } jdk.uninstall(); if (!quiet) { System.err.println("Successfully uninstalled Java version " + versionOrId); } return 0; } } @Command( name = "default", aliases = {"d"}, description = "Manage the default Java version") static class Default extends CmdBase { @Override public Integer call() { return 0; } } @Command( name = "link", aliases = {"L"}, description = "Link an existing Java installation") static class Link extends CmdBase { @Override public Integer call() { return 0; } } @Command( name = "unlink", aliases = {"U"}, description = "Unlink a previously linked Java installation") static class Unlink extends CmdBase { @Override public Integer call() { return 0; } } @Command( name = "env", aliases = {"e"}, description = "Print the required environment variables for a Java installation") static class Env extends CmdBase { @Mixin JavaVersionParamMixin javaVersionParamMixin; @Override public Integer call() { JdkManager jdkMan = createJdkManager(); Jdk jdk = jdkMan.getOrInstallJdk(javaVersionParamMixin.getVersionOrId(quiet)); return 0; } } @Command( name = "run", aliases = {"r"}, description = "Run a command making sure the correct Java version is used") static class Run extends CmdBase { @Mixin JavaVersionOptionMixin javaVersionOptionMixin; @Parameters(arity = "1..*", description = "Command to run") public List cmd = new ArrayList<>(); @Override public Integer call() throws IOException { if (cmd.isEmpty()) { return 0; } JdkManager jdkMan = createJdkManager(); Jdk.InstalledJdk jdk = jdkMan.getOrInstallJdk(javaVersionOptionMixin.getVersionOrId(quiet)); if (Paths.get(cmd.get(0)).getNameCount() == 1) { Path cmdPath = OsUtils.searchPath(cmd.get(0), jdk.home().resolve("bin").toString()); if (cmdPath != null) { cmd.set(0, cmdPath.toString()); } } ProcessBuilder pb = new ProcessBuilder(cmd).inheritIO(); String javaHomeKey = findKey(pb.environment(), "JAVA_HOME"); pb.environment().put(javaHomeKey, jdk.home().toString()); String pathKey = findKey(pb.environment(), "PATH"); String newPath = jdk.home().resolve("bin") + File.pathSeparator + System.getenv("PATH"); pb.environment().put(pathKey, newPath); try { return pb.start().waitFor(); } catch (InterruptedException e) { return 254; } } private String findKey(Map env, String key) { return env.keySet().stream() .filter(k -> k.equalsIgnoreCase(key)) .findFirst() .orElse(key); } } static class JavaVersionOptionMixin { private String versionOrId; @Option( names = {"-j", "--java"}, description = "Java version to use") void setJavaVersion(String versionOrId) { if (!versionOrId.matches("\\d+[+!]?")) { throw new IllegalArgumentException( "Invalid version, should be a number optionally followed by an exclamation mark"); } this.versionOrId = versionOrId; } String getVersionOrId(boolean quiet) { try { return Main.getVersionOrId(versionOrId); } catch (IOException e) { if (!quiet) { System.err.println("Error reading .jvmrc file: " + e.getMessage()); } return null; } } } static class JavaVersionParamMixin { private String versionOrId; @Parameters(paramLabel = "version", description = "Java version to use", arity = "0..1") void setJavaVersion(String versionOrId) { this.versionOrId = versionOrId != null ? validateVersionOrId(versionOrId) : null; } String getVersionOrId(boolean quiet) { try { return Main.getVersionOrId(versionOrId); } catch (IOException e) { if (!quiet) { System.err.println("Error reading .jvmrc file: " + e.getMessage()); } return null; } } } private static String getVersionOrId(String versionOrId) throws IOException { if (versionOrId == null) { Path cfgFile = Paths.get(".jvmrc"); if (!Files.exists(cfgFile)) { cfgFile = Paths.get(System.getProperty("user.home"), ".jvmrc"); } if (Files.exists(cfgFile)) { Properties cfg = new Properties(); try (InputStream is = Files.newInputStream(cfgFile)) { cfg.load(is); versionOrId = cfg.getProperty("java"); } } } return versionOrId != null ? validateVersionOrId(versionOrId) : null; } private static String validateVersionOrId(String versionOrId) { String[] parts = versionOrId.split(","); String v = parts[0]; if (v.endsWith("+") && isInteger(v.substring(0, v.length() - 1))) { // Nothing to do } else if (v.endsWith("!") && isInteger(v.substring(0, v.length() - 1))) { parts[0] = v.substring(0, v.length() - 1); } else if (isInteger(v)) { parts[0] = v + "+"; } else if (parts.length == 1 && isId(v)) { // Nothing to do } else { throw new IllegalArgumentException( "Invalid version, should be a number optionally preceded by an exclamation mark"); } versionOrId = String.join(",", parts); return versionOrId; } private static boolean isInteger(String s) { try { Integer.parseInt(s); return true; } catch (NumberFormatException e) { return false; } } private static boolean isId(String s) { return s.matches("[a-zA-Z0-9_\\-.]+"); } private static JdkManager createJdkManager() { Path installPath = JBangJdkProvider.getJBangJdkDir(); Path cachePath = cachePath(); JdkDiscovery.Config cfg = new JdkDiscovery.Config(installPath, cachePath, null); cfg.properties() .put("link", JBangJdkProvider.getJBangConfigDir().resolve("currentjdk").toString()); return JdkManager.builder().providers(JdkProviders.instance().all(cfg)).build(); } private static Path cachePath() { String cachePath = System.getenv("JVM_CACHE"); Path cacheDir = null; if (cachePath == null) { cachePath = System.getProperty("jvm.cache"); if (cachePath != null) { cacheDir = Paths.get(cachePath); } else { cacheDir = Paths.get(System.getProperty("user.home"), ".cache", "jvm"); } } return cacheDir; } static CommandLine.IExecutionExceptionHandler errorHandler = (ex, commandLine, parseResult) -> { System.err.println("Error: " + ex.getMessage()); if (verbose) { ex.printStackTrace(); } else if (!quiet) { System.err.println( "(Run with --verbose for more details. If you believe you found a bug in jpm, open an issue at https://github.com/codejive/java-jpm/issues)"); } return commandLine.getCommandSpec().exitCodeOnExecutionException(); }; /** * Main entry point for the jvm command line tool. * * @param args The command line arguments. */ public static void main(String... args) { new CommandLine(new Main()).setStopAtPositional(true).execute(args); } }