src/jdk.jpackager/windows/classes/jdk/jpackager/internal/windows/WinExeBundler.java
branchJDK-8200758-branch
changeset 57017 1b08af362a30
parent 57015 5d2008115309
child 57020 a828547f7e50
equal deleted inserted replaced
57016:f63f13da91c0 57017:1b08af362a30
       
     1 /*
       
     2  * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.jpackager.internal.windows;
       
    27 
       
    28 import jdk.jpackager.internal.*;
       
    29 import jdk.jpackager.internal.ConfigException;
       
    30 import jdk.jpackager.internal.Arguments;
       
    31 import jdk.jpackager.internal.UnsupportedPlatformException;
       
    32 import jdk.jpackager.internal.resources.windows.WinResources;
       
    33 
       
    34 import java.io.*;
       
    35 import java.nio.charset.Charset;
       
    36 import java.nio.file.Files;
       
    37 import java.text.MessageFormat;
       
    38 import java.util.*;
       
    39 import java.util.regex.Matcher;
       
    40 import java.util.regex.Pattern;
       
    41 
       
    42 import static jdk.jpackager.internal.windows.WindowsBundlerParam.*;
       
    43 
       
    44 public class WinExeBundler extends AbstractBundler {
       
    45 
       
    46     private static final ResourceBundle I18N = ResourceBundle.getBundle(
       
    47             "jdk.jpackager.internal.resources.windows.WinExeBundler");
       
    48 
       
    49     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
       
    50             new WindowsBundlerParam<>(
       
    51             getString("param.app-bundler.name"),
       
    52             getString("param.app-bundler.description"),
       
    53             "win.app.bundler",
       
    54             WinAppBundler.class,
       
    55             params -> new WinAppBundler(),
       
    56             null);
       
    57 
       
    58     public static final BundlerParamInfo<File> CONFIG_ROOT =
       
    59             new WindowsBundlerParam<>(
       
    60             getString("param.config-root.name"),
       
    61             getString("param.config-root.description"),
       
    62             "configRoot",
       
    63             File.class,
       
    64             params -> {
       
    65                 File imagesRoot =
       
    66                         new File(BUILD_ROOT.fetchFrom(params), "windows");
       
    67                 imagesRoot.mkdirs();
       
    68                 return imagesRoot;
       
    69             },
       
    70             (s, p) -> null);
       
    71 
       
    72     public static final BundlerParamInfo<File> EXE_IMAGE_DIR =
       
    73             new WindowsBundlerParam<>(
       
    74             getString("param.image-dir.name"),
       
    75             getString("param.image-dir.description"),
       
    76             "win.exe.imageDir",
       
    77             File.class,
       
    78             params -> {
       
    79                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
       
    80                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
       
    81                 return new File(imagesRoot, "win-exe.image");
       
    82             },
       
    83             (s, p) -> null);
       
    84 
       
    85     public static final BundlerParamInfo<File> WIN_APP_IMAGE =
       
    86             new WindowsBundlerParam<>(
       
    87             getString("param.app-dir.name"),
       
    88             getString("param.app-dir.description"),
       
    89             "win.app.image",
       
    90             File.class,
       
    91             null,
       
    92             (s, p) -> null);
       
    93 
       
    94 
       
    95     public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE  =
       
    96             new StandardBundlerParam<>(
       
    97             getString("param.system-wide.name"),
       
    98             getString("param.system-wide.description"),
       
    99             Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
       
   100             Boolean.class,
       
   101             params -> true, // default to system wide
       
   102             (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
       
   103                     : Boolean.valueOf(s)
       
   104             );
       
   105     public static final StandardBundlerParam<String> PRODUCT_VERSION =
       
   106             new StandardBundlerParam<>(
       
   107                     getString("param.product-version.name"),
       
   108                     getString("param.product-version.description"),
       
   109                     "win.msi.productVersion",
       
   110                     String.class,
       
   111                     VERSION::fetchFrom,
       
   112                     (s, p) -> s
       
   113             );
       
   114 
       
   115     public static final StandardBundlerParam<Boolean> MENU_HINT =
       
   116         new WindowsBundlerParam<>(
       
   117                 getString("param.menu-shortcut-hint.name"),
       
   118                 getString("param.menu-shortcut-hint.description"),
       
   119                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
       
   120                 Boolean.class,
       
   121                 params -> false,
       
   122                 (s, p) -> (s == null ||
       
   123                         "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
       
   124         );
       
   125 
       
   126     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
       
   127         new WindowsBundlerParam<>(
       
   128                 getString("param.desktop-shortcut-hint.name"),
       
   129                 getString("param.desktop-shortcut-hint.description"),
       
   130                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
       
   131                 Boolean.class,
       
   132                 params -> false,
       
   133                 (s, p) -> (s == null ||
       
   134                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
       
   135         );
       
   136 
       
   137 
       
   138 
       
   139     private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss";
       
   140     private final static String DEFAULT_JRE_EXE_TEMPLATE = "template.jre.iss";
       
   141     private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe";
       
   142 
       
   143     public static final BundlerParamInfo<String>
       
   144             TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>(
       
   145             getString("param.iscc-path.name"),
       
   146             getString("param.iscc-path.description"),
       
   147             "win.exe.iscc.exe",
       
   148             String.class,
       
   149             params -> {
       
   150                 for (String dirString : (System.getenv("PATH")
       
   151                         + ";C:\\Program Files (x86)\\Inno Setup 5;"
       
   152                         + "C:\\Program Files\\Inno Setup 5").split(";")) {
       
   153                     File f = new File(dirString.replace("\"", ""),
       
   154                             TOOL_INNO_SETUP_COMPILER);
       
   155                     if (f.isFile()) {
       
   156                         return f.toString();
       
   157                     }
       
   158                 }
       
   159                 return null;
       
   160             },
       
   161             null);
       
   162 
       
   163     public WinExeBundler() {
       
   164         super();
       
   165         baseResourceLoader = WinResources.class;
       
   166     }
       
   167 
       
   168     @Override
       
   169     public String getName() {
       
   170         return getString("bundler.name");
       
   171     }
       
   172 
       
   173     @Override
       
   174     public String getDescription() {
       
   175         return getString("bundler.description");
       
   176     }
       
   177 
       
   178     @Override
       
   179     public String getID() {
       
   180         return "exe";
       
   181     }
       
   182 
       
   183     @Override
       
   184     public String getBundleType() {
       
   185         return "INSTALLER";
       
   186     }
       
   187 
       
   188     @Override
       
   189     public Collection<BundlerParamInfo<?>> getBundleParameters() {
       
   190         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
       
   191         results.addAll(WinAppBundler.getAppBundleParameters());
       
   192         results.addAll(getExeBundleParameters());
       
   193         return results;
       
   194     }
       
   195 
       
   196     public static Collection<BundlerParamInfo<?>> getExeBundleParameters() {
       
   197         return Arrays.asList(
       
   198                 DESCRIPTION,
       
   199                 COPYRIGHT,
       
   200                 LICENSE_FILE,
       
   201                 MENU_GROUP,
       
   202                 MENU_HINT,
       
   203                 SHORTCUT_HINT,
       
   204                 EXE_SYSTEM_WIDE,
       
   205                 TITLE,
       
   206                 VENDOR,
       
   207                 INSTALLDIR_CHOOSER
       
   208         );
       
   209     }
       
   210 
       
   211     @Override
       
   212     public File execute(
       
   213             Map<String, ? super Object> p, File outputParentDir) {
       
   214         return bundle(p, outputParentDir);
       
   215     }
       
   216 
       
   217     @Override
       
   218     public boolean supported() {
       
   219         return (Platform.getPlatform() == Platform.WINDOWS);
       
   220     }
       
   221 
       
   222     static class VersionExtractor extends PrintStream {
       
   223         double version = 0f;
       
   224 
       
   225         public VersionExtractor() {
       
   226             super(new ByteArrayOutputStream());
       
   227         }
       
   228 
       
   229         double getVersion() {
       
   230             if (version == 0f) {
       
   231                 String content =
       
   232                         new String(((ByteArrayOutputStream) out).toByteArray());
       
   233                 Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)");
       
   234                 Matcher matcher = pattern.matcher(content);
       
   235                 if (matcher.find()) {
       
   236                     String v = matcher.group(1);
       
   237                     version = Double.parseDouble(v);
       
   238                 }
       
   239             }
       
   240             return version;
       
   241         }
       
   242     }
       
   243 
       
   244     private static double findToolVersion(String toolName) {
       
   245         try {
       
   246             if (toolName == null || "".equals(toolName)) return 0f;
       
   247 
       
   248             ProcessBuilder pb = new ProcessBuilder(
       
   249                     toolName,
       
   250                     "/?");
       
   251             VersionExtractor ve = new VersionExtractor();
       
   252             IOUtils.exec(pb, Log.isDebug(), true, ve);
       
   253             // not interested in the output
       
   254             double version = ve.getVersion();
       
   255             Log.verbose(MessageFormat.format(
       
   256                     getString("message.tool-version"), toolName, version));
       
   257             return version;
       
   258         } catch (Exception e) {
       
   259             if (Log.isDebug()) {
       
   260                 Log.verbose(e);
       
   261             }
       
   262             return 0f;
       
   263         }
       
   264     }
       
   265 
       
   266     @Override
       
   267     public boolean validate(Map<String, ? super Object> p)
       
   268             throws UnsupportedPlatformException, ConfigException {
       
   269         try {
       
   270             if (p == null) throw new ConfigException(
       
   271                       getString("error.parameters-null"),
       
   272                       getString("error.parameters-null.advice"));
       
   273 
       
   274             // run basic validation to ensure requirements are met
       
   275             // we are not interested in return code, only possible exception
       
   276             APP_BUNDLER.fetchFrom(p).validate(p);
       
   277 
       
   278             // make sure some key values don't have newlines
       
   279             for (BundlerParamInfo<String> pi : Arrays.asList(
       
   280                     APP_NAME,
       
   281                     COPYRIGHT,
       
   282                     DESCRIPTION,
       
   283                     MENU_GROUP,
       
   284                     TITLE,
       
   285                     VENDOR,
       
   286                     VERSION)
       
   287             ) {
       
   288                 String v = pi.fetchFrom(p);
       
   289                 if (v.contains("\n") | v.contains("\r")) {
       
   290                     throw new ConfigException("Parmeter '" + pi.getID() +
       
   291                             "' cannot contain a newline.",
       
   292                             " Change the value of '" + pi.getID() +
       
   293                             " so that it does not contain any newlines");
       
   294                 }
       
   295             }
       
   296 
       
   297             // exe bundlers trim the copyright to 100 characters,
       
   298             // tell them this will happen
       
   299             if (COPYRIGHT.fetchFrom(p).length() > 100) {
       
   300                 throw new ConfigException(
       
   301                         getString("error.copyright-is-too-long"),
       
   302                         getString("error.copyright-is-too-long.advice"));
       
   303             }
       
   304 
       
   305             double innoVersion = findToolVersion(
       
   306                     TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p));
       
   307 
       
   308             //Inno Setup 5+ is required
       
   309             double minVersion = 5.0f;
       
   310 
       
   311             if (innoVersion < minVersion) {
       
   312                 Log.info(MessageFormat.format(
       
   313                         getString("message.tool-wrong-version"),
       
   314                         TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion));
       
   315                 throw new ConfigException(
       
   316                         getString("error.iscc-not-found"),
       
   317                         getString("error.iscc-not-found.advice"));
       
   318             }
       
   319 
       
   320             /********* validate bundle parameters *************/
       
   321 
       
   322             // only one mime type per association, at least one file extension
       
   323             List<Map<String, ? super Object>> associations =
       
   324                     FILE_ASSOCIATIONS.fetchFrom(p);
       
   325             if (associations != null) {
       
   326                 for (int i = 0; i < associations.size(); i++) {
       
   327                     Map<String, ? super Object> assoc = associations.get(i);
       
   328                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
       
   329                     if (mimes.size() > 1) {
       
   330                         throw new ConfigException(MessageFormat.format(
       
   331                                 getString("error.too-many-content-"
       
   332                                 + "types-for-file-association"), i),
       
   333                                 getString("error.too-many-content-"
       
   334                                 + "types-for-file-association.advice"));
       
   335                     }
       
   336                 }
       
   337             }
       
   338 
       
   339             // validate license file, if used, exists in the proper place
       
   340             if (p.containsKey(LICENSE_FILE.getID())) {
       
   341                 List<RelativeFileSet> appResourcesList =
       
   342                         APP_RESOURCES_LIST.fetchFrom(p);
       
   343                 for (String license : LICENSE_FILE.fetchFrom(p)) {
       
   344                     boolean found = false;
       
   345                     for (RelativeFileSet appResources : appResourcesList) {
       
   346                         found = found || appResources.contains(license);
       
   347                     }
       
   348                     if (!found) {
       
   349                         throw new ConfigException(
       
   350                             MessageFormat.format(getString(
       
   351                                "error.license-missing"), license),
       
   352                             MessageFormat.format(getString(
       
   353                                "error.license-missing.advice"), license));
       
   354                     }
       
   355                 }
       
   356             }
       
   357 
       
   358             return true;
       
   359         } catch (RuntimeException re) {
       
   360             if (re.getCause() instanceof ConfigException) {
       
   361                 throw (ConfigException) re.getCause();
       
   362             } else {
       
   363                 throw new ConfigException(re);
       
   364             }
       
   365         }
       
   366     }
       
   367 
       
   368     private boolean prepareProto(Map<String, ? super Object> p)
       
   369                 throws IOException {
       
   370         File appImage = StandardBundlerParam.getPredefinedAppImage(p);
       
   371         File appDir = null;
       
   372 
       
   373         // we either have an application image or need to build one
       
   374         if (appImage != null) {
       
   375             appDir = new File(
       
   376                     EXE_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p));
       
   377             // copy everything from appImage dir into appDir/name
       
   378             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
       
   379         } else {
       
   380             appDir = APP_BUNDLER.fetchFrom(p).doBundle(p,
       
   381                     EXE_IMAGE_DIR.fetchFrom(p), true);
       
   382         }
       
   383 
       
   384         if (appDir == null) {
       
   385             return false;
       
   386         }
       
   387 
       
   388         p.put(WIN_APP_IMAGE.getID(), appDir);
       
   389 
       
   390         List<String> licenseFiles = LICENSE_FILE.fetchFrom(p);
       
   391         if (licenseFiles != null) {
       
   392             // need to copy license file to the root of win.app.image
       
   393             outerLoop:
       
   394             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(p)) {
       
   395                 for (String s : licenseFiles) {
       
   396                     if (rfs.contains(s)) {
       
   397                         File lfile = new File(rfs.getBaseDirectory(), s);
       
   398                         File destFile =
       
   399                             new File(appDir.getParentFile(), lfile.getName());
       
   400                         IOUtils.copyFile(lfile, destFile);
       
   401                         ensureByMutationFileIsRTF(destFile);
       
   402                         break outerLoop;
       
   403                     }
       
   404                 }
       
   405             }
       
   406         }
       
   407 
       
   408         // copy file association icons
       
   409         List<Map<String, ? super Object>> fileAssociations =
       
   410                 FILE_ASSOCIATIONS.fetchFrom(p);
       
   411 
       
   412         for (Map<String, ? super Object> fa : fileAssociations) {
       
   413             File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
       
   414             if (icon == null) {
       
   415                 continue;
       
   416             }
       
   417 
       
   418             File faIconFile = new File(appDir, icon.getName());
       
   419 
       
   420             if (icon.exists()) {
       
   421                 try {
       
   422                     IOUtils.copyFile(icon, faIconFile);
       
   423                 } catch (IOException e) {
       
   424                     e.printStackTrace();
       
   425                 }
       
   426             }
       
   427         }
       
   428 
       
   429         return true;
       
   430     }
       
   431 
       
   432     public File bundle(Map<String, ? super Object> p, File outdir) {
       
   433         if (!outdir.isDirectory() && !outdir.mkdirs()) {
       
   434             throw new RuntimeException(MessageFormat.format(
       
   435                     getString("error.cannot-create-output-dir"),
       
   436                     outdir.getAbsolutePath()));
       
   437         }
       
   438         if (!outdir.canWrite()) {
       
   439             throw new RuntimeException(MessageFormat.format(
       
   440                     getString("error.cannot-write-to-output-dir"),
       
   441                     outdir.getAbsolutePath()));
       
   442         }
       
   443 
       
   444         if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) {
       
   445             Log.info(MessageFormat.format(
       
   446                     getString("message.potential.windows.defender.issue"),
       
   447                     WindowsDefender.getUserTempDirectory()));
       
   448         }
       
   449 
       
   450         // validate we have valid tools before continuing
       
   451         String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p);
       
   452         if (iscc == null || !new File(iscc).isFile()) {
       
   453             Log.info(getString("error.iscc-not-found"));
       
   454             Log.info(MessageFormat.format(
       
   455                     getString("message.iscc-file-string"), iscc));
       
   456             return null;
       
   457         }
       
   458 
       
   459         File imageDir = EXE_IMAGE_DIR.fetchFrom(p);
       
   460         try {
       
   461             imageDir.mkdirs();
       
   462 
       
   463             boolean menuShortcut = MENU_HINT.fetchFrom(p);
       
   464             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
       
   465             if (!menuShortcut && !desktopShortcut) {
       
   466                 // both can not be false - user will not find the app
       
   467                 Log.verbose(getString("message.one-shortcut-required"));
       
   468                 p.put(MENU_HINT.getID(), true);
       
   469             }
       
   470 
       
   471             if (prepareProto(p) && prepareProjectConfig(p)) {
       
   472                 File configScript = getConfig_Script(p);
       
   473                 if (configScript.exists()) {
       
   474                     Log.info(MessageFormat.format(
       
   475                             getString("message.running-wsh-script"),
       
   476                             configScript.getAbsolutePath()));
       
   477                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
       
   478                 }
       
   479                 return buildEXE(p, outdir);
       
   480             }
       
   481             return null;
       
   482         } catch (IOException ex) {
       
   483             ex.printStackTrace();
       
   484             return null;
       
   485         } finally {
       
   486             try {
       
   487                 if (imageDir != null &&
       
   488                         PREDEFINED_APP_IMAGE.fetchFrom(p) == null &&
       
   489                         (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ||
       
   490                         !Arguments.CREATE_JRE_INSTALLER.fetchFrom(p)) &&
       
   491                         !Log.isDebug()) {
       
   492                     IOUtils.deleteRecursive(imageDir);
       
   493                 } else if (imageDir != null) {
       
   494                     Log.info(MessageFormat.format(
       
   495                             I18N.getString("message.debug-working-directory"),
       
   496                             imageDir.getAbsolutePath()));
       
   497                 }
       
   498             } catch (IOException ex) {
       
   499                 // noinspection ReturnInsideFinallyBlock
       
   500                 Log.debug(ex.getMessage());
       
   501                 return null;
       
   502             }
       
   503         }
       
   504     }
       
   505 
       
   506     // name of post-image script
       
   507     private File getConfig_Script(Map<String, ? super Object> p) {
       
   508         return new File(EXE_IMAGE_DIR.fetchFrom(p),
       
   509                 APP_NAME.fetchFrom(p) + "-post-image.wsf");
       
   510     }
       
   511 
       
   512     private String getAppIdentifier(Map<String, ? super Object> p) {
       
   513         String nm = IDENTIFIER.fetchFrom(p);
       
   514 
       
   515         if (nm == null) {
       
   516             nm = APP_NAME.fetchFrom(p);
       
   517         }
       
   518 
       
   519         // limitation of innosetup
       
   520         if (nm.length() > 126) {
       
   521             Log.info(getString("message-truncating-id"));
       
   522             nm = nm.substring(0, 126);
       
   523         }
       
   524 
       
   525         return nm;
       
   526     }
       
   527 
       
   528 
       
   529     private String getLicenseFile(Map<String, ? super Object> p) {
       
   530         List<String> licenseFiles = LICENSE_FILE.fetchFrom(p);
       
   531         if (licenseFiles == null || licenseFiles.isEmpty()) {
       
   532             return "";
       
   533         } else {
       
   534             return licenseFiles.get(0);
       
   535         }
       
   536     }
       
   537 
       
   538     void validateValueAndPut(Map<String, String> data, String key,
       
   539                 BundlerParamInfo<String> param,
       
   540                 Map<String, ? super Object> p) throws IOException {
       
   541         String value = param.fetchFrom(p);
       
   542         if (value.contains("\r") || value.contains("\n")) {
       
   543             throw new IOException("Configuration Parameter " +
       
   544                      param.getID() + " cannot contain multiple lines of text");
       
   545         }
       
   546         data.put(key, innosetupEscape(value));
       
   547     }
       
   548 
       
   549     private String innosetupEscape(String value) {
       
   550         if (value.contains("\"") || !value.trim().equals(value)) {
       
   551             value = "\"" + value.replace("\"", "\"\"") + "\"";
       
   552         }
       
   553         return value;
       
   554     }
       
   555 
       
   556     boolean prepareMainProjectFile(Map<String, ? super Object> p)
       
   557             throws IOException {
       
   558         Map<String, String> data = new HashMap<>();
       
   559         data.put("PRODUCT_APP_IDENTIFIER",
       
   560                 innosetupEscape(getAppIdentifier(p)));
       
   561 
       
   562 
       
   563         validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p);
       
   564         validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p);
       
   565         validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p);
       
   566         validateValueAndPut(data, "INSTALLER_FILE_NAME",
       
   567                 INSTALLER_FILE_NAME, p);
       
   568 
       
   569         data.put("LAUNCHER_NAME",
       
   570                 innosetupEscape(WinAppBundler.getAppName(p)));
       
   571 
       
   572         data.put("APPLICATION_LAUNCHER_FILENAME",
       
   573                 innosetupEscape(WinAppBundler.getLauncherName(p)));
       
   574 
       
   575         data.put("APPLICATION_DESKTOP_SHORTCUT",
       
   576                 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
       
   577         data.put("APPLICATION_MENU_SHORTCUT",
       
   578                 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
       
   579         validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p);
       
   580         validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p);
       
   581         validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p);
       
   582 
       
   583         data.put("APPLICATION_LICENSE_FILE",
       
   584                 innosetupEscape(getLicenseFile(p)));
       
   585         data.put("DISABLE_DIR_PAGE",
       
   586                 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes");
       
   587 
       
   588         Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p);
       
   589 
       
   590         if (isSystemWide) {
       
   591             data.put("APPLICATION_INSTALL_ROOT", "{pf}");
       
   592             data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
       
   593         } else {
       
   594             data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
       
   595             data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
       
   596         }
       
   597 
       
   598         if (BIT_ARCH_64.fetchFrom(p)) {
       
   599             data.put("ARCHITECTURE_BIT_MODE", "x64");
       
   600         } else {
       
   601             data.put("ARCHITECTURE_BIT_MODE", "");
       
   602         }
       
   603         validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p);
       
   604 
       
   605         validateValueAndPut(data, "APPLICATION_DESCRIPTION",
       
   606                 DESCRIPTION, p);
       
   607 
       
   608         data.put("APPLICATION_SERVICE", "returnFalse");
       
   609         data.put("APPLICATION_NOT_SERVICE", "returnFalse");
       
   610         data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse");
       
   611         data.put("START_ON_INSTALL", "");
       
   612         data.put("STOP_ON_UNINSTALL", "");
       
   613         data.put("RUN_AT_STARTUP", "");
       
   614 
       
   615         StringBuilder secondaryLaunchersCfg = new StringBuilder();
       
   616         for (Map<String, ? super Object>
       
   617                 launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) {
       
   618             String application_name = APP_NAME.fetchFrom(launcher);
       
   619             if (MENU_HINT.fetchFrom(launcher)) {
       
   620                 // Name: "{group}\APPLICATION_NAME";
       
   621                 // Filename: "{app}\APPLICATION_NAME.exe";
       
   622                 // IconFilename: "{app}\APPLICATION_NAME.ico"
       
   623                 secondaryLaunchersCfg.append("Name: \"{group}\\");
       
   624                 secondaryLaunchersCfg.append(application_name);
       
   625                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
       
   626                 secondaryLaunchersCfg.append(application_name);
       
   627                 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
       
   628                 secondaryLaunchersCfg.append(application_name);
       
   629                 secondaryLaunchersCfg.append(".ico\"\r\n");
       
   630             }
       
   631             if (SHORTCUT_HINT.fetchFrom(launcher)) {
       
   632                 // Name: "{commondesktop}\APPLICATION_NAME";
       
   633                 // Filename: "{app}\APPLICATION_NAME.exe";
       
   634                 // IconFilename: "{app}\APPLICATION_NAME.ico"
       
   635                 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\");
       
   636                 secondaryLaunchersCfg.append(application_name);
       
   637                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
       
   638                 secondaryLaunchersCfg.append(application_name);
       
   639                 secondaryLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
       
   640                 secondaryLaunchersCfg.append(application_name);
       
   641                 secondaryLaunchersCfg.append(".ico\"\r\n");
       
   642             }
       
   643         }
       
   644         data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString());
       
   645 
       
   646         StringBuilder registryEntries = new StringBuilder();
       
   647         String regName = APP_REGISTRY_NAME.fetchFrom(p);
       
   648         List<Map<String, ? super Object>> fetchFrom =
       
   649                 FILE_ASSOCIATIONS.fetchFrom(p);
       
   650         for (int i = 0; i < fetchFrom.size(); i++) {
       
   651             Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
       
   652             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
       
   653             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
       
   654 
       
   655             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
       
   656             String entryName = regName + "File";
       
   657             if (i > 0) {
       
   658                 entryName += "." + i;
       
   659             }
       
   660 
       
   661             if (extensions == null) {
       
   662                 Log.info(getString(
       
   663                         "message.creating-association-with-null-extension"));
       
   664             } else {
       
   665                 for (String ext : extensions) {
       
   666                     if (isSystemWide) {
       
   667                         // "Root: HKCR; Subkey: \".myp\";
       
   668                         // ValueType: string; ValueName: \"\";
       
   669                         // ValueData: \"MyProgramFile\";
       
   670                         // Flags: uninsdeletevalue"
       
   671                         registryEntries.append("Root: HKCR; Subkey: \".")
       
   672                                 .append(ext)
       
   673                                 .append("\"; ValueType: string;"
       
   674                                 + " ValueName: \"\"; ValueData: \"")
       
   675                                 .append(entryName)
       
   676                                 .append("\"; Flags: uninsdeletevalue\r\n");
       
   677                     } else {
       
   678                         registryEntries.append(
       
   679                                 "Root: HKCU; Subkey: \"Software\\Classes\\.")
       
   680                                 .append(ext)
       
   681                                 .append("\"; ValueType: string;"
       
   682                                 + " ValueName: \"\"; ValueData: \"")
       
   683                                 .append(entryName)
       
   684                                 .append("\"; Flags: uninsdeletevalue\r\n");
       
   685                     }
       
   686                 }
       
   687             }
       
   688 
       
   689             if (extensions != null && !extensions.isEmpty()) {
       
   690                 String ext = extensions.get(0);
       
   691                 List<String> mimeTypes =
       
   692                         FA_CONTENT_TYPE.fetchFrom(fileAssociation);
       
   693                 for (String mime : mimeTypes) {
       
   694                     if (isSystemWide) {
       
   695                         // "Root: HKCR;
       
   696                         // Subkey: HKCR\\Mime\\Database\\
       
   697                         //         Content Type\\application/chaos;
       
   698                         // ValueType: string;
       
   699                         // ValueName: Extension;
       
   700                         // ValueData: .chaos;
       
   701                         // Flags: uninsdeletevalue"
       
   702                         registryEntries.append("Root: HKCR; Subkey: " +
       
   703                                  "\"Mime\\Database\\Content Type\\")
       
   704                             .append(mime)
       
   705                             .append("\"; ValueType: string; ValueName: " +
       
   706                                  "\"Extension\"; ValueData: \".")
       
   707                             .append(ext)
       
   708                             .append("\"; Flags: uninsdeletevalue\r\n");
       
   709                     } else {
       
   710                         registryEntries.append(
       
   711                                 "Root: HKCU; Subkey: \"Software\\" +
       
   712                                 "Classes\\Mime\\Database\\Content Type\\")
       
   713                                 .append(mime)
       
   714                                 .append("\"; ValueType: string; " +
       
   715                                 "ValueName: \"Extension\"; ValueData: \".")
       
   716                                 .append(ext)
       
   717                                 .append("\"; Flags: uninsdeletevalue\r\n");
       
   718                     }
       
   719                 }
       
   720             }
       
   721 
       
   722             if (isSystemWide) {
       
   723                 // "Root: HKCR;
       
   724                 // Subkey: \"MyProgramFile\";
       
   725                 // ValueType: string;
       
   726                 // ValueName: \"\";
       
   727                 // ValueData: \"My Program File\";
       
   728                 // Flags: uninsdeletekey"
       
   729                 registryEntries.append("Root: HKCR; Subkey: \"")
       
   730                     .append(entryName)
       
   731                     .append(
       
   732                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
       
   733                     .append(removeQuotes(description))
       
   734                     .append("\"; Flags: uninsdeletekey\r\n");
       
   735             } else {
       
   736                 registryEntries.append(
       
   737                     "Root: HKCU; Subkey: \"Software\\Classes\\")
       
   738                     .append(entryName)
       
   739                     .append(
       
   740                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
       
   741                     .append(removeQuotes(description))
       
   742                     .append("\"; Flags: uninsdeletekey\r\n");
       
   743             }
       
   744 
       
   745             if (icon != null && icon.exists()) {
       
   746                 if (isSystemWide) {
       
   747                     // "Root: HKCR;
       
   748                     // Subkey: \"MyProgramFile\\DefaultIcon\";
       
   749                     // ValueType: string;
       
   750                     // ValueName: \"\";
       
   751                     // ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
       
   752                     registryEntries.append("Root: HKCR; Subkey: \"")
       
   753                             .append(entryName)
       
   754                             .append("\\DefaultIcon\"; ValueType: string; " +
       
   755                             "ValueName: \"\"; ValueData: \"{app}\\")
       
   756                             .append(icon.getName())
       
   757                             .append("\"\r\n");
       
   758                 } else {
       
   759                     registryEntries.append(
       
   760                             "Root: HKCU; Subkey: \"Software\\Classes\\")
       
   761                             .append(entryName)
       
   762                             .append("\\DefaultIcon\"; ValueType: string; " +
       
   763                             "ValueName: \"\"; ValueData: \"{app}\\")
       
   764                             .append(icon.getName())
       
   765                             .append("\"\r\n");
       
   766                 }
       
   767             }
       
   768 
       
   769             if (isSystemWide) {
       
   770                 // "Root: HKCR;
       
   771                 // Subkey: \"MyProgramFile\\shell\\open\\command\";
       
   772                 // ValueType: string;
       
   773                 // ValueName: \"\";
       
   774                 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
       
   775                 registryEntries.append("Root: HKCR; Subkey: \"")
       
   776                         .append(entryName)
       
   777                         .append("\\shell\\open\\command\"; ValueType: " +
       
   778                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
       
   779                         .append(APP_NAME.fetchFrom(p))
       
   780                         .append("\"\" \"\"%1\"\"\"\r\n");
       
   781             } else {
       
   782                 registryEntries.append(
       
   783                         "Root: HKCU; Subkey: \"Software\\Classes\\")
       
   784                         .append(entryName)
       
   785                         .append("\\shell\\open\\command\"; ValueType: " +
       
   786                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
       
   787                         .append(APP_NAME.fetchFrom(p))
       
   788                         .append("\"\" \"\"%1\"\"\"\r\n");
       
   789             }
       
   790         }
       
   791         if (registryEntries.length() > 0) {
       
   792             data.put("FILE_ASSOCIATIONS",
       
   793                     "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" +
       
   794                     registryEntries.toString());
       
   795         } else {
       
   796             data.put("FILE_ASSOCIATIONS", "");
       
   797         }
       
   798 
       
   799         // TODO - alternate template for JRE installer
       
   800         String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ?
       
   801                 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE;
       
   802 
       
   803         Writer w = new BufferedWriter(new FileWriter(
       
   804                 getConfig_ExeProjectFile(p)));
       
   805 
       
   806         String content = preprocessTextResource(
       
   807                 WinAppBundler.WIN_BUNDLER_PREFIX +
       
   808                 getConfig_ExeProjectFile(p).getName(),
       
   809                 getString("resource.inno-setup-project-file"),
       
   810                 iss, data, VERBOSE.fetchFrom(p),
       
   811                 DROP_IN_RESOURCES_ROOT.fetchFrom(p));
       
   812         w.write(content);
       
   813         w.close();
       
   814         return true;
       
   815     }
       
   816 
       
   817     private final static String removeQuotes(String s) {
       
   818         if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) {
       
   819             // special case for '"XXX"' return 'XXX' not '-XXX-'
       
   820             // note '"' and '""' are excluded from this special case
       
   821             s = s.substring(1, s.length() - 1);
       
   822         }
       
   823         // if there interior double quotes replace them with '-'
       
   824         return s.replaceAll("\"", "-");
       
   825     }
       
   826         
       
   827     private final static String DEFAULT_INNO_SETUP_ICON =
       
   828             "icon_inno_setup.bmp";
       
   829 
       
   830     private boolean prepareProjectConfig(Map<String, ? super Object> p)
       
   831             throws IOException {
       
   832         prepareMainProjectFile(p);
       
   833 
       
   834         // prepare installer icon
       
   835         File iconTarget = getConfig_SmallInnoSetupIcon(p);
       
   836         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(),
       
   837                 getString("resource.setup-icon"),
       
   838                 DEFAULT_INNO_SETUP_ICON,
       
   839                 iconTarget,
       
   840                 VERBOSE.fetchFrom(p),
       
   841                 DROP_IN_RESOURCES_ROOT.fetchFrom(p));
       
   842 
       
   843         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX +
       
   844                 getConfig_Script(p).getName(),
       
   845                 getString("resource.post-install-script"),
       
   846                 (String) null,
       
   847                 getConfig_Script(p),
       
   848                 VERBOSE.fetchFrom(p),
       
   849                 DROP_IN_RESOURCES_ROOT.fetchFrom(p));
       
   850         return true;
       
   851     }
       
   852 
       
   853     private File getConfig_SmallInnoSetupIcon(
       
   854             Map<String, ? super Object> p) {
       
   855         return new File(EXE_IMAGE_DIR.fetchFrom(p),
       
   856                 APP_NAME.fetchFrom(p) + "-setup-icon.bmp");
       
   857     }
       
   858 
       
   859     private File getConfig_ExeProjectFile(Map<String, ? super Object> p) {
       
   860         return new File(EXE_IMAGE_DIR.fetchFrom(p),
       
   861                 APP_NAME.fetchFrom(p) + ".iss");
       
   862     }
       
   863 
       
   864 
       
   865     private File buildEXE(Map<String, ? super Object> p, File outdir)
       
   866              throws IOException {
       
   867         Log.verbose(MessageFormat.format(
       
   868              getString("message.outputting-to-location"),
       
   869              outdir.getAbsolutePath()));
       
   870 
       
   871         outdir.mkdirs();
       
   872 
       
   873         // run Inno Setup
       
   874         ProcessBuilder pb = new ProcessBuilder(
       
   875                 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p),
       
   876                 "/q",    // turn off inno setup output
       
   877                 "/o"+outdir.getAbsolutePath(),
       
   878                 getConfig_ExeProjectFile(p).getAbsolutePath());
       
   879         pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p));
       
   880         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
       
   881 
       
   882         Log.info(MessageFormat.format(
       
   883                 getString("message.output-location"),
       
   884                 outdir.getAbsolutePath()));
       
   885 
       
   886         // presume the result is the ".exe" file with the newest modified time
       
   887         // not the best solution, but it is the most reliable
       
   888         File result = null;
       
   889         long lastModified = 0;
       
   890         File[] list = outdir.listFiles();
       
   891         if (list != null) {
       
   892             for (File f : list) {
       
   893                 if (f.getName().endsWith(".exe") &&
       
   894                         f.lastModified() > lastModified) {
       
   895                     result = f;
       
   896                     lastModified = f.lastModified();
       
   897                 }
       
   898             }
       
   899         }
       
   900 
       
   901         return result;
       
   902     }
       
   903 
       
   904    public static void ensureByMutationFileIsRTF(File f) {
       
   905         if (f == null || !f.isFile()) return;
       
   906 
       
   907         try {
       
   908             boolean existingLicenseIsRTF = false;
       
   909 
       
   910             try (FileInputStream fin = new FileInputStream(f)) {
       
   911                 byte[] firstBits = new byte[7];
       
   912 
       
   913                 if (fin.read(firstBits) == firstBits.length) {
       
   914                     String header = new String(firstBits);
       
   915                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
       
   916                 }
       
   917             }
       
   918 
       
   919             if (!existingLicenseIsRTF) {
       
   920                 List<String> oldLicense = Files.readAllLines(f.toPath());
       
   921                 try (Writer w = Files.newBufferedWriter(
       
   922                         f.toPath(), Charset.forName("Windows-1252"))) {
       
   923                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
       
   924                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
       
   925                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
       
   926                             + "\\slmult1\\lang9\\fs20 ");
       
   927                     oldLicense.forEach(l -> {
       
   928                         try {
       
   929                             for (char c : l.toCharArray()) {
       
   930                                 if (c < 0x10) {
       
   931                                     w.write("\\'0");
       
   932                                     w.write(Integer.toHexString(c));
       
   933                                 } else if (c > 0xff) {
       
   934                                     w.write("\\ud");
       
   935                                     w.write(Integer.toString(c));
       
   936                                     w.write("?");
       
   937                                 } else if ((c < 0x20) || (c >= 0x80) ||
       
   938                                         (c == 0x5C) || (c == 0x7B) ||
       
   939                                         (c == 0x7D)) {
       
   940                                     w.write("\\'");
       
   941                                     w.write(Integer.toHexString(c));
       
   942                                 } else {
       
   943                                     w.write(c);
       
   944                                 }
       
   945                             }
       
   946                             if (l.length() < 1) {
       
   947                                 w.write("\\par");
       
   948                             } else {
       
   949                                 w.write(" ");
       
   950                             }
       
   951                             w.write("\r\n");
       
   952                         } catch (IOException e) {
       
   953                             Log.verbose(e);
       
   954                         }
       
   955                     });
       
   956                     w.write("}\r\n");
       
   957                 }
       
   958             }
       
   959         } catch (IOException e) {
       
   960             Log.verbose(e);
       
   961         }
       
   962     }
       
   963 
       
   964     private static String getString(String key)
       
   965             throws MissingResourceException {
       
   966         return I18N.getString(key);
       
   967     }
       
   968 }