8170289: Re-examine entry point support in jlink
authorsundar
Mon, 19 Dec 2016 09:48:59 +0530
changeset 42762 11a1ad340d4f
parent 42761 c3798108e1ad
child 42763 31a6badb98e8
8170289: Re-examine entry point support in jlink Reviewed-by: mchung
jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/builder/DefaultImageBuilder.java
jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java
jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java
jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java
jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties
jdk/test/tools/jlink/IntegrationTest.java
jdk/test/tools/jlink/basic/BasicTest.java
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/builder/DefaultImageBuilder.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/builder/DefaultImageBuilder.java	Mon Dec 19 09:48:59 2016 +0530
@@ -130,6 +130,7 @@
     }
 
     private final Path root;
+    private final Map<String, String> launchers;
     private final Path mdir;
     private final Set<String> modules = new HashSet<>();
     private String targetOsName;
@@ -140,10 +141,9 @@
      * @param root The image root directory.
      * @throws IOException
      */
-    public DefaultImageBuilder(Path root) throws IOException {
-        Objects.requireNonNull(root);
-
-        this.root = root;
+    public DefaultImageBuilder(Path root, Map<String, String> launchers) throws IOException {
+        this.root = Objects.requireNonNull(root);
+        this.launchers = Objects.requireNonNull(launchers);
         this.mdir = root.resolve("lib");
         Files.createDirectories(mdir);
     }
@@ -235,7 +235,7 @@
             // If native files are stripped completely, <root>/bin dir won't exist!
             // So, don't bother generating launcher scripts.
             if (Files.isDirectory(bin)) {
-                 prepareApplicationFiles(files, modules);
+                 prepareApplicationFiles(files);
             }
         } catch (IOException ex) {
             throw new PluginException(ex);
@@ -246,22 +246,44 @@
      * Generates launcher scripts.
      *
      * @param imageContent The image content.
-     * @param modules The set of modules that the runtime image contains.
      * @throws IOException
      */
-    protected void prepareApplicationFiles(ResourcePool imageContent, Set<String> modules) throws IOException {
+    protected void prepareApplicationFiles(ResourcePool imageContent) throws IOException {
         // generate launch scripts for the modules with a main class
-        for (String module : modules) {
+        for (Map.Entry<String, String> entry : launchers.entrySet()) {
+            String launcherEntry = entry.getValue();
+            int slashIdx = launcherEntry.indexOf("/");
+            String module, mainClassName;
+            if (slashIdx == -1) {
+                module = launcherEntry;
+                mainClassName = null;
+            } else {
+                module = launcherEntry.substring(0, slashIdx);
+                assert !module.isEmpty();
+                mainClassName = launcherEntry.substring(slashIdx + 1);
+                assert !mainClassName.isEmpty();
+            }
+
             String path = "/" + module + "/module-info.class";
             Optional<ResourcePoolEntry> res = imageContent.findEntry(path);
             if (!res.isPresent()) {
                 throw new IOException("module-info.class not found for " + module + " module");
             }
-            Optional<String> mainClass;
             ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes());
-            mainClass = ModuleDescriptor.read(stream).mainClass();
-            if (mainClass.isPresent()) {
-                Path cmd = root.resolve("bin").resolve(module);
+            Optional<String> mainClass = ModuleDescriptor.read(stream).mainClass();
+            if (mainClassName == null && mainClass.isPresent()) {
+                mainClassName = mainClass.get();
+            }
+
+            if (mainClassName != null) {
+                // make sure main class exists!
+                if (!imageContent.findEntry("/" + module + "/" +
+                        mainClassName.replace('.', '/') + ".class").isPresent()) {
+                    throw new IllegalArgumentException(module + " does not have main class: " + mainClassName);
+                }
+
+                String launcherFile = entry.getKey();
+                Path cmd = root.resolve("bin").resolve(launcherFile);
                 // generate shell script for Unix platforms
                 StringBuilder sb = new StringBuilder();
                 sb.append("#!/bin/sh")
@@ -272,7 +294,7 @@
                         .append("\n");
                 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ")
                         .append(module).append('/')
-                        .append(mainClass.get())
+                        .append(mainClassName)
                         .append(" $@\n");
 
                 try (BufferedWriter writer = Files.newBufferedWriter(cmd,
@@ -286,7 +308,7 @@
                 }
                 // generate .bat file for Windows
                 if (isWindows()) {
-                    Path bat = root.resolve(BIN_DIRNAME).resolve(module + ".bat");
+                    Path bat = root.resolve(BIN_DIRNAME).resolve(launcherFile + ".bat");
                     sb = new StringBuilder();
                     sb.append("@echo off")
                             .append("\r\n");
@@ -296,7 +318,7 @@
                             .append("\r\n");
                     sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ")
                             .append(module).append('/')
-                            .append(mainClass.get())
+                            .append(mainClassName)
                             .append(" %*\r\n");
 
                     try (BufferedWriter writer = Files.newBufferedWriter(bat,
@@ -305,6 +327,8 @@
                         writer.write(sb.toString());
                     }
                 }
+            } else {
+                throw new IllegalArgumentException(module + " doesn't contain main class & main not specified in command line");
             }
         }
     }
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java	Mon Dec 19 09:48:59 2016 +0530
@@ -111,6 +111,27 @@
             task.options.output = path;
         }, "--output"),
         new Option<JlinkTask>(true, (task, opt, arg) -> {
+            String[] values = arg.split("=");
+            // check values
+            if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
+                throw taskHelper.newBadArgs("err.launcher.value.format", arg);
+            } else {
+                String commandName = values[0];
+                String moduleAndMain = values[1];
+                int idx = moduleAndMain.indexOf("/");
+                if (idx != -1) {
+                    if (moduleAndMain.substring(0, idx).isEmpty()) {
+                        throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
+                    }
+
+                    if (moduleAndMain.substring(idx + 1).isEmpty()) {
+                        throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
+                    }
+                }
+                task.options.launchers.put(commandName, moduleAndMain);
+            }
+        }, "--launcher"),
+        new Option<JlinkTask>(true, (task, opt, arg) -> {
             if ("little".equals(arg)) {
                 task.options.endian = ByteOrder.LITTLE_ENDIAN;
             } else if ("big".equals(arg)) {
@@ -170,6 +191,7 @@
         final Set<String> limitMods = new HashSet<>();
         final Set<String> addMods = new HashSet<>();
         Path output;
+        final Map<String, String> launchers = new HashMap<>();
         Path packagedModulesPath;
         ByteOrder endian = ByteOrder.nativeOrder();
         boolean ignoreSigning = false;
@@ -287,7 +309,7 @@
     }
 
     private void postProcessOnly(Path existingImage) throws Exception {
-        PluginsConfiguration config = taskHelper.getPluginsConfig(null);
+        PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
         ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
         if (img == null) {
             throw taskHelper.newBadArgs("err.existing.image.invalid");
@@ -336,7 +358,7 @@
 
         // Then create the Plugin Stack
         ImagePluginStack stack = ImagePluginConfiguration.
-                parseConfiguration(taskHelper.getPluginsConfig(options.output));
+                parseConfiguration(taskHelper.getPluginsConfig(options.output, options.launchers));
 
         //Ask the stack to proceed
         stack.operate(imageProvider);
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java	Mon Dec 19 09:48:59 2016 +0530
@@ -403,7 +403,7 @@
             return null;
         }
 
-        private PluginsConfiguration getPluginsConfig(Path output
+        private PluginsConfiguration getPluginsConfig(Path output, Map<String, String> launchers
                     ) throws IOException, BadArgs {
             if (output != null) {
                 if (Files.exists(output)) {
@@ -440,9 +440,9 @@
             // recreate or postprocessing don't require an output directory.
             ImageBuilder builder = null;
             if (output != null) {
-                builder = new DefaultImageBuilder(output);
+                builder = new DefaultImageBuilder(output, launchers);
+            }
 
-            }
             return new Jlink.PluginsConfiguration(pluginsList,
                     builder, lastSorter);
         }
@@ -745,9 +745,9 @@
                 + bundleHelper.getMessage(key, args));
     }
 
-    public PluginsConfiguration getPluginsConfig(Path output)
+    public PluginsConfiguration getPluginsConfig(Path output, Map<String, String> launchers)
             throws IOException, BadArgs {
-        return pluginOptions.getPluginsConfig(output);
+        return pluginOptions.getPluginsConfig(output, launchers);
     }
 
     public Path getExistingImage() {
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java	Mon Dec 19 09:48:59 2016 +0530
@@ -49,6 +49,7 @@
  */
 public final class AppRuntimeImageBuilder {
     private Path outputDir = null;
+    private Map<String, String> launchers = Collections.emptyMap();
     private List<Path> modulePath = null;
     private Set<String> addModules = null;
     private Set<String> limitModules = null;
@@ -62,6 +63,10 @@
         outputDir = value;
     }
 
+    public void setLaunchers(Map<String, String> value) {
+        launchers = value;
+    }
+
     public void setModulePath(List<Path> value) {
         modulePath = value;
     }
@@ -120,7 +125,7 @@
 
         // build the image
         Jlink.PluginsConfiguration pluginConfig = new Jlink.PluginsConfiguration(
-            plugins, new DefaultImageBuilder(outputDir), null);
+            plugins, new DefaultImageBuilder(outputDir, launchers), null);
         Jlink jlink = new Jlink();
         jlink.build(jlinkConfig, pluginConfig);
     }
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties	Mon Dec 19 09:48:59 2016 +0530
@@ -53,6 +53,11 @@
 main.opt.output=\
 \      --output <path>                   Location of output path
 
+main.opt.launcher=\
+\      --launcher <command>=<module>     Launcher command name for the module\n\
+\      --launcher <command>=<module>/<main>\n\
+\                                        Launcher command name for the module and the main class
+
 main.command.files=\
 \      @<filename>                       Read options from file
 
@@ -91,6 +96,9 @@
 
 
 err.unknown.byte.order:unknown byte order {0}
+err.launcher.main.class.empty:launcher main class name cannot be empty: {0}
+err.launcher.module.name.empty:launcher module name cannot be empty: {0}
+err.launcher.value.format:launcher value should be of form <command>=<module>[/<main-class>]: {0}
 err.output.must.be.specified:--output must be specified
 err.modulepath.must.be.specified:--module-path must be specified
 err.mods.must.be.specified:no modules specified to {0}
--- a/jdk/test/tools/jlink/IntegrationTest.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/test/tools/jlink/IntegrationTest.java	Mon Dec 19 09:48:59 2016 +0530
@@ -186,7 +186,7 @@
             lst.add(new MyPostProcessor());
         }
         // Image builder
-        DefaultImageBuilder builder = new DefaultImageBuilder(output);
+        DefaultImageBuilder builder = new DefaultImageBuilder(output, Collections.emptyMap());
         PluginsConfiguration plugins
                 = new Jlink.PluginsConfiguration(lst, builder, null);
 
--- a/jdk/test/tools/jlink/basic/BasicTest.java	Sun Dec 18 18:09:05 2016 -0800
+++ b/jdk/test/tools/jlink/basic/BasicTest.java	Mon Dec 19 09:48:59 2016 +0530
@@ -87,20 +87,29 @@
         JarUtils.createJarFile(jarfile, classes);
 
         Path image = Paths.get("mysmallimage");
-        runJmod(jarfile.toString(), TEST_MODULE);
-        runJlink(image, TEST_MODULE, "--compress", "2");
-        execute(image, TEST_MODULE);
+        runJmod(jarfile.toString(), TEST_MODULE, true);
+        runJlink(image, TEST_MODULE, "--compress", "2", "--launcher", "foo=" + TEST_MODULE);
+        execute(image, "foo");
 
         Files.delete(jmods.resolve(TEST_MODULE + ".jmod"));
 
         image = Paths.get("myimage");
-        runJmod(classes.toString(), TEST_MODULE);
-        runJlink(image, TEST_MODULE);
-        execute(image, TEST_MODULE);
+        runJmod(classes.toString(), TEST_MODULE, true);
+        runJlink(image, TEST_MODULE, "--launcher", "bar=" + TEST_MODULE);
+        execute(image, "bar");
+
+        Files.delete(jmods.resolve(TEST_MODULE + ".jmod"));
+
+        image = Paths.get("myimage2");
+        runJmod(classes.toString(), TEST_MODULE, false /* no ModuleMainClass! */);
+        // specify main class in --launcher command line
+        runJlink(image, TEST_MODULE, "--launcher", "bar2=" + TEST_MODULE + "/jdk.test.Test");
+        execute(image, "bar2");
+
     }
 
-    private void execute(Path image, String moduleName) throws Throwable {
-        String cmd = image.resolve("bin").resolve(moduleName).toString();
+    private void execute(Path image, String scriptName) throws Throwable {
+        String cmd = image.resolve("bin").resolve(scriptName).toString();
         OutputAnalyzer analyzer;
         if (System.getProperty("os.name").startsWith("Windows")) {
             analyzer = ProcessTools.executeProcess("sh.exe", cmd, "1", "2", "3");
@@ -127,14 +136,25 @@
         }
     }
 
-    private void runJmod(String cp, String modName) {
-        int rc = JMOD_TOOL.run(System.out, System.out, new String[] {
+    private void runJmod(String cp, String modName, boolean main) {
+        int rc;
+        if (main) {
+            rc = JMOD_TOOL.run(System.out, System.out, new String[] {
                 "create",
                 "--class-path", cp,
                 "--module-version", "1.0",
                 "--main-class", "jdk.test.Test",
+                jmods.resolve(modName + ".jmod").toString()
+            });
+        } else {
+            rc = JMOD_TOOL.run(System.out, System.out, new String[] {
+                "create",
+                "--class-path", cp,
+                "--module-version", "1.0",
                 jmods.resolve(modName + ".jmod").toString(),
-        });
+            });
+        }
+
         if (rc != 0) {
             throw new AssertionError("Jmod failed: rc = " + rc);
         }