JDK-8211282: Support creation of 'exe' installers on Windows. JDK-8200758-branch
authorherrick
Mon, 08 Oct 2018 18:17:37 -0400
branchJDK-8200758-branch
changeset 56941 20568f1e1aa7
parent 56940 93d2d065257e
child 56942 cf8d1b2388b8
JDK-8211282: Support creation of 'exe' installers on Windows. Reviewed-by: almatvee, kcr
make/CompileJavaModules.gmk
src/jdk.packager/share/classes/jdk/packager/internal/resources/CLIHelp.properties
src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/WinExeBundler.properties
src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/WinMsiBundler.properties
src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/template.iss
src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinExeBundler.java
src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinMsiBundler.java
src/jdk.packager/windows/classes/module-info.java.extra
--- a/make/CompileJavaModules.gmk	Mon Oct 08 18:10:31 2018 -0400
+++ b/make/CompileJavaModules.gmk	Mon Oct 08 18:17:37 2018 -0400
@@ -388,7 +388,7 @@
 
 jdk.packager_SETUP := GENERATE_JDKBYTECODE_NOWARNINGS
 jdk.packager_COPY += .gif .png .txt .spec .script .prerm .preinst .postrm .postinst .list \
-    .desktop .copyright .control .plist .template .icns .scpt .entitlements .wxs .ico .bmp
+    .desktop .copyright .control .plist .template .icns .scpt .entitlements .wxs .iss .ico .bmp
 
 jdk.packager_CLEAN_FILES += $(wildcard \
     $(TOPDIR)/src/jdk.packager/share/classes/jdk/packager/internal/resources/*.properties \
--- a/src/jdk.packager/share/classes/jdk/packager/internal/resources/CLIHelp.properties	Mon Oct 08 18:10:31 2018 -0400
+++ b/src/jdk.packager/share/classes/jdk/packager/internal/resources/CLIHelp.properties	Mon Oct 08 18:17:37 2018 -0400
@@ -5,12 +5,14 @@
 \          Generates a platform-specific application image.\n\
 \  create-installer <type>\n\
 \          Generates a platform-specific installer for the application.\n\
-\          Valid values for "type" are "msi", "rpm", "deb", "dmg", \u201cpkg\u201d,\n\
-\          \u201cpkg-app-store\u201d. If "type" is omitted, all supported types of installable\n\
-\          packages for current platform will be generated.\n\
+\          Valid values for "type" are "msi", "exe", "rpm", "deb", "dmg",\n\
+\                           "pkg", and "pkg-app-store".\n\
+\          If "type" is omitted, all supported types of installable packages\n\
+\          for current platform will be generated.\n\
 \  create-jre-installer <type>\n\
 \          Generates a platform-specific installer for Server JRE.\n\
-\          Valid values for "type" are "msi", "rpm", "deb", "dmg", \u201cpkg\u201d.\n\
+\          Valid values for "type" are "msi", "exe", "rpm", "deb", "dmg", \n\
+\                           and "pkg".\n\
 \          If "type" is omitted, all supported types of installable packages\n\
 \          for current platform will be generated.\n\
 \          \n\
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/WinExeBundler.properties	Mon Oct 08 18:17:37 2018 -0400
@@ -0,0 +1,87 @@
+bundler.name=EXE Installer
+bundler.description=Microsoft Windows EXE Installer, via InnoIDE.
+
+param.system-wide.name=System Wide
+param.system-wide.description=Should this application attempt to install itself system wide, or only for each user?  Null means use the system default.
+
+param.app-bundler.name=
+param.app-bundler.description=
+
+param.service-bundler.name=
+param.service-bundler.description=
+
+param.can-use-wix36.name=
+param.can-use-wix36.description=
+
+param.out-dir.name=
+param.out-dir.description=
+
+param.config-root.name=
+param.config-root.description=
+
+param.image-dir.name=
+param.image-dir.description=
+
+param.app-dir.name=
+param.app-dir.description=
+
+param.menu-shortcut-hint.name=Menu Hint
+param.menu-shortcut-hint.description=If the bundler can add the application to the system menu, should it?
+
+param.desktop-shortcut-hint.name=Shortcut Hint
+param.desktop-shortcut-hint.description=If the bundler can create desktop shortcuts, should it make one?
+
+param.upgrade-uuid.name=Upgrade UUID
+param.upgrade-uuid.description=The UUID associated with upgrades for this package.
+
+param.product-version.name=Product Version
+param.product-version.description=The version of the application as seen by Windows and MSI, of the form "1.2.3"
+
+param.iscc-path.name=InnoSetup iscc.exe location
+param.iscc-path.description=File path to iscc.exe from the InnoSetup tool.
+
+resource.inno-setup-project-file=Inno Setup project file
+resource.setup-icon=setup dialog icon
+resource.post-install-script=script to run after application image is populated
+
+error.parameters-null=Parameters map is null.
+error.parameters-null.advice=Pass in a non-null parameters map.
+
+error.iscc-not-found=Can not find Inno Setup Compiler (iscc.exe).
+error.iscc-not-found.advice=Download Inno Setup 5 or later from http\://www.jrsoftware.org and add it to the PATH.
+
+error.license-missing=Specified license file is missing.
+error.license-missing.advice=Make sure that "{0}" references a file in the app resources, and that it is relative file reference.
+
+error.copyright-is-too-long=The copyright string is too long for InnoSetup.
+error.copyright-is-too-long.advice=Provide a copyright string shorter than 100 characters.
+
+error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
+error.too-many-content-types-for-file-association.advice=Specify one and only one MIME type for each file association.
+
+error.cannot-create-output-dir=Output directory {0} cannot be created.
+error.cannot-write-to-output-dir=Output directory {0} is not writable.
+
+message.tool-wrong-version=Detected [{0}] version {1} but version {2} is required.
+message.debug-working-directory=Kept working directory for debug\: {0}
+message.config-save-location=\  Config files are saved to {0}. Use them to customize package.
+message.outputting-to-location=Generating EXE for installer to\: {0}
+message.output-location=Installer (.exe) saved to\: {0}
+message.tool-version=\  Detected [{0}] version [{1}]
+message.one-shortcut-required=At least one type of shortcut is required. Enabling menu shortcut.
+message.running-wsh-script=Running WSH script on application image [{0}]
+message.iscc-file-string=\  InnoSetup compiler set to {0}
+message.creating-association-with-null-extension=Creating association with null extension.
+message.potential.windows.defender.issue=Warning: Windows Defender may prevent the Java Packager 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.tool-version=Detected [{0}] version [{1}]
+message.running-wsh-script=Running WSH script on application image [{0}]
+message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required.
+message.use-wix36-features=WiX 3.6 detected. Enabling advanced cleanup action.
+message.version-string-too-many-components=Version sting may have up to 3 components - major.minor.build .
+message.debug-working-directory=Kept working directory for debug\: {0}
+message.config-save-location=Config files are saved to {0}. Use them to customize package.
+message.one-shortcut-required=At least one type of shortcut is required. Enabling menu shortcut.
+message.creating-association-with-null-extension=Creating association with null extension.
--- a/src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/WinMsiBundler.properties	Mon Oct 08 18:10:31 2018 -0400
+++ b/src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/WinMsiBundler.properties	Mon Oct 08 18:17:37 2018 -0400
@@ -60,7 +60,7 @@
 error.cannot-create-output-dir=Output directory {0} cannot be created.
 error.cannot-write-to-output-dir=Output directory {0} is not writable.
 error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
-error.too-many-content-types-for-file-association.advice=For Linux Bundling specify one and only one MIME type for each file association.
+error.too-many-content-types-for-file-association.advice=Specify one and only one MIME type for each file association.
 error.license-missing=can not find license file {0}.
 error.license-missing.advice=include license file {0} in the --files argument.
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.packager/windows/classes/jdk/packager/internal/resources/windows/template.iss	Mon Oct 08 18:17:37 2018 -0400
@@ -0,0 +1,76 @@
+;This file will be executed next to the application bundle image
+;I.e. current directory will contain folder APPLICATION_NAME with application files
+[Setup]
+AppId={{PRODUCT_APP_IDENTIFIER}}
+AppName=APPLICATION_NAME
+AppVersion=APPLICATION_VERSION
+AppVerName=APPLICATION_NAME APPLICATION_VERSION
+AppPublisher=APPLICATION_VENDOR
+AppComments=APPLICATION_COMMENTS
+AppCopyright=APPLICATION_COPYRIGHT
+;AppPublisherURL=http://java.com/
+;AppSupportURL=http://java.com/
+;AppUpdatesURL=http://java.com/
+DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
+DisableStartupPrompt=Yes
+DisableDirPage=DISABLE_DIR_PAGE
+DisableProgramGroupPage=Yes
+DisableReadyPage=Yes
+DisableFinishedPage=Yes
+DisableWelcomePage=Yes
+DefaultGroupName=APPLICATION_GROUP
+;Optional License
+LicenseFile=APPLICATION_LICENSE_FILE
+;WinXP or above
+MinVersion=0,5.1 
+OutputBaseFilename=INSTALLER_FILE_NAME
+Compression=lzma
+SolidCompression=yes
+PrivilegesRequired=APPLICATION_INSTALL_PRIVILEGE
+SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
+UninstallDisplayIcon={app}\APPLICATION_NAME.ico
+UninstallDisplayName=APPLICATION_NAME
+WizardImageStretch=No
+WizardSmallImageFile=APPLICATION_NAME-setup-icon.bmp   
+ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
+FILE_ASSOCIATIONS
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Files]
+Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
+Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
+Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe";  IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
+SECONDARY_LAUNCHERS
+
+[Run]
+Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-Xappcds:generatecache"; Check: APPLICATION_APP_CDS_INSTALL()
+Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
+Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
+
+[UninstallRun]
+Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
+
+[Code]
+function returnTrue(): Boolean;
+begin
+  Result := True;
+end;
+
+function returnFalse(): Boolean;
+begin
+  Result := False;
+end;
+
+function InitializeSetup(): Boolean;
+begin
+// Possible future improvements:
+//   if version less or same => just launch app
+//   if upgrade => check if same app is running and wait for it to exit
+//   Add pack200/unpack200 support? 
+  Result := True;
+end;  
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinExeBundler.java	Mon Oct 08 18:17:37 2018 -0400
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (c) 2017, 2018, 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.packager.internal.windows;
+
+import jdk.packager.internal.*;
+import jdk.packager.internal.ConfigException;
+import jdk.packager.internal.Arguments;
+import jdk.packager.internal.UnsupportedPlatformException;
+import jdk.packager.internal.resources.windows.WinResources;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static jdk.packager.internal.windows.WindowsBundlerParam.*;
+
+public class WinExeBundler extends AbstractBundler {
+
+    private static final ResourceBundle I18N = ResourceBundle.getBundle(
+            "jdk.packager.internal.resources.windows.WinExeBundler");
+
+    public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
+            new WindowsBundlerParam<>(
+            getString("param.app-bundler.name"),
+            getString("param.app-bundler.description"),
+            "win.app.bundler",
+            WinAppBundler.class,
+            params -> new WinAppBundler(),
+            null);
+
+    public static final BundlerParamInfo<File> CONFIG_ROOT =
+            new WindowsBundlerParam<>(
+            getString("param.config-root.name"),
+            getString("param.config-root.description"),
+            "configRoot",
+            File.class,
+            params -> {
+                File imagesRoot =
+                        new File(BUILD_ROOT.fetchFrom(params), "windows");
+                imagesRoot.mkdirs();
+                return imagesRoot;
+            },
+            (s, p) -> null);
+
+    public static final BundlerParamInfo<File> EXE_IMAGE_DIR =
+            new WindowsBundlerParam<>(
+            getString("param.image-dir.name"),
+            getString("param.image-dir.description"),
+            "win.exe.imageDir",
+            File.class,
+            params -> {
+                File imagesRoot = IMAGES_ROOT.fetchFrom(params);
+                if (!imagesRoot.exists()) imagesRoot.mkdirs();
+                return new File(imagesRoot, "win-exe.image");
+            },
+            (s, p) -> null);
+
+    public static final BundlerParamInfo<File> WIN_APP_IMAGE =
+            new WindowsBundlerParam<>(
+            getString("param.app-dir.name"),
+            getString("param.app-dir.description"),
+            "win.app.image",
+            File.class,
+            null,
+            (s, p) -> null);
+
+
+    public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE  =
+            new StandardBundlerParam<>(
+            getString("param.system-wide.name"),
+            getString("param.system-wide.description"),
+            Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
+            Boolean.class,
+            params -> true, // default to system wide
+            (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
+                    : Boolean.valueOf(s)
+            );
+    public static final StandardBundlerParam<String> PRODUCT_VERSION =
+            new StandardBundlerParam<>(
+                    getString("param.product-version.name"),
+                    getString("param.product-version.description"),
+                    "win.msi.productVersion",
+                    String.class,
+                    VERSION::fetchFrom,
+                    (s, p) -> s
+            );
+
+    public static final StandardBundlerParam<Boolean> MENU_HINT =
+        new WindowsBundlerParam<>(
+                getString("param.menu-shortcut-hint.name"),
+                getString("param.menu-shortcut-hint.description"),
+                Arguments.CLIOptions.WIN_MENU_HINT.getId(),
+                Boolean.class,
+                params -> false,
+                (s, p) -> (s == null ||
+                        "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
+        );
+
+    public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
+        new WindowsBundlerParam<>(
+                getString("param.desktop-shortcut-hint.name"),
+                getString("param.desktop-shortcut-hint.description"),
+                Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
+                Boolean.class,
+                params -> false,
+                (s, p) -> (s == null ||
+                       "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
+        );
+
+
+
+    private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss";
+    private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe";
+
+    public static final BundlerParamInfo<String>
+            TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>(
+            getString("param.iscc-path.name"),
+            getString("param.iscc-path.description"),
+            "win.exe.iscc.exe",
+            String.class,
+            params -> {
+                for (String dirString : (System.getenv("PATH") + ";C:\\Program Files (x86)\\Inno Setup 5;C:\\Program Files\\Inno Setup 5").split(";")) {
+                    File f = new File(dirString.replace("\"", ""),
+                            TOOL_INNO_SETUP_COMPILER);
+                    if (f.isFile()) {
+                        return f.toString();
+                    }
+                }
+                return null;
+            },
+            null);
+
+    public WinExeBundler() {
+        super();
+        baseResourceLoader = WinResources.class;
+    }
+
+    @Override
+    public String getName() {
+        return getString("bundler.name");
+    }
+
+    @Override
+    public String getDescription() {
+        return getString("bundler.description");
+    }
+
+    @Override
+    public String getID() {
+        return "exe";
+    }
+
+    @Override
+    public String getBundleType() {
+        return "INSTALLER";
+    }
+
+    @Override
+    public Collection<BundlerParamInfo<?>> getBundleParameters() {
+        Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
+        results.addAll(WinAppBundler.getAppBundleParameters());
+        results.addAll(getExeBundleParameters());
+        return results;
+    }
+
+    public static Collection<BundlerParamInfo<?>> getExeBundleParameters() {
+        return Arrays.asList(
+                DESCRIPTION,
+                COPYRIGHT,
+                LICENSE_FILE,
+                MENU_GROUP,
+                MENU_HINT,
+                // RUN_AT_STARTUP,
+                SHORTCUT_HINT,
+                // SERVICE_HINT,
+                // START_ON_INSTALL,
+                // STOP_ON_UNINSTALL,
+                EXE_SYSTEM_WIDE,
+                TITLE,
+                VENDOR,
+                INSTALLDIR_CHOOSER
+        );
+    }
+
+    @Override
+    public File execute(
+            Map<String, ? super Object> p, File outputParentDir) {
+        return bundle(p, outputParentDir);
+    }
+
+    @Override
+    public boolean supported() {
+        return (Platform.getPlatform() == Platform.WINDOWS);
+    }
+
+    static class VersionExtractor extends PrintStream {
+        double version = 0f;
+
+        public VersionExtractor() {
+            super(new ByteArrayOutputStream());
+        }
+
+        double getVersion() {
+            if (version == 0f) {
+                String content =
+                        new String(((ByteArrayOutputStream) out).toByteArray());
+                Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)");
+                Matcher matcher = pattern.matcher(content);
+                if (matcher.find()) {
+                    String v = matcher.group(1);
+                    version = new Double(v);
+                }
+            }
+            return version;
+        }
+    }
+
+    private static double findToolVersion(String toolName) {
+        try {
+            if (toolName == null || "".equals(toolName)) return 0f;
+
+            ProcessBuilder pb = new ProcessBuilder(
+                    toolName,
+                    "/?");
+            VersionExtractor ve = new VersionExtractor();
+            IOUtils.exec(pb, Log.isDebug(), true, ve);
+            // not interested in the output
+            double version = ve.getVersion();
+            Log.verbose(MessageFormat.format(
+                    getString("message.tool-version"), toolName, version));
+            return version;
+        } catch (Exception e) {
+            if (Log.isDebug()) {
+                Log.verbose(e);
+            }
+            return 0f;
+        }
+    }
+
+    @Override
+    public boolean validate(Map<String, ? super Object> p)
+            throws UnsupportedPlatformException, ConfigException {
+        try {
+            if (p == null) throw new ConfigException(
+                      getString("error.parameters-null"),
+                      getString("error.parameters-null.advice"));
+
+            // run basic validation to ensure requirements are met
+            // we are not interested in return code, only possible exception
+            APP_BUNDLER.fetchFrom(p).validate(p);
+
+            // make sure some key values don't have newlines
+            for (BundlerParamInfo<String> pi : Arrays.asList(
+                    APP_NAME,
+                    COPYRIGHT,
+                    DESCRIPTION,
+                    MENU_GROUP,
+                    TITLE,
+                    VENDOR,
+                    VERSION)
+            ) {
+                String v = pi.fetchFrom(p);
+                if (v.contains("\n") | v.contains("\r")) {
+                    throw new ConfigException("Parmeter '" + pi.getID() +
+                            "' cannot contain a newline.",
+                            " Change the value of '" + pi.getID() +
+                            " so that it does not contain any newlines");
+                }
+            }
+
+            // exe bundlers trim the copyright to 100 characters,
+            // tell them this will happen
+            if (COPYRIGHT.fetchFrom(p).length() > 100) {
+                throw new ConfigException(
+                        getString("error.copyright-is-too-long"),
+                        getString("error.copyright-is-too-long.advice"));
+            }
+
+            // validate license file, if used, exists in the proper place
+            if (p.containsKey(LICENSE_FILE.getID())) {
+                List<RelativeFileSet> appResourcesList =
+                        APP_RESOURCES_LIST.fetchFrom(p);
+                for (String license : LICENSE_FILE.fetchFrom(p)) {
+                    boolean found = false;
+                    for (RelativeFileSet appResources : appResourcesList) {
+                        found = found || appResources.contains(license);
+                    }
+                    if (!found) {
+                        throw new ConfigException(
+                                getString("error.license-missing"),
+                                MessageFormat.format(getString(
+                                "error.license-missing.advice"), license));
+                    }
+                }
+            }
+//
+//            if (SERVICE_HINT.fetchFrom(p)) {
+//                SERVICE_BUNDLER.fetchFrom(p).validate(p);
+//            }
+
+            double innoVersion = findToolVersion(
+                    TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p));
+
+            //Inno Setup 5+ is required
+            double minVersion = 5.0f;
+
+            if (innoVersion < minVersion) {
+                Log.info(MessageFormat.format(
+                        getString("message.tool-wrong-version"),
+                        TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion));
+                throw new ConfigException(
+                        getString("error.iscc-not-found"),
+                        getString("error.iscc-not-found.advice"));
+            }
+
+            /********* validate bundle parameters *************/
+
+            // only one mime type per association, at least one file extension
+            List<Map<String, ? super Object>> associations =
+                    FILE_ASSOCIATIONS.fetchFrom(p);
+            if (associations != null) {
+                for (int i = 0; i < associations.size(); i++) {
+                    Map<String, ? super Object> assoc = associations.get(i);
+                    List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
+                    if (mimes.size() > 1) {
+                        throw new ConfigException(MessageFormat.format(
+                                getString("error.too-many-content-"
+                                + "types-for-file-association"), i),
+                                getString("error.too-many-content-"
+                                + "types-for-file-association.advice"));
+                    }
+                }
+            }
+
+            // validate license file, if used, exists in the proper place
+            if (p.containsKey(LICENSE_FILE.getID())) {
+                List<RelativeFileSet> appResourcesList =
+                        APP_RESOURCES_LIST.fetchFrom(p);
+                for (String license : LICENSE_FILE.fetchFrom(p)) {
+                    boolean found = false;
+                    for (RelativeFileSet appResources : appResourcesList) {
+                        found = found || appResources.contains(license);
+                    }
+                    if (!found) {
+                        throw new ConfigException(
+                            MessageFormat.format(getString(
+                               "error.license-missing"), license),
+                            MessageFormat.format(getString(
+                               "error.license-missing.advice"), license));
+                    }
+                }
+            }
+
+            return true;
+        } catch (RuntimeException re) {
+            if (re.getCause() instanceof ConfigException) {
+                throw (ConfigException) re.getCause();
+            } else {
+                throw new ConfigException(re);
+            }
+        }
+    }
+
+    private boolean prepareProto(Map<String, ? super Object> p)
+                throws IOException {
+        File appImage = StandardBundlerParam.getPredefinedAppImage(p);
+        File appDir = null;
+
+        // we either have an application image or need to build one
+        if (appImage != null) {
+            appDir = new File(
+                    EXE_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p));
+            // copy everything from appImage dir into appDir/name
+            IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
+        } else {
+            appDir = APP_BUNDLER.fetchFrom(p).doBundle(p,
+                    EXE_IMAGE_DIR.fetchFrom(p), true);
+        }
+
+        if (appDir == null) {
+            return false;
+        }
+
+        p.put(WIN_APP_IMAGE.getID(), appDir);
+
+        List<String> licenseFiles = LICENSE_FILE.fetchFrom(p);
+        if (licenseFiles != null) {
+            // need to copy license file to the root of win.app.image
+            outerLoop:
+            for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(p)) {
+                for (String s : licenseFiles) {
+                    if (rfs.contains(s)) {
+                        File lfile = new File(rfs.getBaseDirectory(), s);
+                        File destFile = new File(appDir, lfile.getName());
+                        IOUtils.copyFile(lfile, destFile);
+                        ensureByMutationFileIsRTF(destFile);
+                        break outerLoop;
+                    }
+                }
+            }
+        }
+
+        // copy file association icons
+        List<Map<String, ? super Object>> fileAssociations =
+                FILE_ASSOCIATIONS.fetchFrom(p);
+
+        for (Map<String, ? super Object> fa : fileAssociations) {
+            File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
+            if (icon == null) {
+                continue;
+            }
+
+            File faIconFile = new File(appDir, icon.getName());
+
+            if (icon.exists()) {
+                try {
+                    IOUtils.copyFile(icon, faIconFile);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+//
+//        if (SERVICE_HINT.fetchFrom(p)) {
+//            // copies the service launcher to the app root folder
+//            appDir = SERVICE_BUNDLER.fetchFrom(p).doBundle(
+//                    p, appOutputDir, true);
+//            if (appDir == null) {
+//                return false;
+//            }
+//        }
+        return true;
+    }
+
+    public File bundle(Map<String, ? super Object> p, File outdir) {
+        if (!outdir.isDirectory() && !outdir.mkdirs()) {
+            throw new RuntimeException(MessageFormat.format(
+                    getString("error.cannot-create-output-dir"),
+                    outdir.getAbsolutePath()));
+        }
+        if (!outdir.canWrite()) {
+            throw new RuntimeException(MessageFormat.format(
+                    getString("error.cannot-write-to-output-dir"),
+                    outdir.getAbsolutePath()));
+        }
+
+        if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) {
+            Log.info(MessageFormat.format(
+                    getString("message.potential.windows.defender.issue"),
+                    WindowsDefender.getUserTempDirectory()));
+        }
+
+        // validate we have valid tools before continuing
+        String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p);
+        if (iscc == null || !new File(iscc).isFile()) {
+            Log.info(getString("error.iscc-not-found"));
+            Log.info(MessageFormat.format(
+                    getString("message.iscc-file-string"), iscc));
+            return null;
+        }
+
+        File imageDir = EXE_IMAGE_DIR.fetchFrom(p);
+        try {
+            imageDir.mkdirs();
+
+            boolean menuShortcut = MENU_HINT.fetchFrom(p);
+            boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
+            if (!menuShortcut && !desktopShortcut) {
+                // both can not be false - user will not find the app
+                Log.verbose(getString("message.one-shortcut-required"));
+                p.put(MENU_HINT.getID(), true);
+            }
+
+            if (prepareProto(p) && prepareProjectConfig(p)) {
+                File configScript = getConfig_Script(p);
+                if (configScript.exists()) {
+                    Log.info(MessageFormat.format(
+                            getString("message.running-wsh-script"),
+                            configScript.getAbsolutePath()));
+                    IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
+                }
+                return buildEXE(p, outdir);
+            }
+            return null;
+        } catch (IOException ex) {
+            ex.printStackTrace();
+            return null;
+        } finally {
+            try {
+                if (VERBOSE.fetchFrom(p)) {
+                    saveConfigFiles(p);
+                }
+                if (imageDir != null && !Log.isDebug()) {
+                    IOUtils.deleteRecursive(imageDir);
+                } else if (imageDir != null) {
+                    Log.info(MessageFormat.format(
+                            getString("message.debug-working-directory"),
+                            imageDir.getAbsolutePath()));
+                }
+            } catch (IOException ex) {
+                // noinspection ReturnInsideFinallyBlock
+                Log.debug(ex.getMessage());
+                return null;
+            }
+        }
+    }
+
+    // name of post-image script
+    private File getConfig_Script(Map<String, ? super Object> p) {
+        return new File(EXE_IMAGE_DIR.fetchFrom(p),
+                APP_NAME.fetchFrom(p) + "-post-image.wsf");
+    }
+
+    protected void saveConfigFiles(Map<String, ? super Object> p) {
+        try {
+            File configRoot = CONFIG_ROOT.fetchFrom(p);
+            if (getConfig_ExeProjectFile(p).exists()) {
+                IOUtils.copyFile(getConfig_ExeProjectFile(p),
+                        new File(configRoot,
+                        getConfig_ExeProjectFile(p).getName()));
+            }
+            if (getConfig_Script(p).exists()) {
+                IOUtils.copyFile(getConfig_Script(p),
+                        new File(configRoot,
+                         getConfig_Script(p).getName()));
+            }
+            if (getConfig_SmallInnoSetupIcon(p).exists()) {
+                IOUtils.copyFile(getConfig_SmallInnoSetupIcon(p),
+                        new File(configRoot,
+                        getConfig_SmallInnoSetupIcon(p).getName()));
+            }
+            Log.info(MessageFormat.format(
+                        getString("message.config-save-location"),
+                        configRoot.getAbsolutePath()));
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+        }
+    }
+
+    private String getAppIdentifier(Map<String, ? super Object> p) {
+        String nm = IDENTIFIER.fetchFrom(p);
+
+        //limitation of innosetup
+        if (nm.length() > 126)
+            nm = nm.substring(0, 126);
+
+        return nm;
+    }
+
+
+    private String getLicenseFile(Map<String, ? super Object> p) {
+        List<String> licenseFiles = LICENSE_FILE.fetchFrom(p);
+        if (licenseFiles == null || licenseFiles.isEmpty()) {
+            return "";
+        } else {
+            return licenseFiles.get(0);
+        }
+    }
+
+    void validateValueAndPut(Map<String, String> data, String key,
+                BundlerParamInfo<String> param,
+                Map<String, ? super Object> p) throws IOException {
+        String value = param.fetchFrom(p);
+        if (value.contains("\r") || value.contains("\n")) {
+            throw new IOException("Configuration Parameter " +
+                     param.getID() + " cannot contain multiple lines of text");
+        }
+        data.put(key, innosetupEscape(value));
+    }
+
+    private String innosetupEscape(String value) {
+        if (value.contains("\"") || !value.trim().equals(value)) {
+            value = "\"" + value.replace("\"", "\"\"") + "\"";
+        }
+        return value;
+    }
+
+    boolean prepareMainProjectFile(Map<String, ? super Object> p)
+            throws IOException {
+        Map<String, String> data = new HashMap<>();
+        data.put("PRODUCT_APP_IDENTIFIER",
+                innosetupEscape(getAppIdentifier(p)));
+
+        validateValueAndPut(data, "APPLICATION_NAME", APP_NAME, p);
+
+        validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p);
+        validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p);
+        validateValueAndPut(data, "INSTALLER_FILE_NAME",
+                INSTALLER_FILE_NAME, p);
+
+        data.put("APPLICATION_LAUNCHER_FILENAME",
+                innosetupEscape(WinAppBundler.getLauncherName(p)));
+
+        data.put("APPLICATION_DESKTOP_SHORTCUT",
+                SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
+        data.put("APPLICATION_MENU_SHORTCUT",
+                MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
+        validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p);
+        validateValueAndPut(data, "APPLICATION_COMMENTS",
+                TITLE, p); // TODO this seems strange, at least in name
+        validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p);
+
+        data.put("APPLICATION_LICENSE_FILE",
+                innosetupEscape(getLicenseFile(p)));
+        data.put("DISABLE_DIR_PAGE",
+                INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes");
+
+        Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p);
+
+        if (isSystemWide) {
+            data.put("APPLICATION_INSTALL_ROOT", "{pf}");
+            data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
+        } else {
+            data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
+            data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
+        }
+
+        if (BIT_ARCH_64.fetchFrom(p)) {
+            data.put("ARCHITECTURE_BIT_MODE", "x64");
+        } else {
+            data.put("ARCHITECTURE_BIT_MODE", "");
+        }
+//
+//        if (SERVICE_HINT.fetchFrom(p)) {
+//            data.put("RUN_FILENAME",
+//                innosetupEscape(WinServiceBundler.getAppSvcName(p)));
+//        } else {
+              validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p);
+//        }
+
+        validateValueAndPut(data, "APPLICATION_DESCRIPTION",
+                DESCRIPTION, p);
+
+//        data.put("APPLICATION_SERVICE",
+//                SERVICE_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
+        data.put("APPLICATION_SERVICE", "returnFalse");
+
+//        data.put("APPLICATION_NOT_SERVICE",
+//                SERVICE_HINT.fetchFrom(p) ? "returnFalse" : "returnTrue");
+        data.put("APPLICATION_NOT_SERVICE", "returnFalse");
+
+//        data.put("APPLICATION_APP_CDS_INSTALL",
+//                (UNLOCK_COMMERCIAL_FEATURES.fetchFrom(p) &&
+//                ENABLE_APP_CDS.fetchFrom(p) &&
+//                ("install".equals(APP_CDS_CACHE_MODE.fetchFrom(p)) ||
+//                "auto+install".equals(APP_CDS_CACHE_MODE.fetchFrom(p))))
+//                ? "returnTrue" : "returnFalse");
+        data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse");
+
+//        data.put("START_ON_INSTALL",
+//                START_ON_INSTALL.fetchFrom(p) ? "-startOnInstall" : "");
+//        data.put("STOP_ON_UNINSTALL",
+//                STOP_ON_UNINSTALL.fetchFrom(p) ? "-stopOnUninstall" : "");
+//        data.put("RUN_AT_STARTUP",
+//                RUN_AT_STARTUP.fetchFrom(p) ? "-runAtStartup" : "");
+        data.put("START_ON_INSTALL", "");
+        data.put("STOP_ON_UNINSTALL", "");
+        data.put("RUN_AT_STARTUP", "");
+
+        StringBuilder secondaryLaunchersCfg = new StringBuilder();
+        for (Map<String, ? super Object>
+                launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) {
+            String application_name = APP_NAME.fetchFrom(launcher);
+            if (MENU_HINT.fetchFrom(launcher)) {
+                // Name: "{group}\APPLICATION_NAME";
+                // Filename: "{app}\APPLICATION_NAME.exe";
+                // IconFilename: "{app}\APPLICATION_NAME.ico"
+                secondaryLaunchersCfg.append("Name: \"{group}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append(".ico\"\r\n");
+            }
+            if (SHORTCUT_HINT.fetchFrom(launcher)) {
+                // Name: "{commondesktop}\APPLICATION_NAME";
+                // Filename: "{app}\APPLICATION_NAME.exe";
+                // IconFilename: "{app}\APPLICATION_NAME.ico"
+                secondaryLaunchersCfg.append("Name: \"{commondesktop}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
+                secondaryLaunchersCfg.append(application_name);
+                secondaryLaunchersCfg.append(".ico\"\r\n");
+            }
+        }
+        data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString());
+
+        StringBuilder registryEntries = new StringBuilder();
+        String regName = APP_REGISTRY_NAME.fetchFrom(p);
+        List<Map<String, ? super Object>> fetchFrom =
+                FILE_ASSOCIATIONS.fetchFrom(p);
+        for (int i = 0; i < fetchFrom.size(); i++) {
+            Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
+            String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
+            File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
+
+            List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
+            String entryName = regName + "File";
+            if (i > 0) {
+                entryName += "." + i;
+            }
+
+            if (extensions == null) {
+                Log.info(getString(
+                        "message.creating-association-with-null-extension"));
+            } else {
+                for (String ext : extensions) {
+                    if (isSystemWide) {
+                        // "Root: HKCR; Subkey: \".myp\";
+                        // ValueType: string; ValueName: \"\";
+                        // ValueData: \"MyProgramFile\";
+                        // Flags: uninsdeletevalue"
+                        registryEntries.append("Root: HKCR; Subkey: \".")
+                                .append(ext)
+                                .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
+                                .append(entryName)
+                                .append("\"; Flags: uninsdeletevalue\r\n");
+                    } else {
+                        registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\.")
+                                .append(ext)
+                                .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
+                                .append(entryName)
+                                .append("\"; Flags: uninsdeletevalue\r\n");
+                    }
+                }
+            }
+
+            if (extensions != null && !extensions.isEmpty()) {
+                String ext = extensions.get(0);
+                List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
+                for (String mime : mimeTypes) {
+                    if (isSystemWide) {
+                        // "Root: HKCR;
+                        // Subkey: HKCR\\Mime\\Database\\Content Type\\application/chaos;
+                        // ValueType: string;
+                        // ValueName: Extension;
+                        // ValueData: .chaos;
+                        // Flags: uninsdeletevalue"
+                        registryEntries.append("Root: HKCR; Subkey: " +
+                                 "\"Mime\\Database\\Content Type\\")
+                            .append(mime)
+                            .append("\"; ValueType: string; ValueName: " +
+                                 "\"Extension\"; ValueData: \".")
+                            .append(ext)
+                            .append("\"; Flags: uninsdeletevalue\r\n");
+                    } else {
+                        registryEntries.append(
+                                "Root: HKCU; Subkey: \"Software\\" +
+                                "Classes\\Mime\\Database\\Content Type\\")
+                                .append(mime)
+                                .append("\"; ValueType: string; " +
+                                "ValueName: \"Extension\"; ValueData: \".")
+                                .append(ext)
+                                .append("\"; Flags: uninsdeletevalue\r\n");
+                    }
+                }
+            }
+
+            if (isSystemWide) {
+                // "Root: HKCR;
+                // Subkey: \"MyProgramFile\";
+                // ValueType: string;
+                // ValueName: \"\";
+                // ValueData: \"My Program File\";
+                // Flags: uninsdeletekey"
+                registryEntries.append("Root: HKCR; Subkey: \"")
+                    .append(entryName)
+                    .append(
+                    "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
+                    .append(description)
+                    .append("\"; Flags: uninsdeletekey\r\n");
+            } else {
+                registryEntries.append(
+                    "Root: HKCU; Subkey: \"Software\\Classes\\")
+                    .append(entryName)
+                    .append(
+                    "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
+                    .append(description)
+                    .append("\"; Flags: uninsdeletekey\r\n");
+            }
+
+            if (icon != null && icon.exists()) {
+                if (isSystemWide) {
+                    // "Root: HKCR;
+                    // Subkey: \"MyProgramFile\\DefaultIcon\";
+                    // ValueType: string;
+                    // ValueName: \"\";
+                    // ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
+                    registryEntries.append("Root: HKCR; Subkey: \"")
+                            .append(entryName)
+                            .append("\\DefaultIcon\"; ValueType: string; " +
+                            "ValueName: \"\"; ValueData: \"{app}\\")
+                            .append(icon.getName())
+                            .append("\"\r\n");
+                } else {
+                    registryEntries.append(
+                            "Root: HKCU; Subkey: \"Software\\Classes\\")
+                            .append(entryName)
+                            .append("\\DefaultIcon\"; ValueType: string; " +
+                            "ValueName: \"\"; ValueData: \"{app}\\")
+                            .append(icon.getName())
+                            .append("\"\r\n");
+                }
+            }
+
+            if (isSystemWide) {
+                // "Root: HKCR;
+                // Subkey: \"MyProgramFile\\shell\\open\\command\";
+                // ValueType: string;
+                // ValueName: \"\";
+                // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
+                registryEntries.append("Root: HKCR; Subkey: \"")
+                        .append(entryName)
+                        .append("\\shell\\open\\command\"; ValueType: " +
+                        "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
+                        .append(APP_NAME.fetchFrom(p))
+                        .append("\"\" \"\"%1\"\"\"\r\n");
+            } else {
+                registryEntries.append(
+                        "Root: HKCU; Subkey: \"Software\\Classes\\")
+                        .append(entryName)
+                        .append("\\shell\\open\\command\"; ValueType: " +
+                        "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
+                        .append(APP_NAME.fetchFrom(p))
+                        .append("\"\" \"\"%1\"\"\"\r\n");
+            }
+        }
+        if (registryEntries.length() > 0) {
+            data.put("FILE_ASSOCIATIONS",
+                    "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" +
+                    registryEntries.toString());
+        } else {
+            data.put("FILE_ASSOCIATIONS", "");
+        }
+
+        // TODO - alternate template for JRE installer
+        String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ?
+                DEFAULT_EXE_PROJECT_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE;
+
+        Writer w = new BufferedWriter(new FileWriter(
+                getConfig_ExeProjectFile(p)));
+
+        String content = preprocessTextResource(
+                WinAppBundler.WIN_BUNDLER_PREFIX +
+                getConfig_ExeProjectFile(p).getName(),
+                getString("resource.inno-setup-project-file"),
+                iss, data, VERBOSE.fetchFrom(p),
+                DROP_IN_RESOURCES_ROOT.fetchFrom(p));
+        w.write(content);
+        w.close();
+        return true;
+    }
+
+    private final static String DEFAULT_INNO_SETUP_ICON =
+            "icon_inno_setup.bmp";
+
+    private boolean prepareProjectConfig(Map<String, ? super Object> p)
+            throws IOException {
+        prepareMainProjectFile(p);
+
+        //prepare installer icon
+        File iconTarget = getConfig_SmallInnoSetupIcon(p);
+        fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(),
+                getString("resource.setup-icon"),
+                DEFAULT_INNO_SETUP_ICON,
+                iconTarget,
+                VERBOSE.fetchFrom(p),
+                DROP_IN_RESOURCES_ROOT.fetchFrom(p));
+
+        fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX +
+                getConfig_Script(p).getName(),
+                getString("resource.post-install-script"),
+                (String) null,
+                getConfig_Script(p),
+                VERBOSE.fetchFrom(p),
+                DROP_IN_RESOURCES_ROOT.fetchFrom(p));
+        return true;
+    }
+
+    private File getConfig_SmallInnoSetupIcon(
+            Map<String, ? super Object> p) {
+        return new File(EXE_IMAGE_DIR.fetchFrom(p),
+                APP_NAME.fetchFrom(p) + "-setup-icon.bmp");
+    }
+
+    private File getConfig_ExeProjectFile(Map<String, ? super Object> p) {
+        return new File(EXE_IMAGE_DIR.fetchFrom(p),
+                APP_NAME.fetchFrom(p) + ".iss");
+    }
+
+
+    private File buildEXE(Map<String, ? super Object> p, File outdir)
+             throws IOException {
+        Log.verbose(MessageFormat.format(
+             getString("message.outputting-to-location"),
+             outdir.getAbsolutePath()));
+
+        outdir.mkdirs();
+
+        //run candle
+        ProcessBuilder pb = new ProcessBuilder(
+                TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p),
+                "/o"+outdir.getAbsolutePath(),
+                getConfig_ExeProjectFile(p).getAbsolutePath());
+        pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p));
+        IOUtils.exec(pb, VERBOSE.fetchFrom(p));
+
+        Log.info(MessageFormat.format(
+                getString("message.output-location"),
+                outdir.getAbsolutePath()));
+
+        // presume the result is the ".exe" file with the newest modified time
+        // not the best solution, but it is the most reliable
+        File result = null;
+        long lastModified = 0;
+        File[] list = outdir.listFiles();
+        if (list != null) {
+            for (File f : list) {
+                if (f.getName().endsWith(".exe") &&
+                        f.lastModified() > lastModified) {
+                    result = f;
+                    lastModified = f.lastModified();
+                }
+            }
+        }
+
+        return result;
+    }
+
+   public static void ensureByMutationFileIsRTF(File f) {
+        if (f == null || !f.isFile()) return;
+
+        try {
+            boolean existingLicenseIsRTF = false;
+
+            try (FileInputStream fin = new FileInputStream(f)) {
+                byte[] firstBits = new byte[7];
+
+                if (fin.read(firstBits) == firstBits.length) {
+                    String header = new String(firstBits);
+                    existingLicenseIsRTF = "{\\rtf1\\".equals(header);
+                }
+            }
+
+            if (!existingLicenseIsRTF) {
+                List<String> oldLicense = Files.readAllLines(f.toPath());
+                try (Writer w = Files.newBufferedWriter(
+                        f.toPath(), Charset.forName("Windows-1252"))) {
+                    w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
+                            + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
+                            + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
+                            + "\\slmult1\\lang9\\fs20 ");
+                    oldLicense.forEach(l -> {
+                        try {
+                            for (char c : l.toCharArray()) {
+                                if (c < 0x10) {
+                                    w.write("\\'0");
+                                    w.write(Integer.toHexString(c));
+                                } else if (c > 0xff) {
+                                    w.write("\\ud");
+                                    w.write(Integer.toString(c));
+                                    w.write("?");
+                                } else if ((c < 0x20) || (c >= 0x80) ||
+                                        (c == 0x5C) || (c == 0x7B) ||
+                                        (c == 0x7D)) {
+                                    w.write("\\'");
+                                    w.write(Integer.toHexString(c));
+                                } else {
+                                    w.write(c);
+                                }
+                            }
+                            if (l.length() < 1) {
+                                w.write("\\par");
+                            } else {
+                                w.write(" ");
+                            }
+                            w.write("\r\n");
+                        } catch (IOException e) {
+                            Log.verbose(e);
+                        }
+                    });
+                    w.write("}\r\n");
+                }
+            }
+        } catch (IOException e) {
+            Log.verbose(e);
+        }
+    }
+
+    private static String getString(String key)
+            throws MissingResourceException {
+        return I18N.getString(key);
+    }
+}
--- a/src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinMsiBundler.java	Mon Oct 08 18:10:31 2018 -0400
+++ b/src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinMsiBundler.java	Mon Oct 08 18:17:37 2018 -0400
@@ -742,14 +742,17 @@
 
         String wxs = Arguments.CREATE_JRE_INSTALLER.fetchFrom(params) ?
                 MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE;
+
         Writer w = new BufferedWriter(
                 new FileWriter(getConfig_ProjectFile(params)));
-        w.write(preprocessTextResource(
-                WinAppBundler.WIN_BUNDLER_PREFIX
-                + getConfig_ProjectFile(params).getName(),
+
+        String content = preprocessTextResource(
+                WinAppBundler.WIN_BUNDLER_PREFIX +
+                getConfig_ProjectFile(params).getName(),
                 I18N.getString("resource.wix-config-file"),
                 wxs, data, VERBOSE.fetchFrom(params),
-                DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
+                DROP_IN_RESOURCES_ROOT.fetchFrom(params));
+        w.write(content);
         w.close();
         return true;
     }
--- a/src/jdk.packager/windows/classes/module-info.java.extra	Mon Oct 08 18:10:31 2018 -0400
+++ b/src/jdk.packager/windows/classes/module-info.java.extra	Mon Oct 08 18:17:37 2018 -0400
@@ -26,5 +26,6 @@
 
 provides jdk.packager.internal.Bundler with
     jdk.packager.internal.windows.WinAppBundler,
+    jdk.packager.internal.windows.WinExeBundler,
     jdk.packager.internal.windows.WinMsiBundler;