changeset 59643:c637b86b047a

8246627: Consolidate app image bundlers Reviewed-by: herrick, almatvee
author asemenyuk
date Mon, 08 Jun 2020 09:13:00 -0400
parents e622c2cce517
children c1c690f4dd9c
files src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java src/jdk.incubator.jpackage/macosx/native/applauncher/MacLauncher.cpp src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageBundler.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CfgFile.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java test/jdk/tools/jpackage/share/jdk/jpackage/tests/JLinkOptionsTest.java
diffstat 33 files changed, 719 insertions(+), 1063 deletions(-) [+]
line wrap: on
line diff
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,19 +25,34 @@
 package jdk.incubator.jpackage.internal;
 
 import java.awt.image.BufferedImage;
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.imageio.ImageIO;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.ICON_PNG;
 import static jdk.incubator.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON;
+import static jdk.incubator.jpackage.internal.LinuxAppImageBuilder.ICON_PNG;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.DESCRIPTION;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FILE_ASSOCIATIONS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.ICON;
 
 /**
  * Helper to create files for desktop integration.
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,140 +25,8 @@
 
 package jdk.incubator.jpackage.internal;
 
-import java.io.File;
-import java.text.MessageFormat;
-import java.util.*;
-
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-
-public class LinuxAppBundler extends AbstractImageBundler {
-
-    static final BundlerParamInfo<File> ICON_PNG =
-            new StandardBundlerParam<>(
-            "icon.png",
-            File.class,
-            params -> {
-                File f = ICON.fetchFrom(params);
-                if (f != null && !f.getName().toLowerCase().endsWith(".png")) {
-                    Log.error(MessageFormat.format(
-                            I18N.getString("message.icon-not-png"), f));
-                    return null;
-                }
-                return f;
-            },
-            (s, p) -> new File(s));
-
-    static final BundlerParamInfo<String> LINUX_INSTALL_DIR =
-            new StandardBundlerParam<>(
-            "linux-install-dir",
-            String.class,
-            params -> {
-                 String dir = INSTALL_DIR.fetchFrom(params);
-                 if (dir != null) {
-                     if (dir.endsWith("/")) {
-                         dir = dir.substring(0, dir.length()-1);
-                     }
-                     return dir;
-                 }
-                 return "/opt";
-             },
-            (s, p) -> s
-    );
-
-    static final BundlerParamInfo<String> LINUX_PACKAGE_DEPENDENCIES =
-            new StandardBundlerParam<>(
-            Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(),
-            String.class,
-            params -> {
-                 return "";
-             },
-            (s, p) -> s
-    );
-
-    @Override
-    public boolean validate(Map<String, ? super Object> params)
-            throws ConfigException {
-        try {
-            Objects.requireNonNull(params);
-            return doValidate(params);
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
-            }
-        }
+public class LinuxAppBundler extends AppImageBundler {
+    public LinuxAppBundler() {
+        setAppImageSupplier(LinuxAppImageBuilder::new);
     }
-
-    private boolean doValidate(Map<String, ? super Object> params)
-            throws ConfigException {
-
-        imageBundleValidation(params);
-
-        return true;
-    }
-
-    File doBundle(Map<String, ? super Object> params, File outputDirectory,
-            boolean dependentTask) throws PackagerException {
-        if (StandardBundlerParam.isRuntimeInstaller(params)) {
-            return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
-        } else {
-            return doAppBundle(params, outputDirectory, dependentTask);
-        }
-    }
-
-    private File doAppBundle(Map<String, ? super Object> params,
-            File outputDirectory, boolean dependentTask)
-            throws PackagerException {
-        try {
-            File rootDirectory = createRoot(params, outputDirectory,
-                    dependentTask, APP_NAME.fetchFrom(params));
-            AbstractAppImageBuilder appBuilder = new LinuxAppImageBuilder(
-                    params, outputDirectory.toPath());
-            if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) {
-                JLinkBundlerHelper.execute(params, appBuilder);
-            } else {
-                StandardBundlerParam.copyPredefinedRuntimeImage(
-                        params, appBuilder);
-            }
-            return rootDirectory;
-        } catch (PackagerException pe) {
-            throw pe;
-        } catch (Exception ex) {
-            Log.verbose(ex);
-            throw new PackagerException(ex);
-        }
-    }
-
-    @Override
-    public String getName() {
-        return I18N.getString("app.bundler.name");
-    }
-
-    @Override
-    public String getID() {
-        return "linux.app";
-    }
-
-    @Override
-    public String getBundleType() {
-        return "IMAGE";
-    }
-
-    @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return doBundle(params, outputParentDir, false);
-    }
-
-    @Override
-    public boolean supported(boolean runtimeInstaller) {
-        return true;
-    }
-
-    @Override
-    public boolean isDefault() {
-        return false;
-    }
-
 }
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java	Mon Jun 08 09:13:00 2020 -0400
@@ -30,31 +30,33 @@
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.HashMap;
+import java.text.MessageFormat;
 import java.util.List;
 import java.util.Map;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.ICON_PNG;
-import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
-
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.ICON;
 
 public class LinuxAppImageBuilder extends AbstractAppImageBuilder {
 
+    static final BundlerParamInfo<File> ICON_PNG =
+            new StandardBundlerParam<>(
+            "icon.png",
+            File.class,
+            params -> {
+                File f = ICON.fetchFrom(params);
+                if (f != null && !f.getName().toLowerCase().endsWith(".png")) {
+                    Log.error(MessageFormat.format(
+                            I18N.getString("message.icon-not-png"), f));
+                    return null;
+                }
+                return f;
+            },
+            (s, p) -> new File(s));
+
     final static String DEFAULT_ICON = "java32.png";
 
-    private final ApplicationLayout appLayout;
-
-    private static ApplicationLayout createAppLayout(Map<String, Object> params,
-            Path imageOutDir) {
-        return ApplicationLayout.linuxAppImage().resolveAt(
-                imageOutDir.resolve(APP_NAME.fetchFrom(params)));
-    }
-
-    public LinuxAppImageBuilder(Map<String, Object> params, Path imageOutDir)
-            throws IOException {
-        super(params, createAppLayout(params, imageOutDir).runtimeDirectory());
-
-        appLayout = createAppLayout(params, imageOutDir);
+    LinuxAppImageBuilder(Path imageOutDir) {
+        super(imageOutDir);
     }
 
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
@@ -66,34 +68,6 @@
         return APP_NAME.fetchFrom(params);
     }
 
-    private Path getLauncherCfgPath(Map<String, ? super Object> params) {
-        return appLayout.appDirectory().resolve(
-                APP_NAME.fetchFrom(params) + ".cfg");
-    }
-
-    @Override
-    public Path getAppDir() {
-        return appLayout.appDirectory();
-    }
-
-    @Override
-    public Path getAppModsDir() {
-        return appLayout.appModsDirectory();
-    }
-
-    @Override
-    protected String getCfgAppDir() {
-        return Path.of("$ROOTDIR").resolve(
-                ApplicationLayout.linuxAppImage().appDirectory()).toString()
-                + File.separator;
-    }
-
-    @Override
-    protected String getCfgRuntimeDir() {
-        return Path.of("$ROOTDIR").resolve(
-              ApplicationLayout.linuxAppImage().runtimeDirectory()).toString();
-    }
-
     @Override
     public void prepareApplicationFiles(Map<String, ? super Object> params)
             throws IOException {
@@ -120,10 +94,6 @@
         copyApplication(params);
     }
 
-    @Override
-    public void prepareJreFiles(Map<String, ? super Object> params)
-            throws IOException {}
-
     private void createLauncherForEntryPoint(Map<String, ? super Object> params,
             Map<String, ? super Object> mainParams) throws IOException {
         // Copy executable to launchers folder
@@ -136,7 +106,7 @@
         executableFile.toFile().setExecutable(true, false);
         executableFile.toFile().setWritable(true, true);
 
-        writeCfgFile(params, getLauncherCfgPath(params).toFile());
+        writeCfgFile(params);
 
         var iconResource = createIconResource(DEFAULT_ICON, ICON_PNG, params,
                 mainParams);
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -40,7 +40,6 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
 
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -34,8 +34,6 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import static jdk.incubator.jpackage.internal.DesktopIntegration.*;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
 
 
@@ -43,6 +41,7 @@
 
     LinuxPackageBundler(BundlerParamInfo<String> packageName) {
         this.packageName = packageName;
+        appImageBundler = new LinuxAppBundler().setDependentTask(true);
     }
 
     @Override
@@ -51,7 +50,7 @@
 
         // run basic validation to ensure requirements are met
         // we are not interested in return code, only possible exception
-        APP_BUNDLER.fetchFrom(params).validate(params);
+        appImageBundler.validate(params);
 
         validateInstallDir(LINUX_INSTALL_DIR.fetchFrom(params));
 
@@ -115,8 +114,8 @@
                 initAppImageLayout.apply(appImage).copy(
                         thePackage.sourceApplicationLayout());
             } else {
-                appImage = APP_BUNDLER.fetchFrom(params).doBundle(params,
-                        thePackage.sourceRoot().toFile(), true);
+                appImage = appImageBundler.execute(params,
+                        thePackage.sourceRoot().toFile());
                 ApplicationLayout srcAppLayout = initAppImageLayout.apply(
                         appImage);
                 if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) {
@@ -314,15 +313,32 @@
     }
 
     private final BundlerParamInfo<String> packageName;
+    private final Bundler appImageBundler;
     private boolean withFindNeededPackages;
     private DesktopIntegration desktopIntegration;
 
-    private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
-        new StandardBundlerParam<>(
-                "linux.app.bundler",
-                LinuxAppBundler.class,
-                (params) -> new LinuxAppBundler(),
-                null
-        );
+    private static final BundlerParamInfo<String> LINUX_PACKAGE_DEPENDENCIES =
+            new StandardBundlerParam<>(
+            Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(),
+            String.class,
+            params -> "",
+            (s, p) -> s
+    );
 
+    static final BundlerParamInfo<String> LINUX_INSTALL_DIR =
+            new StandardBundlerParam<>(
+            "linux-install-dir",
+            String.class,
+            params -> {
+                 String dir = INSTALL_DIR.fetchFrom(params);
+                 if (dir != null) {
+                     if (dir.endsWith("/")) {
+                         dir = dir.substring(0, dir.length()-1);
+                     }
+                     return dir;
+                 }
+                 return "/opt";
+             },
+            (s, p) -> s
+    );
 }
--- a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -32,10 +32,8 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
 
 /**
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,22 +25,21 @@
 
 package jdk.incubator.jpackage.internal;
 
-import java.io.File;
 import java.io.IOException;
-import java.math.BigInteger;
 import java.text.MessageFormat;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
-import java.util.ResourceBundle;
+import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
+import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.MAIN_CLASS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERBOSE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
 
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*;
-
-public class MacAppBundler extends AbstractImageBundler {
-
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.MacResources");
+public class MacAppBundler extends AppImageBundler {
+     public MacAppBundler() {
+        setAppImageSupplier(MacAppImageBuilder::new);
+        setParamsValidator(MacAppBundler::doValidate);
+    }
 
     private static final String TEMPLATE_BUNDLE_ICON = "java.icns";
 
@@ -99,27 +98,11 @@
         return s;
     }
 
-    @Override
-    public boolean validate(Map<String, ? super Object> params)
-            throws ConfigException {
-        try {
-            return doValidate(params);
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
-            }
-        }
-    }
-
-    private boolean doValidate(Map<String, ? super Object> params)
+    private static void doValidate(Map<String, ? super Object> params)
             throws ConfigException {
 
-        imageBundleValidation(params);
-
         if (StandardBundlerParam.getPredefinedAppImage(params) != null) {
-            return true;
+            return;
         }
 
         // validate short version
@@ -156,74 +139,5 @@
                 throw new ConfigException(ex);
             }
         }
-
-        return true;
     }
-
-    File doBundle(Map<String, ? super Object> params, File outputDirectory,
-            boolean dependentTask) throws PackagerException {
-        if (StandardBundlerParam.isRuntimeInstaller(params)) {
-            return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
-        } else {
-            return doAppBundle(params, outputDirectory, dependentTask);
-        }
-    }
-
-    File doAppBundle(Map<String, ? super Object> params, File outputDirectory,
-            boolean dependentTask) throws PackagerException {
-        try {
-            File rootDirectory = createRoot(params, outputDirectory,
-                    dependentTask, APP_NAME.fetchFrom(params) + ".app");
-            AbstractAppImageBuilder appBuilder =
-                    new MacAppImageBuilder(params, outputDirectory.toPath());
-            if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) {
-                JLinkBundlerHelper.execute(params, appBuilder);
-            } else {
-                StandardBundlerParam.copyPredefinedRuntimeImage(
-                        params, appBuilder);
-            }
-            return rootDirectory;
-        } catch (PackagerException pe) {
-            throw pe;
-        } catch (Exception ex) {
-            Log.verbose(ex);
-            throw new PackagerException(ex);
-        }
-    }
-
-    /////////////////////////////////////////////////////////////////////////
-    // Implement Bundler
-    /////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public String getName() {
-        return I18N.getString("app.bundler.name");
-    }
-
-    @Override
-    public String getID() {
-        return "mac.app";
-    }
-
-    @Override
-    public String getBundleType() {
-        return "IMAGE";
-    }
-
-    @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return doBundle(params, outputParentDir, false);
-    }
-
-    @Override
-    public boolean supported(boolean runtimeInstaller) {
-        return true;
-    }
-
-    @Override
-    public boolean isDefault() {
-        return false;
-    }
-
 }
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java	Mon Jun 08 09:13:00 2020 -0400
@@ -30,10 +30,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
-import java.math.BigInteger;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.PosixFilePermission;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -42,7 +40,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.Set;
@@ -54,11 +51,22 @@
 import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathFactory;
-
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*;
-import static jdk.incubator.jpackage.internal.MacAppBundler.*;
+import static jdk.incubator.jpackage.internal.MacAppBundler.BUNDLE_ID_SIGNING_PREFIX;
+import static jdk.incubator.jpackage.internal.MacAppBundler.DEVELOPER_ID_APP_SIGNING_KEY;
+import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.COPYRIGHT;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FA_CONTENT_TYPE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FA_DESCRIPTION;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FA_EXTENSIONS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FA_ICON;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.FILE_ASSOCIATIONS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.ICON;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.MAIN_CLASS;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
 
 public class MacAppImageBuilder extends AbstractAppImageBuilder {
 
@@ -74,13 +82,10 @@
 
     private final Path root;
     private final Path contentsDir;
-    private final Path appDir;
-    private final Path javaModsDir;
     private final Path resourcesDir;
     private final Path macOSDir;
     private final Path runtimeDir;
     private final Path runtimeRoot;
-    private final Path mdir;
 
     private static List<String> keyChains;
 
@@ -139,26 +144,15 @@
                     null : Boolean.valueOf(s)
         );
 
-    public MacAppImageBuilder(Map<String, Object> params, Path imageOutDir)
-            throws IOException {
-        super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params)
-                + ".app/Contents/runtime/Contents/Home"));
+    public MacAppImageBuilder(Path imageOutDir) {
+        super(imageOutDir);
 
-        Objects.requireNonNull(imageOutDir);
-
-        this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
+        this.root = imageOutDir;
         this.contentsDir = root.resolve("Contents");
-        this.appDir = contentsDir.resolve("app");
-        this.javaModsDir = appDir.resolve("mods");
-        this.resourcesDir = contentsDir.resolve("Resources");
-        this.macOSDir = contentsDir.resolve("MacOS");
-        this.runtimeDir = contentsDir.resolve("runtime");
-        this.runtimeRoot = runtimeDir.resolve("Contents/Home");
-        this.mdir = runtimeRoot.resolve("lib");
-        Files.createDirectories(appDir);
-        Files.createDirectories(resourcesDir);
-        Files.createDirectories(macOSDir);
-        Files.createDirectories(runtimeDir);
+        this.resourcesDir = appLayout.destktopIntegrationDirectory();
+        this.macOSDir = appLayout.launchersDirectory();
+        this.runtimeDir = appLayout.runtimeDirectory();
+        this.runtimeRoot = appLayout.runtimeHomeDirectory();
     }
 
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
@@ -167,18 +161,10 @@
     }
 
     @Override
-    public Path getAppDir() {
-        return appDir;
-    }
-
-    @Override
-    public Path getAppModsDir() {
-        return javaModsDir;
-    }
-
-    @Override
     public void prepareApplicationFiles(Map<String, ? super Object> params)
             throws IOException {
+        Files.createDirectories(macOSDir);
+
         Map<String, ? super Object> originalParams = new HashMap<>(params);
         // Generate PkgInfo
         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
@@ -195,8 +181,7 @@
         }
         executable.toFile().setExecutable(true, false);
         // generate main app launcher config file
-        File cfg = new File(root.toFile(), getLauncherCfgName(params));
-        writeCfgFile(params, cfg);
+        writeCfgFile(params);
 
         // create additional app launcher(s) and config file(s)
         List<Map<String, ? super Object>> entryPoints =
@@ -213,8 +198,7 @@
             addExecutable.toFile().setExecutable(true, false);
 
             // add config file for add launcher
-            cfg = new File(root.toFile(), getLauncherCfgName(tmp));
-            writeCfgFile(tmp, cfg);
+            writeCfgFile(tmp);
         }
 
         // Copy class path entries to Java folder
@@ -244,19 +228,6 @@
         sign(params);
     }
 
-    @Override
-    public void prepareJreFiles(Map<String, ? super Object> params)
-            throws IOException {
-        copyRuntimeFiles(params);
-        sign(params);
-    }
-
-    @Override
-    File getRuntimeImageDir(File runtimeImageTop) {
-        File home = new File(runtimeImageTop, "Contents/Home");
-        return (home.exists() ? home : runtimeImageTop);
-    }
-
     private void copyRuntimeFiles(Map<String, ? super Object> params)
             throws IOException {
         // Generate Info.plist
@@ -265,18 +236,6 @@
         // generate java runtime info.plist
         writeRuntimeInfoPlist(
                 runtimeDir.resolve("Contents/Info.plist").toFile(), params);
-
-        // copy library
-        Path runtimeMacOSDir = Files.createDirectories(
-                runtimeDir.resolve("Contents/MacOS"));
-
-        // JDK 9, 10, and 11 have extra '/jli/' subdir
-        Path jli = runtimeRoot.resolve("lib/libjli.dylib");
-        if (!Files.exists(jli)) {
-            jli = runtimeRoot.resolve("lib/jli/libjli.dylib");
-        }
-
-        Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib"));
     }
 
     private void sign(Map<String, ? super Object> params) throws IOException {
@@ -315,11 +274,6 @@
         return APP_NAME.fetchFrom(params);
     }
 
-    public static String getLauncherCfgName(
-            Map<String, ? super Object> params) {
-        return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg";
-    }
-
     private String getBundleName(Map<String, ? super Object> params) {
         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -34,25 +34,15 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.ResourceBundle;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.INSTALL_DIR;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
 
 public abstract class MacBaseInstallerBundler extends AbstractBundler {
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.MacResources");
-
-    // This could be generalized more to be for any type of Image Bundler
-    public static final BundlerParamInfo<MacAppBundler> APP_BUNDLER =
-            new StandardBundlerParam<>(
-            "mac.app.bundler",
-            MacAppBundler.class,
-            params -> new MacAppBundler(),
-            (s, p) -> null);
-
     public final BundlerParamInfo<File> APP_IMAGE_TEMP_ROOT =
             new StandardBundlerParam<>(
             "mac.app.imageRoot",
@@ -113,6 +103,10 @@
         return returnValue;
     }
 
+    public MacBaseInstallerBundler() {
+        appImageBundler = new MacAppBundler().setDependentTask(true);
+    }
+
     protected void validateAppImageAndBundeler(
             Map<String, ? super Object> params) throws ConfigException {
         if (PREDEFINED_APP_IMAGE.fetchFrom(params) != null) {
@@ -134,7 +128,7 @@
                             "message.app-image-requires-app-name.advice"));
             }
         } else {
-            APP_BUNDLER.fetchFrom(params).validate(params);
+            appImageBundler.validate(params);
         }
     }
 
@@ -147,8 +141,7 @@
         }
         File appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
 
-        return APP_BUNDLER.fetchFrom(params).doBundle(
-                params, appImageRoot, true);
+        return appImageBundler.execute(params, appImageRoot);
     }
 
     @Override
@@ -197,4 +190,6 @@
             return null;
         }
     }
+
+    private final Bundler appImageBundler;
 }
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -25,16 +25,27 @@
 
 package jdk.incubator.jpackage.internal;
 
-import java.io.*;
+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.text.MessageFormat;
-import java.util.*;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ResourceBundle;
 import static jdk.incubator.jpackage.internal.MacAppImageBuilder.ICON_ICNS;
 import static jdk.incubator.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
 
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERBOSE;
 
 public class MacDmgBundler extends MacBaseInstallerBundler {
 
@@ -65,9 +76,7 @@
 
         IOUtils.writableOutputDir(outdir.toPath());
 
-        File appImageDir = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
         try {
-            appImageDir.mkdirs();
             File appLocation = prepareAppBundle(params);
 
             if (appLocation != null && prepareConfigFiles(params)) {
@@ -327,6 +336,10 @@
         File mountedRoot = new File(imagesRoot.getAbsolutePath(),
                     APP_NAME.fetchFrom(params));
         try {
+            Files.deleteIfExists(AppImageFile.getPathInAppImage(
+                    mountedRoot.toPath().resolve(APP_NAME.fetchFrom(params)
+                            + ".app")));
+
             // background image
             File bgdir = new File(mountedRoot, BACKGROUND_IMAGE_FOLDER);
             bgdir.mkdirs();
--- a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -407,6 +407,8 @@
                     root,
                     "--install-location",
                     getInstallDir(params),
+                    "--filter",
+                    AppImageFile.getPathInAppImage(Path.of("")).toString(),
                     "--analyze",
                     cpl.getAbsolutePath());
 
@@ -422,6 +424,8 @@
                     root,
                     "--install-location",
                     getInstallDir(params),
+                    "--filter",
+                    AppImageFile.getPathInAppImage(Path.of("")).toString(),
                     "--component-plist",
                     cpl.getAbsolutePath(),
                     "--scripts",
--- a/src/jdk.incubator.jpackage/macosx/native/applauncher/MacLauncher.cpp	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/macosx/native/applauncher/MacLauncher.cpp	Mon Jun 08 09:13:00 2020 -0400
@@ -45,18 +45,16 @@
     const tstring launcherPath = SysInfo::getProcessModulePath();
 
     // Launcher should be in "Contents/MacOS" subdirectory of app image.
-    // However, don't strip "Contents" folder from launcher path, so that app
-    // image root directory would be set to "Contents" subfolder of app image.
-    const tstring appImageRoot = FileUtils::dirname(
-            FileUtils::dirname(launcherPath));
+    const tstring appImageRoot = FileUtils::dirname(FileUtils::dirname(
+            FileUtils::dirname(launcherPath)));
 
     // Create JVM launcher and save in global variable.
     jvmLauncher = AppLauncher()
         .setImageRoot(appImageRoot)
         .addJvmLibName(_T("Contents/Home/lib/libjli.dylib"))
-        .setAppDir(FileUtils::mkpath() << appImageRoot << _T("app"))
+        .setAppDir(FileUtils::mkpath() << appImageRoot << _T("Contents/app"))
         .setDefaultRuntimePath(FileUtils::mkpath() << appImageRoot
-                << _T("runtime"))
+                << _T("Contents/runtime"))
         .createJvmLauncher();
 
     // Kick start JVM launching. The function wouldn't return!
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java	Mon Jun 08 09:13:00 2020 -0400
@@ -28,19 +28,14 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.PrintStream;
-import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.ResourceBundle;
 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.ICON;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.SOURCE_DIR;
 import jdk.incubator.jpackage.internal.resources.ResourceLocator;
 
-
 /*
  * AbstractAppImageBuilder
  *     This is sub-classed by each of the platform dependent AppImageBuilder
@@ -49,13 +44,12 @@
 
 public abstract class AbstractAppImageBuilder {
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.MainResources");
+    private final Path root;
+    protected final ApplicationLayout appLayout;
 
-    private final Path root;
-
-    public AbstractAppImageBuilder(Map<String, Object> unused, Path root) {
+    public AbstractAppImageBuilder(Path root) {
         this.root = root;
+        appLayout = ApplicationLayout.platformAppImage().resolveAt(root);
     }
 
     public InputStream getResourceAsStream(String name) {
@@ -64,121 +58,24 @@
 
     public abstract void prepareApplicationFiles(
             Map<String, ? super Object> params) throws IOException;
-    public abstract void prepareJreFiles(
-            Map<String, ? super Object> params) throws IOException;
-    public abstract Path getAppDir();
-    public abstract Path getAppModsDir();
 
-    public Path getRuntimeRoot() {
-        return this.root;
+    protected void writeCfgFile(Map<String, ? super Object> params) throws
+            IOException {
+        new CfgFile().initFromParams(params).create(root);
     }
 
-    protected void copyEntry(Path appDir, File srcdir, String fname)
-            throws IOException {
-        Path dest = appDir.resolve(fname);
-        Files.createDirectories(dest.getParent());
-        File src = new File(srcdir, fname);
-        if (src.isDirectory()) {
-            IOUtils.copyRecursive(src.toPath(), dest);
-        } else {
-            Files.copy(src.toPath(), dest);
-        }
-    }
-
-    public void writeCfgFile(Map<String, ? super Object> params,
-            File cfgFileName) throws IOException {
-        cfgFileName.getParentFile().mkdirs();
-        cfgFileName.delete();
-
-        LauncherData launcherData = StandardBundlerParam.LAUNCHER_DATA.fetchFrom(
-                params);
-
-        try (PrintStream out = new PrintStream(cfgFileName)) {
-
-            out.println("[Application]");
-            out.println("app.name=" + APP_NAME.fetchFrom(params));
-            out.println("app.version=" + VERSION.fetchFrom(params));
-            out.println("app.runtime=" + getCfgRuntimeDir());
-
-            for (var path : launcherData.classPath()) {
-                out.println("app.classpath=" + getCfgAppDir()
-                        + path.toString().replace("\\", "/"));
-            }
-
-            // The main app is required to be a jar, modular or unnamed.
-            if (launcherData.isModular()) {
-                out.println("app.mainmodule=" + launcherData.moduleName() + "/"
-                        + launcherData.qualifiedClassName());
-            } else {
-                // If the app is contained in an unnamed jar then launch it the
-                // legacy way and the main class string must be
-                // of the format com/foo/Main
-                if (launcherData.mainJarName() != null) {
-                    out.println("app.classpath=" + getCfgAppDir()
-                            + launcherData.mainJarName().toString());
-                }
-
-                out.println("app.mainclass=" + launcherData.qualifiedClassName());
-            }
-
-            out.println();
-            out.println("[JavaOptions]");
-            List<String> jvmargs = JAVA_OPTIONS.fetchFrom(params);
-            for (String arg : jvmargs) {
-                out.println("java-options=" + arg);
-            }
-            Path modsDir = getAppModsDir();
-
-            if (modsDir != null && modsDir.toFile().exists()) {
-                out.println("java-options=" + "--module-path");
-                out.println("java-options=" + getCfgAppDir().replace("\\","/") + "mods");
-            }
-
-            out.println();
-            out.println("[ArgOptions]");
-            List<String> args = ARGUMENTS.fetchFrom(params);
-            for (String arg : args) {
-                out.println("arguments=" + arg);
-            }
-        }
+    ApplicationLayout getAppLayout() {
+        return appLayout;
     }
 
     protected void copyApplication(Map<String, ? super Object> params)
             throws IOException {
-        Path inputPath = StandardBundlerParam.SOURCE_DIR.fetchFrom(params);
+        Path inputPath = SOURCE_DIR.fetchFrom(params);
         if (inputPath != null) {
-            IOUtils.copyRecursive(SOURCE_DIR.fetchFrom(params), getAppDir());
+            IOUtils.copyRecursive(SOURCE_DIR.fetchFrom(params),
+                    appLayout.appDirectory());
         }
-    }
-
-    File getRuntimeImageDir(File runtimeImageTop) {
-        return runtimeImageTop;
-    }
-
-    protected String getCfgAppDir() {
-        return "$ROOTDIR" + File.separator
-                + getAppDir().getFileName() + File.separator;
-    }
-
-    protected String getCfgRuntimeDir() {
-        return "$ROOTDIR" + File.separator + "runtime";
-    }
-
-    String getCfgClassPath(String classpath) {
-        String cfgAppDir = getCfgAppDir();
-
-        StringBuilder sb = new StringBuilder();
-        for (String path : classpath.split("[:;]")) {
-            if (path.length() > 0) {
-                sb.append(cfgAppDir);
-                sb.append(path);
-                sb.append(File.pathSeparator);
-            }
-        }
-        if (sb.length() > 0) {
-            sb.deleteCharAt(sb.length() - 1);
-        }
-        return sb.toString();
+        AppImageFile.save(root, params);
     }
 
     public static OverridableResource createIconResource(String defaultIconName,
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.jpackage.internal;
-
-import java.text.MessageFormat;
-import java.util.Map;
-import java.io.File;
-
-import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
-
-/**
- * AbstractImageBundler
- *
- * This is the base class for each of the Application Image Bundlers.
- *
- * It contains methods and parameters common to all Image Bundlers.
- *
- * Application Image Bundlers are created in "create-app-image" mode,
- * or as an intermediate step in "create-installer" mode.
- *
- * The concrete implementations are in the platform specific Bundlers.
- */
-public abstract class AbstractImageBundler extends AbstractBundler {
-
-    protected void imageBundleValidation(Map<String, ? super Object> params)
-             throws ConfigException {
-        if (!params.containsKey(PREDEFINED_APP_IMAGE.getID())
-                && !StandardBundlerParam.isRuntimeInstaller(params)) {
-            StandardBundlerParam.LAUNCHER_DATA.fetchFrom(params);
-        }
-    }
-
-    protected File createRoot(Map<String, ? super Object> params,
-            File outputDirectory, boolean dependentTask, String name)
-            throws PackagerException {
-
-        IOUtils.writableOutputDir(outputDirectory.toPath());
-
-        if (!dependentTask) {
-            Log.verbose(MessageFormat.format(
-                    I18N.getString("message.creating-app-bundle"),
-                    name, outputDirectory.getAbsolutePath()));
-        }
-
-        // Create directory structure
-        File rootDirectory = new File(outputDirectory, name);
-
-        if (rootDirectory.exists()) {
-            throw new PackagerException("error.root-exists",
-                    rootDirectory.getAbsolutePath());
-        }
-
-        rootDirectory.mkdirs();
-
-        return rootDirectory;
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.jpackage.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE;
+
+
+class AppImageBundler extends AbstractBundler {
+
+    @Override
+    final public String getName() {
+        return I18N.getString("app.bundler.name");
+    }
+
+    @Override
+    final public String getID() {
+        return "app";
+    }
+
+    @Override
+    final public String getBundleType() {
+        return "IMAGE";
+    }
+
+    @Override
+    final public boolean validate(Map<String, ? super Object> params)
+            throws ConfigException {
+        try {
+            Objects.requireNonNull(params);
+
+            if (!params.containsKey(PREDEFINED_APP_IMAGE.getID())
+                    && !StandardBundlerParam.isRuntimeInstaller(params)) {
+                StandardBundlerParam.LAUNCHER_DATA.fetchFrom(params);
+            }
+
+            if (paramsValidator != null) {
+                paramsValidator.validate(params);
+            }
+        } catch (RuntimeException re) {
+            if (re.getCause() instanceof ConfigException) {
+                throw (ConfigException) re.getCause();
+            } else {
+                throw new ConfigException(re);
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    final public File execute(Map<String, ? super Object> params,
+            File outputParentDir) throws PackagerException {
+        if (StandardBundlerParam.isRuntimeInstaller(params)) {
+            return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
+        }
+
+        try {
+            return createAppBundle(params, outputParentDir.toPath()).toFile();
+        } catch (PackagerException pe) {
+            throw pe;
+        } catch (RuntimeException|IOException|ConfigException ex) {
+            Log.verbose(ex);
+            throw new PackagerException(ex);
+        }
+    }
+
+    @Override
+    final public boolean supported(boolean runtimeInstaller) {
+        return true;
+    }
+
+    @Override
+    final public boolean isDefault() {
+        return false;
+    }
+
+    final AppImageBundler setDependentTask(boolean v) {
+        dependentTask = v;
+        return this;
+    }
+
+    final AppImageBundler setAppImageSupplier(
+            Function<Path, AbstractAppImageBuilder> v) {
+        appImageSupplier = v;
+        return this;
+    }
+
+    final AppImageBundler setParamsValidator(ParamsValidator v) {
+        paramsValidator = v;
+        return this;
+    }
+
+    @FunctionalInterface
+    interface ParamsValidator {
+        void validate(Map<String, ? super Object> params) throws ConfigException;
+    }
+
+    private Path createRoot(Map<String, ? super Object> params,
+            Path outputDirectory) throws PackagerException, IOException {
+
+        IOUtils.writableOutputDir(outputDirectory);
+
+        String imageName = StandardBundlerParam.APP_NAME.fetchFrom(params);
+        if (Platform.isMac()) {
+            imageName = imageName + ".app";
+        }
+
+        if (!dependentTask) {
+            Log.verbose(MessageFormat.format(
+                    I18N.getString("message.creating-app-bundle"),
+                    imageName, outputDirectory.toAbsolutePath()));
+        }
+
+        // Create directory structure
+        Path rootDirectory = outputDirectory.resolve(imageName);
+        if (Files.exists(rootDirectory)) {
+            throw new PackagerException("error.root-exists",
+                    rootDirectory.toAbsolutePath().toString());
+        }
+
+        Files.createDirectories(rootDirectory);
+
+        return rootDirectory;
+    }
+
+    private Path createAppBundle(Map<String, ? super Object> params,
+            Path outputDirectory) throws PackagerException, IOException,
+            ConfigException {
+
+        Path rootDirectory = createRoot(params, outputDirectory);
+        AbstractAppImageBuilder appBuilder = appImageSupplier.apply(rootDirectory);
+        if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) {
+            JLinkBundlerHelper.execute(params,
+                    appBuilder.getAppLayout().runtimeHomeDirectory());
+        } else {
+            StandardBundlerParam.copyPredefinedRuntimeImage(
+                    params, appBuilder.getAppLayout());
+        }
+        appBuilder.prepareApplicationFiles(params);
+        return rootDirectory;
+    }
+
+    private boolean dependentTask;
+    private ParamsValidator paramsValidator;
+    private Function<Path, AbstractAppImageBuilder> appImageSupplier;
+}
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -110,6 +110,10 @@
             xml.writeAttribute("version", getVersion());
             xml.writeAttribute("platform", getPlatform());
 
+            xml.writeStartElement("app-version");
+            xml.writeCharacters(VERSION.fetchFrom(params));
+            xml.writeEndElement();
+
             xml.writeStartElement("main-launcher");
             xml.writeCharacters(APP_NAME.fetchFrom(params));
             xml.writeEndElement();
@@ -134,14 +138,7 @@
      */
     static AppImageFile load(Path appImageDir) throws IOException {
         try {
-            Path path = getPathInAppImage(appImageDir);
-            DocumentBuilderFactory dbf =
-                    DocumentBuilderFactory.newDefaultInstance();
-            dbf.setFeature(
-                   "http://apache.org/xml/features/nonvalidating/load-external-dtd",
-                    false);
-            DocumentBuilder b = dbf.newDocumentBuilder();
-            Document doc = b.parse(new FileInputStream(path.toFile()));
+            Document doc = readXml(appImageDir);
 
             XPath xPath = XPathFactory.newInstance().newXPath();
 
@@ -174,12 +171,26 @@
                 file = new AppImageFile();
             }
             return file;
+        } catch (XPathExpressionException ex) {
+            // This should never happen as XPath expressions should be correct
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public static Document readXml(Path appImageDir) throws IOException {
+        try {
+            Path path = getPathInAppImage(appImageDir);
+
+            DocumentBuilderFactory dbf =
+                    DocumentBuilderFactory.newDefaultInstance();
+            dbf.setFeature(
+                   "http://apache.org/xml/features/nonvalidating/load-external-dtd",
+                    false);
+            DocumentBuilder b = dbf.newDocumentBuilder();
+            return b.parse(new FileInputStream(path.toFile()));
         } catch (ParserConfigurationException | SAXException ex) {
             // Let caller sort this out
             throw new IOException(ex);
-        } catch (XPathExpressionException ex) {
-            // This should never happen as XPath expressions should be correct
-            throw new RuntimeException(ex);
         }
     }
 
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -33,7 +33,35 @@
  */
 public final class ApplicationLayout implements PathGroup.Facade<ApplicationLayout> {
     enum PathRole {
-        RUNTIME, APP, LAUNCHERS, DESKTOP, APP_MODS, DLLS, RELEASE
+        /**
+         * Java run-time directory.
+         */
+        RUNTIME,
+
+        /**
+         * Java run-time home directory.
+         */
+        RUNTIME_HOME,
+
+        /**
+         * Application data directory.
+         */
+        APP,
+
+        /**
+         * Directory with application launchers.
+         */
+        LAUNCHERS,
+
+        /**
+         * Directory for files for desktop integration.
+         */
+        DESKTOP,
+
+        /**
+         * Directory with application Java modules.
+         */
+        MODULES,
     }
 
     ApplicationLayout(Map<Object, Path> paths) {
@@ -62,13 +90,6 @@
     }
 
     /**
-     * Path to directory with dynamic libraries.
-     */
-    public Path dllDirectory() {
-        return pathGroup().getPath(PathRole.DLLS);
-    }
-
-    /**
      * Path to application data directory.
      */
     public Path appDirectory() {
@@ -76,17 +97,24 @@
     }
 
     /**
-     * Path to Java runtime directory.
+     * Path to Java run-time directory.
      */
     public Path runtimeDirectory() {
         return pathGroup().getPath(PathRole.RUNTIME);
     }
 
     /**
+     * Path to Java run-time home directory.
+     */
+    public Path runtimeHomeDirectory() {
+        return pathGroup().getPath(PathRole.RUNTIME_HOME);
+    }
+
+    /**
      * Path to application mods directory.
      */
     public Path appModsDirectory() {
-        return pathGroup().getPath(PathRole.APP_MODS);
+        return pathGroup().getPath(PathRole.MODULES);
     }
 
     /**
@@ -96,22 +124,14 @@
         return pathGroup().getPath(PathRole.DESKTOP);
     }
 
-    /**
-     * Path to release file in the Java runtime directory.
-     */
-    public Path runtimeRelease() {
-        return pathGroup().getPath(PathRole.RELEASE);
-    }
-
     static ApplicationLayout linuxAppImage() {
         return new ApplicationLayout(Map.of(
                 PathRole.LAUNCHERS, Path.of("bin"),
                 PathRole.APP, Path.of("lib/app"),
                 PathRole.RUNTIME, Path.of("lib/runtime"),
+                PathRole.RUNTIME_HOME, Path.of("lib/runtime"),
                 PathRole.DESKTOP, Path.of("lib"),
-                PathRole.DLLS, Path.of("lib"),
-                PathRole.APP_MODS, Path.of("lib/app/mods"),
-                PathRole.RELEASE, Path.of("lib/runtime/release")
+                PathRole.MODULES, Path.of("lib/app/mods")
         ));
     }
 
@@ -120,10 +140,9 @@
                 PathRole.LAUNCHERS, Path.of(""),
                 PathRole.APP, Path.of("app"),
                 PathRole.RUNTIME, Path.of("runtime"),
+                PathRole.RUNTIME_HOME, Path.of("runtime"),
                 PathRole.DESKTOP, Path.of(""),
-                PathRole.DLLS, Path.of(""),
-                PathRole.APP_MODS, Path.of("app/mods"),
-                PathRole.RELEASE, Path.of("runtime/release")
+                PathRole.MODULES, Path.of("app/mods")
         ));
     }
 
@@ -132,10 +151,9 @@
                 PathRole.LAUNCHERS, Path.of("Contents/MacOS"),
                 PathRole.APP, Path.of("Contents/app"),
                 PathRole.RUNTIME, Path.of("Contents/runtime"),
+                PathRole.RUNTIME_HOME, Path.of("Contents/runtime/Contents/Home"),
                 PathRole.DESKTOP, Path.of("Contents/Resources"),
-                PathRole.DLLS, Path.of("Contents/MacOS"),
-                PathRole.APP_MODS, Path.of("Contents/app/mods"),
-                PathRole.RELEASE, Path.of("Contents/runtime/Contents/Home/release")
+                PathRole.MODULES, Path.of("Contents/app/mods")
         ));
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CfgFile.java	Mon Jun 08 09:13:00 2020 -0400
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.incubator.jpackage.internal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+
+/**
+ * App launcher's config file.
+ */
+final class CfgFile {
+    CfgFile() {
+        appLayout = ApplicationLayout.platformAppImage();
+    }
+
+    CfgFile initFromParams(Map<String, ? super Object> params) {
+        launcherData = StandardBundlerParam.LAUNCHER_DATA.fetchFrom(params);
+        launcherName = StandardBundlerParam.APP_NAME.fetchFrom(params);
+        javaOptions = JAVA_OPTIONS.fetchFrom(params);
+        arguments = ARGUMENTS.fetchFrom(params);
+        return this;
+    }
+
+    void create(Path appImage) throws IOException {
+        List<Map.Entry<String, Object>> content = new ArrayList<>();
+
+        ApplicationLayout appCfgLayout = createAppCfgLayout();
+
+        content.add(Map.entry("[Application]", SECTION_TAG));
+        content.add(Map.entry("app.runtime", appCfgLayout.runtimeDirectory()));
+
+        if (launcherData.isModular()) {
+            content.add(Map.entry("app.mainmodule", launcherData.moduleName()
+                    + "/" + launcherData.qualifiedClassName()));
+        } else {
+            // If the app is contained in an unnamed jar then launch it the
+            // legacy way and the main class string must be
+            // of the format com/foo/Main
+            if (launcherData.mainJarName() != null) {
+                content.add(Map.entry("app.classpath",
+                        appCfgLayout.appDirectory().resolve(
+                                launcherData.mainJarName())));
+            }
+            content.add(Map.entry("app.mainclass",
+                    launcherData.qualifiedClassName()));
+        }
+
+        for (var value : launcherData.classPath()) {
+            content.add(Map.entry("app.classpath",
+                    appCfgLayout.appDirectory().resolve(value).toString()));
+        }
+
+        ApplicationLayout appImagelayout = appLayout.resolveAt(appImage);
+        Path modsDir = appImagelayout.appModsDirectory();
+        if (!javaOptions.isEmpty() || Files.isDirectory(modsDir)) {
+            content.add(Map.entry("[JavaOptions]", SECTION_TAG));
+            for (var value : javaOptions) {
+                content.add(Map.entry("java-options", value));
+            }
+            content.add(Map.entry("java-options", "--module-path"));
+            content.add(Map.entry("java-options",
+                    appCfgLayout.appModsDirectory()));
+        }
+
+        if (!arguments.isEmpty()) {
+            content.add(Map.entry("[ArgOptions]", SECTION_TAG));
+            for (var value : arguments) {
+                content.add(Map.entry("arguments", value));
+            }
+        }
+
+        Path cfgFile = appImagelayout.appDirectory().resolve(launcherName + ".cfg");
+        Files.createDirectories(cfgFile.getParent());
+
+        boolean[] addLineBreakAtSection = new boolean[1];
+        Stream<String> lines = content.stream().map(entry -> {
+            if (entry.getValue() == SECTION_TAG) {
+                if (!addLineBreakAtSection[0]) {
+                    addLineBreakAtSection[0] = true;
+                    return entry.getKey();
+                }
+                return "\n" + entry.getKey();
+            }
+            return entry.getKey() + "=" + entry.getValue();
+        });
+        Files.write(cfgFile, (Iterable<String>) lines::iterator);
+    }
+
+    private ApplicationLayout createAppCfgLayout() {
+        ApplicationLayout appCfgLayout = appLayout.resolveAt(Path.of("$ROOTDIR"));
+        appCfgLayout.pathGroup().setPath(ApplicationLayout.PathRole.APP,
+                Path.of("$APPDIR"));
+        appCfgLayout.pathGroup().setPath(ApplicationLayout.PathRole.MODULES,
+                appCfgLayout.appDirectory().resolve(appCfgLayout.appModsDirectory().getFileName()));
+        return appCfgLayout;
+    }
+
+    private String launcherName;
+    private LauncherData launcherData;
+    List<String> arguments;
+    List<String> javaOptions;
+    private final ApplicationLayout appLayout;
+
+    private final static Object SECTION_TAG = new Object();
+}
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java	Mon Jun 08 09:13:00 2020 -0400
@@ -29,37 +29,32 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.module.Configuration;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.lang.module.ResolvedModule;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.ResourceBundle;
 import java.util.Set;
+import java.util.function.Supplier;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.spi.ToolProvider;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import java.util.regex.Matcher;
-import java.util.spi.ToolProvider;
-import java.util.jar.JarFile;
-import java.lang.module.Configuration;
-import java.lang.module.ResolvedModule;
-import java.lang.module.ModuleDescriptor;
-import java.lang.module.ModuleFinder;
-import java.lang.module.ModuleReference;
 import jdk.internal.module.ModulePath;
 
 
 final class JLinkBundlerHelper {
 
-    private static final ToolProvider JLINK_TOOL =
-            ToolProvider.findFirst("jlink").orElseThrow();
-
-    static void execute(Map<String, ? super Object> params,
-            AbstractAppImageBuilder imageBuilder)
-            throws IOException, Exception {
+    static void execute(Map<String, ? super Object> params, Path outputDir)
+            throws IOException, PackagerException {
 
         List<Path> modulePath =
                 StandardBundlerParam.MODULE_PATH.fetchFrom(params);
@@ -69,7 +64,6 @@
                 StandardBundlerParam.LIMIT_MODULES.fetchFrom(params);
         List<String> options =
                 StandardBundlerParam.JLINK_OPTIONS.fetchFrom(params);
-        Path outputDir = imageBuilder.getRuntimeRoot();
 
         LauncherData launcherData = StandardBundlerParam.LAUNCHER_DATA.fetchFrom(
                 params);
@@ -79,11 +73,10 @@
 
         // Modules
         if (!launcherData.isModular() && addModules.isEmpty()) {
-            addModules.add(ModuleHelper.ALL_DEFAULT);
+            addModules.add(ALL_DEFAULT);
         }
 
-        Set<String> modules = new ModuleHelper(
-                modulePath, addModules, limitModules).modules();
+        Set<String> modules = createModuleList(modulePath, addModules, limitModules);
 
         if (launcherData.isModular()) {
             modules.add(launcherData.moduleName());
@@ -91,8 +84,6 @@
 
         runJLink(outputDir, modulePath, modules, limitModules,
                 options, bindServices);
-
-        imageBuilder.prepareApplicationFiles(params);
     }
 
     /*
@@ -137,72 +128,41 @@
                 ModuleFinder.ofSystem());
     }
 
-    private static class ModuleHelper {
-        // The token for "all modules on the module path".
-        private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
+    private static Set<String> createModuleList(List<Path> paths,
+            Set<String> addModules, Set<String> limitModules) {
 
-        // The token for "all valid runtime modules".
-        static final String ALL_DEFAULT = "ALL-DEFAULT";
+        final Set<String> modules = new HashSet<>();
 
-        private final Set<String> modules = new HashSet<>();
-        ModuleHelper(List<Path> paths, Set<String> addModules,
-                Set<String> limitModules) {
-            boolean addAllModulePath = false;
-            boolean addDefaultMods = false;
+        final Map<String, Supplier<Collection<String>>> phonyModules = Map.of(
+                ALL_MODULE_PATH,
+                () -> createModuleFinder(paths)
+                            .findAll()
+                            .stream()
+                            .map(ModuleReference::descriptor)
+                            .map(ModuleDescriptor::name)
+                            .collect(Collectors.toSet()),
+                ALL_DEFAULT,
+                () -> getDefaultModules(paths, modules));
 
-            for (Iterator<String> iterator = addModules.iterator();
-                    iterator.hasNext();) {
-                String module = iterator.next();
-
-                switch (module) {
-                    case ALL_MODULE_PATH:
-                        iterator.remove();
-                        addAllModulePath = true;
-                        break;
-                    case ALL_DEFAULT:
-                        iterator.remove();
-                        addDefaultMods = true;
-                        break;
-                    default:
-                        this.modules.add(module);
-                }
-            }
-
-            if (addAllModulePath) {
-                this.modules.addAll(getModuleNamesFromPath(paths));
-            } else if (addDefaultMods) {
-                this.modules.addAll(getDefaultModules(
-                        paths, addModules));
+        Supplier<Collection<String>> phonyModule = null;
+        for (var module : addModules) {
+            phonyModule = phonyModules.get(module);
+            if (phonyModule == null) {
+                modules.add(module);
             }
         }
 
-        Set<String> modules() {
-            return modules;
+        if (phonyModule != null) {
+            modules.addAll(phonyModule.get());
         }
 
-        private static Set<String> getModuleNamesFromPath(List<Path> paths) {
-
-            return createModuleFinder(paths)
-                    .findAll()
-                    .stream()
-                    .map(ModuleReference::descriptor)
-                    .map(ModuleDescriptor::name)
-                    .collect(Collectors.toSet());
-        }
+        return modules;
     }
 
     private static void runJLink(Path output, List<Path> modulePath,
             Set<String> modules, Set<String> limitModules,
             List<String> options, boolean bindServices)
-            throws PackagerException {
-
-        // This is just to ensure jlink is given a non-existant directory
-        // The passed in output path should be non-existant or empty directory
-        try {
-            IOUtils.deleteRecursive(output.toFile());
-        } catch (IOException ioe) {
-            throw new PackagerException(ioe);
-        }
+            throws PackagerException, IOException {
 
         ArrayList<String> args = new ArrayList<String>();
         args.add("--output");
@@ -237,14 +197,14 @@
         PrintWriter pw = new PrintWriter(writer);
 
         Log.verbose("jlink arguments: " + args);
-        int retVal = JLINK_TOOL.run(pw, pw, args.toArray(new String[0]));
+        int retVal = LazyLoad.JLINK_TOOL.run(pw, pw, args.toArray(new String[0]));
         String jlinkOut = writer.toString();
 
         if (retVal != 0) {
             throw new PackagerException("error.jlink.failed" , jlinkOut);
-        } else if (jlinkOut.length() > 0) {
-            Log.verbose("jlink output: " + jlinkOut);
         }
+
+        Log.verbose("jlink output: " + jlinkOut);
     }
 
     private static String getPathList(List<Path> pathList) {
@@ -258,4 +218,15 @@
         return Matcher.quoteReplacement(strings.stream().collect(
                 Collectors.joining(",")));
     }
+
+    // The token for "all modules on the module path".
+    private final static String ALL_MODULE_PATH = "ALL-MODULE-PATH";
+
+    // The token for "all valid runtime modules".
+    private final static String ALL_DEFAULT = "ALL-DEFAULT";
+
+    private static class LazyLoad {
+        static final ToolProvider JLINK_TOOL = ToolProvider.findFirst(
+                "jlink").orElseThrow();
+    };
 }
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java	Mon Jun 08 09:13:00 2020 -0400
@@ -470,10 +470,8 @@
         return applicationImage;
     }
 
-    static void copyPredefinedRuntimeImage(
-            Map<String, ? super Object> params,
-            AbstractAppImageBuilder appBuilder)
-            throws IOException , ConfigException {
+    static void copyPredefinedRuntimeImage(Map<String, ? super Object> params,
+            ApplicationLayout appLayout) throws IOException, ConfigException {
         File topImage = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
         if (!topImage.exists()) {
             throw new ConfigException(
@@ -485,16 +483,17 @@
                     "message.runtime-image-dir-does-not-exist.advice"),
                     PREDEFINED_RUNTIME_IMAGE.getID()));
         }
-        File image = appBuilder.getRuntimeImageDir(topImage);
+
         // copy whole runtime, need to skip jmods and src.zip
         final List<String> excludes = Arrays.asList("jmods", "src.zip");
-        IOUtils.copyRecursive(image.toPath(), appBuilder.getRuntimeRoot(), excludes);
+        IOUtils.copyRecursive(topImage.toPath(),
+                appLayout.runtimeHomeDirectory(), excludes);
 
         // if module-path given - copy modules to appDir/mods
         List<Path> modulePath =
                 StandardBundlerParam.MODULE_PATH.fetchFrom(params);
         List<Path> defaultModulePath = getDefaultModulePath();
-        Path dest = appBuilder.getAppModsDir();
+        Path dest = appLayout.appModsDirectory();
 
         if (dest != null) {
             for (Path mp : modulePath) {
@@ -504,8 +503,6 @@
                 }
             }
         }
-
-        appBuilder.prepareApplicationFiles(params);
     }
 
     private static List<Path> getDefaultModulePath() {
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,111 +25,8 @@
 
 package jdk.incubator.jpackage.internal;
 
-import java.io.File;
-import java.nio.file.Path;
-import java.text.MessageFormat;
-import java.util.*;
-
-import static jdk.incubator.jpackage.internal.WindowsBundlerParam.*;
-
-public class WinAppBundler extends AbstractImageBundler {
-
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.WinResources");
-
-    @Override
-    public boolean validate(Map<String, ? super Object> params)
-            throws ConfigException {
-        try {
-            Objects.requireNonNull(params);
-            return doValidate(params);
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
-            }
-        }
+public class WinAppBundler extends AppImageBundler {
+    public WinAppBundler() {
+        setAppImageSupplier(WindowsAppImageBuilder::new);
     }
-
-    // to be used by chained bundlers, e.g. by EXE bundler to avoid
-    // skipping validation if p.type does not include "image"
-    private boolean doValidate(Map<String, ? super Object> p)
-            throws ConfigException {
-
-        imageBundleValidation(p);
-        return true;
-    }
-
-    public boolean bundle(Map<String, ? super Object> p, File outputDirectory)
-            throws PackagerException {
-        return doBundle(p, outputDirectory, false) != null;
-    }
-
-    File doBundle(Map<String, ? super Object> p, File outputDirectory,
-            boolean dependentTask) throws PackagerException {
-        if (StandardBundlerParam.isRuntimeInstaller(p)) {
-            return PREDEFINED_RUNTIME_IMAGE.fetchFrom(p);
-        } else {
-            return doAppBundle(p, outputDirectory, dependentTask);
-        }
-    }
-
-    File doAppBundle(Map<String, ? super Object> p, File outputDirectory,
-            boolean dependentTask) throws PackagerException {
-        try {
-            File rootDirectory = createRoot(p, outputDirectory, dependentTask,
-                    APP_NAME.fetchFrom(p));
-            AbstractAppImageBuilder appBuilder =
-                    new WindowsAppImageBuilder(p, outputDirectory.toPath());
-            if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ) {
-                JLinkBundlerHelper.execute(p, appBuilder);
-            } else {
-                StandardBundlerParam.copyPredefinedRuntimeImage(p, appBuilder);
-            }
-            if (!dependentTask) {
-                Log.verbose(MessageFormat.format(
-                        I18N.getString("message.result-dir"),
-                        outputDirectory.getAbsolutePath()));
-            }
-            return rootDirectory;
-        } catch (PackagerException pe) {
-            throw pe;
-        } catch (Exception e) {
-            Log.verbose(e);
-            throw new PackagerException(e);
-        }
-    }
-
-    @Override
-    public String getName() {
-        return I18N.getString("app.bundler.name");
-    }
-
-    @Override
-    public String getID() {
-        return "windows.app";
-    }
-
-    @Override
-    public String getBundleType() {
-        return "IMAGE";
-    }
-
-    @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return doBundle(params, outputParentDir, false);
-    }
-
-    @Override
-    public boolean supported(boolean platformInstaller) {
-        return true;
-    }
-
-    @Override
-    public boolean isDefault() {
-        return false;
-    }
-
 }
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -37,18 +37,8 @@
         System.loadLibrary("jpackage");
     }
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.WinResources");
-
-    public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER
-            = new WindowsBundlerParam<>(
-                    "win.app.bundler",
-                    WinAppBundler.class,
-                    params -> new WinAppBundler(),
-                    null);
-
     public static final BundlerParamInfo<File> EXE_IMAGE_DIR
-            = new WindowsBundlerParam<>(
+            = new StandardBundlerParam<>(
                     "win.exe.imageDir",
                     File.class,
                     params -> {
@@ -107,7 +97,7 @@
         File exeImageDir = EXE_IMAGE_DIR.fetchFrom(params);
 
         // Write msi to temporary directory.
-        File msi = msiBundler.bundle(params, exeImageDir);
+        File msi = msiBundler.execute(params, exeImageDir);
 
         try {
             new ScriptRunner()
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java	Mon Jun 08 09:13:00 2020 -0400
@@ -54,8 +54,6 @@
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR;
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
-import static jdk.incubator.jpackage.internal.WindowsBundlerParam.INSTALLDIR_CHOOSER;
-import static jdk.incubator.jpackage.internal.WindowsBundlerParam.INSTALLER_FILE_NAME;
 
 /**
  * WinMsiBundler
@@ -105,15 +103,8 @@
  */
 public class WinMsiBundler  extends AbstractBundler {
 
-    public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
-            new WindowsBundlerParam<>(
-            "win.app.bundler",
-            WinAppBundler.class,
-            params -> new WinAppBundler(),
-            null);
-
     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
-            new WindowsBundlerParam<>(
+            new StandardBundlerParam<>(
             "win.msi.imageDir",
             File.class,
             params -> {
@@ -124,7 +115,7 @@
             (s, p) -> null);
 
     public static final BundlerParamInfo<File> WIN_APP_IMAGE =
-            new WindowsBundlerParam<>(
+            new StandardBundlerParam<>(
             "win.app.image",
             File.class,
             null,
@@ -151,12 +142,41 @@
             );
 
     private static final BundlerParamInfo<String> UPGRADE_UUID =
-            new WindowsBundlerParam<>(
+            new StandardBundlerParam<>(
             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
             String.class,
             null,
             (s, p) -> s);
 
+    private static final BundlerParamInfo<String> INSTALLER_FILE_NAME =
+            new StandardBundlerParam<> (
+            "win.installerName",
+            String.class,
+            params -> {
+                String nm = APP_NAME.fetchFrom(params);
+                if (nm == null) return null;
+
+                String version = VERSION.fetchFrom(params);
+                if (version == null) {
+                    return nm;
+                } else {
+                    return nm + "-" + version;
+                }
+            },
+            (s, p) -> s);
+
+    private static final BundlerParamInfo<Boolean> INSTALLDIR_CHOOSER =
+            new StandardBundlerParam<> (
+            Arguments.CLIOptions.WIN_DIR_CHOOSER.getId(),
+            Boolean.class,
+            params -> Boolean.FALSE,
+            (s, p) -> Boolean.valueOf(s)
+    );
+
+    public WinMsiBundler() {
+        appImageBundler = new WinAppBundler().setDependentTask(true);
+    }
+
     @Override
     public String getName() {
         return I18N.getString("msi.bundler.name");
@@ -173,12 +193,6 @@
     }
 
     @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return bundle(params, outputParentDir);
-    }
-
-    @Override
     public boolean supported(boolean platformInstaller) {
         try {
             if (wixToolset == null) {
@@ -226,7 +240,7 @@
     public boolean validate(Map<String, ? super Object> params)
             throws ConfigException {
         try {
-            APP_BUNDLER.fetchFrom(params).validate(params);
+            appImageBundler.validate(params);
 
             if (wixToolset == null) {
                 wixToolset = WixTool.toolset();
@@ -282,8 +296,8 @@
             // copy everything from appImage dir into appDir/name
             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
         } else {
-            appDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
-                    MSI_IMAGE_DIR.fetchFrom(params), true);
+            appDir = appImageBundler.execute(params, MSI_IMAGE_DIR.fetchFrom(
+                    params));
         }
 
         // Configure installer icon
@@ -317,10 +331,11 @@
         }
     }
 
-    public File bundle(Map<String, ? super Object> params, File outdir)
-            throws PackagerException {
+    @Override
+    public File execute(Map<String, ? super Object> params,
+            File outputParentDir) throws PackagerException {
 
-        IOUtils.writableOutputDir(outdir.toPath());
+        IOUtils.writableOutputDir(outputParentDir.toPath());
 
         Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath();
         try {
@@ -342,14 +357,14 @@
             .setEnvironmentVariable("JpAppImageDir", imageDir.toAbsolutePath().toString())
             .run(params);
 
-            return buildMSI(params, wixVars, outdir);
+            return buildMSI(params, wixVars, outputParentDir);
         } catch (IOException ex) {
             Log.verbose(ex);
             throw new PackagerException(ex);
         }
     }
 
-    Map<String, String> prepareMainProjectFile(
+    private Map<String, String> prepareMainProjectFile(
             Map<String, ? super Object> params) throws IOException {
         Map<String, String> data = new HashMap<>();
 
@@ -467,7 +482,7 @@
         return msiOut;
     }
 
-    public static void ensureByMutationFileIsRTF(File f) {
+    private static void ensureByMutationFileIsRTF(File f) {
         if (f == null || !f.isFile()) return;
 
         try {
@@ -540,6 +555,7 @@
 
     private Path installerIcon;
     private Map<WixTool, WixTool.ToolInfo> wixToolset;
+    private AppImageBundler appImageBundler;
     private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder();
 
 }
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java	Mon Jun 08 09:13:00 2020 -0400
@@ -49,13 +49,6 @@
 
     private static final String TEMPLATE_APP_ICON ="java48.ico";
 
-    private final Path root;
-    private final Path appDir;
-    private final Path appModsDir;
-    private final Path runtimeDir;
-    private final Path mdir;
-    private final Path binDir;
-
     public static final BundlerParamInfo<File> ICON_ICO =
             new StandardBundlerParam<>(
             "icon.ico",
@@ -72,7 +65,7 @@
             (s, p) -> new File(s));
 
     public static final StandardBundlerParam<Boolean> CONSOLE_HINT =
-            new WindowsBundlerParam<>(
+            new StandardBundlerParam<>(
             Arguments.CLIOptions.WIN_CONSOLE_HINT.getId(),
             Boolean.class,
             params -> false,
@@ -81,21 +74,8 @@
             (s, p) -> (s == null
             || "null".equalsIgnoreCase(s)) ? true : Boolean.valueOf(s));
 
-    public WindowsAppImageBuilder(Map<String, Object> params, Path imageOutDir)
-            throws IOException {
-        super(params,
-                imageOutDir.resolve(APP_NAME.fetchFrom(params) + "/runtime"));
-
-        Objects.requireNonNull(imageOutDir);
-
-        this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params));
-        this.appDir = root.resolve("app");
-        this.appModsDir = appDir.resolve("mods");
-        this.runtimeDir = root.resolve("runtime");
-        this.mdir = runtimeDir.resolve("lib");
-        this.binDir = root;
-        Files.createDirectories(appDir);
-        Files.createDirectories(runtimeDir);
+    WindowsAppImageBuilder(Path imageOutDir) {
+        super(imageOutDir);
     }
 
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
@@ -117,32 +97,9 @@
         }
     }
 
-    public static String getLauncherCfgName(
-            Map<String, ? super Object> params) {
-        return "app/" + APP_NAME.fetchFrom(params) +".cfg";
-    }
-
-    @Override
-    public Path getAppDir() {
-        return appDir;
-    }
-
-    @Override
-    public Path getAppModsDir() {
-        return appModsDir;
-    }
-
     @Override
     public void prepareApplicationFiles(Map<String, ? super Object> params)
             throws IOException {
-        try {
-            IOUtils.writableOutputDir(root);
-            IOUtils.writableOutputDir(binDir);
-        } catch (PackagerException pe) {
-            throw new RuntimeException(pe);
-        }
-        AppImageFile.save(root, params);
-
         // create the .exe launchers
         createLauncherForEntryPoint(params, null);
 
@@ -158,10 +115,6 @@
         }
     }
 
-    @Override
-    public void prepareJreFiles(Map<String, ? super Object> params)
-        throws IOException {}
-
     private void createLauncherForEntryPoint(Map<String, ? super Object> params,
             Map<String, ? super Object> mainParams) throws IOException {
 
@@ -169,17 +122,18 @@
                 mainParams);
         Path iconTarget = null;
         if (iconResource != null) {
-            iconTarget = binDir.resolve(APP_NAME.fetchFrom(params) + ".ico");
+            iconTarget = appLayout.destktopIntegrationDirectory().resolve(
+                    APP_NAME.fetchFrom(params) + ".ico");
             if (null == iconResource.saveToFile(iconTarget)) {
                 iconTarget = null;
             }
         }
 
-        writeCfgFile(params, root.resolve(
-                getLauncherCfgName(params)).toFile());
+        writeCfgFile(params);
 
         // Copy executable to bin folder
-        Path executableFile = binDir.resolve(getLauncherName(params));
+        Path executableFile = appLayout.launchersDirectory().resolve(
+                getLauncherName(params));
 
         try (InputStream is_launcher =
                 getResourceAsStream(getLauncherResourceName(params))) {
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java	Mon Jun 08 09:12:58 2020 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.jpackage.internal;
-
-import java.text.MessageFormat;
-import java.util.Map;
-import java.util.ResourceBundle;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-class WindowsBundlerParam<T> extends StandardBundlerParam<T> {
-
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.incubator.jpackage.internal.resources.WinResources");
-
-    WindowsBundlerParam(String id, Class<T> valueType,
-            Function<Map<String, ? super Object>, T> defaultValueFunction,
-            BiFunction<String,
-            Map<String, ? super Object>, T> stringConverter) {
-        super(id, valueType, defaultValueFunction, stringConverter);
-    }
-
-    static final BundlerParamInfo<String> INSTALLER_FILE_NAME =
-            new StandardBundlerParam<> (
-            "win.installerName",
-            String.class,
-            params -> {
-                String nm = APP_NAME.fetchFrom(params);
-                if (nm == null) return null;
-
-                String version = VERSION.fetchFrom(params);
-                if (version == null) {
-                    return nm;
-                } else {
-                    return nm + "-" + version;
-                }
-            },
-            (s, p) -> s);
-
-    static final StandardBundlerParam<String> MENU_GROUP =
-            new StandardBundlerParam<>(
-                    Arguments.CLIOptions.WIN_MENU_GROUP.getId(),
-                    String.class,
-                    params -> I18N.getString("param.menu-group.default"),
-                    (s, p) -> s
-            );
-
-    static final BundlerParamInfo<Boolean> INSTALLDIR_CHOOSER =
-            new StandardBundlerParam<> (
-            Arguments.CLIOptions.WIN_DIR_CHOOSER.getId(),
-            Boolean.class,
-            params -> Boolean.FALSE,
-            (s, p) -> Boolean.valueOf(s)
-    );
-
-    static final BundlerParamInfo<String> WINDOWS_INSTALL_DIR =
-            new StandardBundlerParam<>(
-            "windows-install-dir",
-            String.class,
-            params -> {
-                 String dir = INSTALL_DIR.fetchFrom(params);
-                 if (dir != null) {
-                     if (dir.contains(":") || dir.contains("..")) {
-                         Log.error(MessageFormat.format(I18N.getString(
-                                "message.invalid.install.dir"), dir,
-                                APP_NAME.fetchFrom(params)));
-                     } else {
-                        if (dir.startsWith("\\")) {
-                             dir = dir.substring(1);
-                        }
-                        if (dir.endsWith("\\")) {
-                             dir = dir.substring(0, dir.length() - 1);
-                        }
-                        return dir;
-                     }
-                 }
-                 return APP_NAME.fetchFrom(params); // Default to app name
-             },
-            (s, p) -> s
-    );
-}
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -38,8 +38,6 @@
 import jdk.incubator.jpackage.internal.IOUtils.XmlConsumer;
 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
 import static jdk.incubator.jpackage.internal.WinMsiBundler.*;
-import static jdk.incubator.jpackage.internal.WindowsBundlerParam.MENU_GROUP;
-import static jdk.incubator.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR;
 
 /**
  * Creates application WiX source files.
@@ -824,7 +822,7 @@
             PROGRAM_MENU_PATH, DESKTOP_PATH);
 
     private static final StandardBundlerParam<Boolean> MENU_HINT =
-        new WindowsBundlerParam<>(
+        new StandardBundlerParam<>(
                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
                 Boolean.class,
                 params -> false,
@@ -835,7 +833,7 @@
         );
 
     private static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
-        new WindowsBundlerParam<>(
+        new StandardBundlerParam<>(
                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
                 Boolean.class,
                 params -> false,
@@ -844,4 +842,38 @@
                 (s, p) -> (s == null ||
                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
         );
+
+    private static final StandardBundlerParam<String> MENU_GROUP =
+            new StandardBundlerParam<>(
+                    Arguments.CLIOptions.WIN_MENU_GROUP.getId(),
+                    String.class,
+                    params -> I18N.getString("param.menu-group.default"),
+                    (s, p) -> s
+            );
+
+    private static final BundlerParamInfo<String> WINDOWS_INSTALL_DIR =
+            new StandardBundlerParam<>(
+            "windows-install-dir",
+            String.class,
+            params -> {
+                 String dir = INSTALL_DIR.fetchFrom(params);
+                 if (dir != null) {
+                     if (dir.contains(":") || dir.contains("..")) {
+                         Log.error(MessageFormat.format(I18N.getString(
+                                "message.invalid.install.dir"), dir,
+                                APP_NAME.fetchFrom(params)));
+                     } else {
+                        if (dir.startsWith("\\")) {
+                             dir = dir.substring(1);
+                        }
+                        if (dir.endsWith("\\")) {
+                             dir = dir.substring(0, dir.length() - 1);
+                        }
+                        return dir;
+                     }
+                 }
+                 return APP_NAME.fetchFrom(params); // Default to app name
+             },
+            (s, p) -> s
+    );
 }
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties	Mon Jun 08 09:13:00 2020 -0400
@@ -49,7 +49,6 @@
 error.invalid-envvar=Invalid value of {0} environment variable
 error.lock-resource=Failed to lock: {0}
 
-message.result-dir=Result application bundle: {0}.
 message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place.
 message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}".
 message.outputting-to-location=Generating EXE for installer to: {0}.
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties	Mon Jun 08 09:13:00 2020 -0400
@@ -47,9 +47,8 @@
 error.msi-product-version-minor-out-of-range=\u30DE\u30A4\u30CA\u30FC\u30FB\u30D0\u30FC\u30B8\u30E7\u30F3\u306F\u7BC4\u56F2[0, 255]\u5185\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059
 error.version-swap={0}\u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u60C5\u5831\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F
 error.invalid-envvar={0}\u74B0\u5883\u5909\u6570\u306E\u5024\u304C\u7121\u52B9\u3067\u3059
-+error.lock-resource=Failed to lock: {0}
+error.lock-resource=Failed to lock: {0}
 
-message.result-dir=\u7D50\u679C\u306E\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30FB\u30D0\u30F3\u30C9\u30EB: {0}
 message.icon-not-ico=\u6307\u5B9A\u3057\u305F\u30A2\u30A4\u30B3\u30F3"{0}"\u306FICO\u30D5\u30A1\u30A4\u30EB\u3067\u306F\u306A\u304F\u3001\u4F7F\u7528\u3055\u308C\u307E\u305B\u3093\u3002\u30C7\u30D5\u30A9\u30EB\u30C8\u30FB\u30A2\u30A4\u30B3\u30F3\u304C\u305D\u306E\u4F4D\u7F6E\u306B\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002
 message.potential.windows.defender.issue=\u8B66\u544A: Windows Defender\u304C\u539F\u56E0\u3067jpackage\u304C\u6A5F\u80FD\u3057\u306A\u3044\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002\u554F\u984C\u304C\u767A\u751F\u3057\u305F\u5834\u5408\u306F\u3001\u30EA\u30A2\u30EB\u30BF\u30A4\u30E0\u30FB\u30E2\u30CB\u30BF\u30EA\u30F3\u30B0\u3092\u7121\u52B9\u306B\u3059\u308B\u304B\u3001\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA"{0}"\u306E\u9664\u5916\u3092\u8FFD\u52A0\u3059\u308B\u3053\u3068\u306B\u3088\u308A\u3001\u554F\u984C\u306B\u5BFE\u51E6\u3067\u304D\u307E\u3059\u3002
 message.outputting-to-location=\u30A4\u30F3\u30B9\u30C8\u30FC\u30E9\u306EEXE\u3092\u6B21\u306B\u751F\u6210\u3057\u3066\u3044\u307E\u3059: {0}
--- a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties	Mon Jun 08 09:12:58 2020 -0400
+++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties	Mon Jun 08 09:13:00 2020 -0400
@@ -47,9 +47,8 @@
 error.msi-product-version-minor-out-of-range=\u6B21\u7248\u672C\u5FC5\u987B\u4F4D\u4E8E [0, 255] \u8303\u56F4\u4E2D
 error.version-swap=\u65E0\u6CD5\u66F4\u65B0 {0} \u7684\u7248\u672C\u4FE1\u606F
 error.invalid-envvar={0} \u73AF\u5883\u53D8\u91CF\u7684\u503C\u65E0\u6548
-+error.lock-resource=Failed to lock: {0}
+error.lock-resource=Failed to lock: {0}
 
-message.result-dir=\u751F\u6210\u7684\u5E94\u7528\u7A0B\u5E8F\u5305: {0}\u3002
 message.icon-not-ico=\u6307\u5B9A\u7684\u56FE\u6807 "{0}" \u4E0D\u662F ICO \u6587\u4EF6, \u4E0D\u4F1A\u4F7F\u7528\u3002\u5C06\u4F7F\u7528\u9ED8\u8BA4\u56FE\u6807\u4EE3\u66FF\u3002
 message.potential.windows.defender.issue=\u8B66\u544A\uFF1AWindows Defender \u53EF\u80FD\u4F1A\u963B\u6B62 jpackage \u6B63\u5E38\u5DE5\u4F5C\u3002\u5982\u679C\u5B58\u5728\u95EE\u9898\uFF0C\u53EF\u4EE5\u901A\u8FC7\u7981\u7528\u5B9E\u65F6\u76D1\u89C6\u6216\u8005\u4E3A\u76EE\u5F55 "{0}" \u6DFB\u52A0\u6392\u9664\u9879\u6765\u89E3\u51B3\u3002
 message.outputting-to-location=\u6B63\u5728\u4E3A\u5B89\u88C5\u7A0B\u5E8F\u751F\u6210 EXE, \u4F4D\u7F6E: {0}\u3002
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Mon Jun 08 09:13:00 2020 -0400
@@ -729,10 +729,7 @@
 
     public List<String> readRuntimeReleaseFile() {
         verifyIsOfType(PackageType.IMAGE);
-        if (isRuntime()) {
-            return null;
-        }
-        Path release = appLayout().runtimeRelease();
+        Path release = appLayout().runtimeHomeDirectory().resolve("release");
         try {
             return Files.readAllLines(release);
         } catch (IOException ioe) {
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,14 +23,21 @@
 
 package jdk.jpackage.tests;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.ArrayList;
 import java.util.List;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import jdk.incubator.jpackage.internal.AppImageFile;
 import jdk.jpackage.test.Annotations.Parameters;
 import jdk.jpackage.test.Annotations.Test;
 import jdk.jpackage.test.JPackageCommand;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.TKit;
+import jdk.incubator.jpackage.internal.AppImageFile;
+import org.w3c.dom.Document;
 
 /*
  * @test
@@ -93,7 +100,7 @@
     }
 
     @Test
-    public void test() {
+    public void test() throws XPathExpressionException, IOException {
         if (expectedVersion == null) {
             new PackageTest()
             .setExpectedExitCode(1)
@@ -110,8 +117,11 @@
             cmd.addArguments(jpackageArgs);
         }
         cmd.executeAndAssertHelloAppImageCreated();
-        String actualVersion = cmd.readLaunherCfgFile().getValue("Application",
-                "app.version");
+
+        Document xml = AppImageFile.readXml(cmd.outputBundle());
+        String actualVersion = XPathFactory.newInstance().newXPath().evaluate(
+                "/jpackage-state/app-version/text()", xml, XPathConstants.STRING).toString();
+
         TKit.assertEquals(expectedVersion, actualVersion,
                 "Check application version");
     }
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Mon Jun 08 09:13:00 2020 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -129,7 +129,6 @@
         List<String> expectedVerboseOutputStrings = new ArrayList<>();
         expectedVerboseOutputStrings.add("Creating app package:");
         if (TKit.isWindows()) {
-            expectedVerboseOutputStrings.add("Result application bundle:");
             expectedVerboseOutputStrings.add(
                     "Succeeded in building Windows Application Image package");
         } else if (TKit.isLinux()) {
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/JLinkOptionsTest.java	Mon Jun 08 09:12:58 2020 -0400
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/JLinkOptionsTest.java	Mon Jun 08 09:13:00 2020 -0400
@@ -115,10 +115,10 @@
     public JLinkOptionsTest(String javaAppDesc, String[] jpackageArgs, String[] required, String[] prohibited) {
         this.required = required;
         this.prohibited = prohibited;
-        cmd = JPackageCommand.helloAppImage(javaAppDesc);
-        if (jpackageArgs != null) {
-            cmd.addArguments(jpackageArgs);
-        }
+        cmd = JPackageCommand
+                .helloAppImage(javaAppDesc)
+                .ignoreDefaultRuntime(true)
+                .addArguments(jpackageArgs);
     }
 
     @Test