1 /*
   2  * Copyright (c) 2012, 2017, 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 com.oracle.tools.packager.windows;
  27 
  28 import com.oracle.tools.packager.*;
  29 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  30 
  31 import java.io.*;
  32 import java.text.MessageFormat;
  33 import java.util.*;
  34 import java.util.regex.Matcher;
  35 import java.util.regex.Pattern;
  36 
  37 import static com.oracle.tools.packager.StandardBundlerParam.SECONDARY_LAUNCHERS;
  38 import static com.oracle.tools.packager.StandardBundlerParam.SERVICE_HINT;
  39 import static com.oracle.tools.packager.StandardBundlerParam.VERBOSE;
  40 import static com.oracle.tools.packager.windows.WindowsBundlerParam.*;
  41 import jdk.packager.internal.legacy.windows.WindowsDefender;
  42 
  43 public class WinExeBundler extends AbstractBundler {
  44 
  45     private static final ResourceBundle I18N =
  46             ResourceBundle.getBundle(WinExeBundler.class.getName());
  47 
  48     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
  49             I18N.getString("param.app-bundler.name"),
  50             I18N.getString("param.app-bundler.description"),
  51             "win.app.bundler",
  52             WinAppBundler.class,
  53             params -> new WinAppBundler(),
  54             null);
  55 
  56     public static final BundlerParamInfo<WinServiceBundler> SERVICE_BUNDLER = new WindowsBundlerParam<>(
  57             I18N.getString("param.service-bundler.name"),
  58             I18N.getString("param.service-bundler.description"),
  59             "win.service.bundler",
  60             WinServiceBundler.class,
  61             params -> new WinServiceBundler(),
  62             null);
  63 
  64     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  65             I18N.getString("param.config-root.name"),
  66             I18N.getString("param.config-root.description"),
  67             "configRoot",
  68             File.class, params -> {
  69                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  70                 imagesRoot.mkdirs();
  71                 return imagesRoot;
  72             },
  73             (s, p) -> null);
  74 
  75     //default for .exe is user level installation
  76     // only do system wide if explicitly requested
  77     public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE  =
  78             new StandardBundlerParam<>(
  79                     I18N.getString("param.system-wide.name"),
  80                     I18N.getString("param.system-wide.description"),
  81                     "win.exe." + BundleParams.PARAM_SYSTEM_WIDE,
  82                     Boolean.class,
  83                     params -> params.containsKey(SYSTEM_WIDE.getID())
  84                                 ? SYSTEM_WIDE.fetchFrom(params)
  85                                 : false, // EXEs default to user local install
  86                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
  87             );
  88 
  89     public static final BundlerParamInfo<File> EXE_IMAGE_DIR = new WindowsBundlerParam<>(
  90             I18N.getString("param.image-dir.name"),
  91             I18N.getString("param.image-dir.description"),
  92             "win.exe.imageDir",
  93             File.class,
  94             params -> {
  95                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  96                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  97                 return new File(imagesRoot, "win-exe.image");
  98             },
  99             (s, p) -> null);
 100 
 101     private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss";
 102     private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe";
 103 
 104     public static final BundlerParamInfo<String> TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>(
 105             I18N.getString("param.iscc-path.name"),
 106             I18N.getString("param.iscc-path.description"),
 107             "win.exe.iscc.exe",
 108             String.class,
 109             params -> {
 110                 for (String dirString : (System.getenv("PATH") + ";C:\\Program Files (x86)\\Inno Setup 5;C:\\Program Files\\Inno Setup 5").split(";")) {
 111                     File f = new File(dirString.replace("\"", ""), TOOL_INNO_SETUP_COMPILER);
 112                     if (f.isFile()) {
 113                         return f.toString();
 114                     }
 115                 }
 116                 return null;
 117             },
 118             null);
 119 
 120     public WinExeBundler() {
 121         super();
 122         baseResourceLoader = WinResources.class;
 123     }
 124 
 125     @Override
 126     public String getName() {
 127         return I18N.getString("bundler.name");
 128     }
 129 
 130     @Override
 131     public String getDescription() {
 132         return I18N.getString("bundler.description");
 133     }
 134 
 135     @Override
 136     public String getID() {
 137         return "exe";
 138     }
 139 
 140     @Override
 141     public String getBundleType() {
 142         return "INSTALLER";
 143     }
 144 
 145     @Override
 146     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 147         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 148         results.addAll(WinAppBundler.getAppBundleParameters());
 149         results.addAll(getExeBundleParameters());
 150         return results;
 151     }
 152 
 153     public static Collection<BundlerParamInfo<?>> getExeBundleParameters() {
 154         return Arrays.asList(
 155                 DESCRIPTION,
 156                 COPYRIGHT,
 157                 LICENSE_FILE,
 158                 MENU_GROUP,
 159                 MENU_HINT,
 160 //                RUN_AT_STARTUP,
 161                 SHORTCUT_HINT,
 162 //                SERVICE_HINT,
 163 //                START_ON_INSTALL,
 164 //                STOP_ON_UNINSTALL,
 165                 SYSTEM_WIDE,
 166                 TITLE,
 167                 VENDOR,
 168                 INSTALLDIR_CHOOSER
 169         );
 170     }
 171 
 172     @Override
 173     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 174         return bundle(params, outputParentDir);
 175     }
 176 
 177     static class VersionExtractor extends PrintStream {
 178         double version = 0f;
 179 
 180         public VersionExtractor() {
 181             super(new ByteArrayOutputStream());
 182         }
 183 
 184         double getVersion() {
 185             if (version == 0f) {
 186                 String content = new String(((ByteArrayOutputStream) out).toByteArray());
 187                 Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)");
 188                 Matcher matcher = pattern.matcher(content);
 189                 if (matcher.find()) {
 190                     String v = matcher.group(1);
 191                     version = new Double(v);
 192                 }
 193             }
 194             return version;
 195         }
 196     }
 197 
 198     private static double findToolVersion(String toolName) {
 199         try {
 200             if (toolName == null || "".equals(toolName)) return 0f;
 201 
 202             ProcessBuilder pb = new ProcessBuilder(
 203                     toolName,
 204                     "/?");
 205             VersionExtractor ve = new VersionExtractor();
 206             IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output
 207             double version = ve.getVersion();
 208             Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version));
 209             return version;
 210         } catch (Exception e) {
 211             if (Log.isDebug()) {
 212                 e.printStackTrace();
 213             }
 214             return 0f;
 215         }
 216     }
 217 
 218     @Override
 219     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 220         try {
 221             if (p == null) throw new ConfigException(I18N.getString("error.parameters-null"), I18N.getString("error.parameters-null.advice"));
 222 
 223             //run basic validation to ensure requirements are met
 224             //we are not interested in return code, only possible exception
 225             APP_BUNDLER.fetchFrom(p).validate(p);
 226 
 227             // make sure some key values don't have newlines
 228             for (BundlerParamInfo<String> pi : Arrays.asList(
 229                     APP_NAME,
 230                     COPYRIGHT,
 231                     DESCRIPTION,
 232                     MENU_GROUP,
 233                     TITLE,
 234                     VENDOR,
 235                     VERSION)
 236             ) {
 237                 String v = pi.fetchFrom(p);
 238                 if (v.contains("\n") | v.contains("\r")) {
 239                     throw new ConfigException("Parmeter '" + pi.getID() + "' cannot contain a newline.",
 240                             "Change the value of '" + pi.getID() + " so that it does not contain any newlines");
 241                 }
 242             }
 243 
 244             //exe bundlers trim the copyright to 100 characters, tell them this will happen
 245             if (COPYRIGHT.fetchFrom(p).length() > 100) {
 246                 throw new ConfigException(
 247                         I18N.getString("error.copyright-is-too-long"),
 248                         I18N.getString("error.copyright-is-too-long.advice"));
 249             }
 250 
 251             // validate license file, if used, exists in the proper place
 252             if (p.containsKey(LICENSE_FILE.getID())) {
 253                 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(p);
 254                 for (String license : LICENSE_FILE.fetchFrom(p)) {
 255                     boolean found = false;
 256                     for (RelativeFileSet appResources : appResourcesList) {
 257                         found = found || appResources.contains(license);
 258                     }
 259                     if (!found) {
 260                         throw new ConfigException(
 261                                 I18N.getString("error.license-missing"),
 262                                 MessageFormat.format(I18N.getString("error.license-missing.advice"),
 263                                         license));
 264                     }
 265                 }
 266             }
 267 
 268             if (SERVICE_HINT.fetchFrom(p)) {
 269                 SERVICE_BUNDLER.fetchFrom(p).validate(p);
 270             }
 271 
 272             double innoVersion = findToolVersion(TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p));
 273 
 274             //Inno Setup 5+ is required
 275             double minVersion = 5.0f;
 276 
 277             if (innoVersion < minVersion) {
 278                 Log.info(MessageFormat.format(I18N.getString("message.tool-wrong-version"), TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion));
 279                 throw new ConfigException(
 280                         I18N.getString("error.iscc-not-found"),
 281                         I18N.getString("error.iscc-not-found.advice"));
 282             }
 283 
 284             return true;
 285         } catch (RuntimeException re) {
 286             if (re.getCause() instanceof ConfigException) {
 287                 throw (ConfigException) re.getCause();
 288             } else {
 289                 throw new ConfigException(re);
 290             }
 291         }
 292     }
 293 
 294     private boolean prepareProto(Map<String, ? super Object> params) throws IOException {
 295         File imageDir = EXE_IMAGE_DIR.fetchFrom(params);
 296         File appOutputDir = APP_BUNDLER.fetchFrom(params).doBundle(params, imageDir, true);
 297         if (appOutputDir == null) {
 298             return false;
 299         }
 300 
 301         List<String> licenseFiles = LICENSE_FILE.fetchFrom(params);
 302         if (licenseFiles != null) {
 303             //need to copy license file to the root of win.app.image
 304             outerLoop:
 305             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
 306                 for (String s : licenseFiles) {
 307                     if (rfs.contains(s)) {
 308                         File lfile = new File(rfs.getBaseDirectory(), s);
 309                         IOUtils.copyFile(lfile, new File(imageDir, lfile.getName()));
 310                         break outerLoop;
 311                     }
 312                 }
 313             }
 314         }
 315 
 316         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 317             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 318             if (icon != null && icon.exists()) {
 319                 IOUtils.copyFile(icon, new File(appOutputDir, icon.getName()));
 320             }
 321         }
 322 
 323         if (SERVICE_HINT.fetchFrom(params)) {
 324             // copies the service launcher to the app root folder
 325             appOutputDir = SERVICE_BUNDLER.fetchFrom(params).doBundle(params, appOutputDir, true);
 326             if (appOutputDir == null) {
 327                 return false;
 328             }
 329         }
 330         return true;
 331     }
 332 
 333     public File bundle(Map<String, ? super Object> p, File outputDirectory) {
 334         if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 335             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 336         }
 337 
 338         if (!outputDirectory.canWrite()) {
 339             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 340         }
 341 
 342         if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) {
 343             Log.info(MessageFormat.format(I18N.getString("message.potential.windows.defender.issue"), WindowsDefender.getUserTempDirectory()));
 344         }
 345 
 346         // validate we have valid tools before continuing
 347         String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p);
 348         if (iscc == null || !new File(iscc).isFile()) {
 349             Log.info(I18N.getString("error.iscc-not-found"));
 350             Log.info(MessageFormat.format(I18N.getString("message.iscc-file-string"), iscc));
 351             return null;
 352         }
 353 
 354         File imageDir = EXE_IMAGE_DIR.fetchFrom(p);
 355         try {
 356             imageDir.mkdirs();
 357 
 358             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 359             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 360             if (!menuShortcut && !desktopShortcut) {
 361                 //both can not be false - user will not find the app
 362                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 363                 p.put(MENU_HINT.getID(), true);
 364             }
 365 
 366             if (prepareProto(p) && prepareProjectConfig(p)) {
 367                 File configScript = getConfig_Script(p);
 368                 if (configScript.exists()) {
 369                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
 370                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
 371                 }
 372                 return buildEXE(p, outputDirectory);
 373             }
 374             return null;
 375         } catch (IOException ex) {
 376             ex.printStackTrace();
 377             return null;
 378         } finally {
 379             try {
 380                 if (VERBOSE.fetchFrom(p)) {
 381                     saveConfigFiles(p);
 382                 }
 383                 if (imageDir != null && !Log.isDebug()) {
 384                     IOUtils.deleteRecursive(imageDir);
 385                 } else if (imageDir != null) {
 386                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 387                 }
 388             } catch (IOException ex) {
 389                 //noinspection ReturnInsideFinallyBlock
 390                 Log.debug(ex.getMessage());
 391                 return null;
 392             }
 393         }
 394     }
 395 
 396     //name of post-image script
 397     private File getConfig_Script(Map<String, ? super Object> params) {
 398         return new File(EXE_IMAGE_DIR.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.wsf");
 399     }
 400 
 401     protected void saveConfigFiles(Map<String, ? super Object> params) {
 402         try {
 403             File configRoot = CONFIG_ROOT.fetchFrom(params);
 404             if (getConfig_ExeProjectFile(params).exists()) {
 405                 IOUtils.copyFile(getConfig_ExeProjectFile(params),
 406                         new File(configRoot, getConfig_ExeProjectFile(params).getName()));
 407             }
 408             if (getConfig_Script(params).exists()) {
 409                 IOUtils.copyFile(getConfig_Script(params),
 410                         new File(configRoot, getConfig_Script(params).getName()));
 411             }
 412             if (getConfig_SmallInnoSetupIcon(params).exists()) {
 413                 IOUtils.copyFile(getConfig_SmallInnoSetupIcon(params),
 414                         new File(configRoot, getConfig_SmallInnoSetupIcon(params).getName()));
 415             }
 416             Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), configRoot.getAbsolutePath()));
 417         } catch (IOException ioe) {
 418             ioe.printStackTrace();
 419         }
 420     }
 421 
 422     private String getAppIdentifier(Map<String, ? super Object> params) {
 423         String nm = IDENTIFIER.fetchFrom(params);
 424 
 425         //limitation of innosetup
 426         if (nm.length() > 126)
 427             nm = nm.substring(0, 126);
 428 
 429         return nm;
 430     }
 431 
 432 
 433     private String getLicenseFile(Map<String, ? super Object> params) {
 434         List<String> licenseFiles = LICENSE_FILE.fetchFrom(params);
 435         if (licenseFiles == null || licenseFiles.isEmpty()) {
 436             return "";
 437         } else {
 438             return licenseFiles.get(0);
 439         }
 440     }
 441 
 442     void validateValueAndPut(Map<String, String> data, String key, BundlerParamInfo<String> param, Map<String, ? super Object> params) throws IOException {
 443         String value = param.fetchFrom(params);
 444         if (value.contains("\r") || value.contains("\n")) {
 445             throw new IOException("Configuration Parameter " + param.getID() + " cannot contain multiple lines of text");
 446         }
 447         data.put(key, innosetupEscape(value));
 448     }
 449 
 450     private String innosetupEscape(String value) {
 451         if (value.contains("\"") || !value.trim().equals(value)) {
 452             value = "\"" + value.replace("\"", "\"\"") + "\"";
 453         }
 454         return value;
 455     }
 456 
 457     boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException {
 458         Map<String, String> data = new HashMap<>();
 459         data.put("PRODUCT_APP_IDENTIFIER", innosetupEscape(getAppIdentifier(params)));
 460 
 461         validateValueAndPut(data, "APPLICATION_NAME", APP_NAME, params);
 462 
 463         validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, params);
 464         validateValueAndPut(data, "APPLICATION_VERSION", VERSION, params);
 465         validateValueAndPut(data, "INSTALLER_FILE_NAME", INSTALLER_FILE_NAME, params);
 466 
 467         data.put("APPLICATION_LAUNCHER_FILENAME", innosetupEscape(WinAppBundler.getLauncherName(params)));
 468 
 469         data.put("APPLICATION_DESKTOP_SHORTCUT", SHORTCUT_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 470         data.put("APPLICATION_MENU_SHORTCUT", MENU_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 471         validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, params);
 472         validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, params); // TODO this seems strange, at least in name
 473         validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, params);
 474 
 475         data.put("APPLICATION_LICENSE_FILE", innosetupEscape(getLicenseFile(params)));
 476         data.put("DISABLE_DIR_PAGE", INSTALLDIR_CHOOSER.fetchFrom(params) ? "No" : "Yes");
 477 
 478         Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(params);
 479 
 480         if (isSystemWide) {
 481             data.put("APPLICATION_INSTALL_ROOT", "{pf}");
 482             data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
 483         } else {
 484             data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
 485             data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
 486         }
 487 
 488         if (BIT_ARCH_64.fetchFrom(params)) {
 489             data.put("ARCHITECTURE_BIT_MODE", "x64");
 490         } else {
 491             data.put("ARCHITECTURE_BIT_MODE", "");
 492         }
 493 
 494         if (SERVICE_HINT.fetchFrom(params)) {
 495             data.put("RUN_FILENAME", innosetupEscape(WinServiceBundler.getAppSvcName(params)));
 496         } else {
 497             validateValueAndPut(data, "RUN_FILENAME", APP_NAME, params);
 498         }
 499         validateValueAndPut(data, "APPLICATION_DESCRIPTION", DESCRIPTION, params);
 500         data.put("APPLICATION_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 501         data.put("APPLICATION_NOT_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnFalse" : "returnTrue");
 502         data.put("APPLICATION_APP_CDS_INSTALL",
 503                 (UNLOCK_COMMERCIAL_FEATURES.fetchFrom(params) && ENABLE_APP_CDS.fetchFrom(params)
 504                         && ("install".equals(APP_CDS_CACHE_MODE.fetchFrom(params))
 505                             || "auto+install".equals(APP_CDS_CACHE_MODE.fetchFrom(params))))
 506                 ? "returnTrue"
 507                 : "returnFalse");
 508         data.put("START_ON_INSTALL", START_ON_INSTALL.fetchFrom(params) ? "-startOnInstall" : "");
 509         data.put("STOP_ON_UNINSTALL", STOP_ON_UNINSTALL.fetchFrom(params) ? "-stopOnUninstall" : "");
 510         data.put("RUN_AT_STARTUP", RUN_AT_STARTUP.fetchFrom(params) ? "-runAtStartup" : "");
 511 
 512         StringBuilder secondaryLaunchersCfg = new StringBuilder();
 513         for (Map<String, ? super Object> launcher : SECONDARY_LAUNCHERS.fetchFrom(params)) {
 514             String application_name = APP_NAME.fetchFrom(launcher);
 515             if (MENU_HINT.fetchFrom(launcher)) {
 516                 //Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"
 517                 secondaryLaunchersCfg.append("Name: \"{group}\\");
 518                 secondaryLaunchersCfg.append(application_name);
 519                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
 520                 secondaryLaunchersCfg.append(application_name);
 521                 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
 522                 secondaryLaunchersCfg.append(application_name);
 523                 secondaryLaunchersCfg.append(".ico\"\r\n");
 524             }
 525             if (SHORTCUT_HINT.fetchFrom(launcher)) {
 526                 //Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe";  IconFilename: "{app}\APPLICATION_NAME.ico"
 527                 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\");
 528                 secondaryLaunchersCfg.append(application_name);
 529                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
 530                 secondaryLaunchersCfg.append(application_name);
 531                 secondaryLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
 532                 secondaryLaunchersCfg.append(application_name);
 533                 secondaryLaunchersCfg.append(".ico\"\r\n");
 534             }
 535         }
 536         data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString());
 537 
 538         StringBuilder registryEntries = new StringBuilder();
 539         String regName = APP_REGISTRY_NAME.fetchFrom(params);
 540         List<Map<String, ? super Object>> fetchFrom = FILE_ASSOCIATIONS.fetchFrom(params);
 541         for (int i = 0; i < fetchFrom.size(); i++) {
 542             Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
 543             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 544             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 545 
 546             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 547             String entryName = regName + "File";
 548             if (i > 0) {
 549                 entryName += "." + i;
 550             }
 551 
 552             if (extensions == null) {
 553                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 554             } else {
 555                 for (String ext : extensions) {
 556                     if (isSystemWide) {
 557                         // "Root: HKCR; Subkey: \".myp\"; ValueType: string; ValueName: \"\"; ValueData: \"MyProgramFile\"; Flags: uninsdeletevalue"
 558                         registryEntries.append("Root: HKCR; Subkey: \".")
 559                                 .append(ext)
 560                                 .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 561                                 .append(entryName)
 562                                 .append("\"; Flags: uninsdeletevalue\r\n");
 563                     } else {
 564                         registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\.")
 565                                 .append(ext)
 566                                 .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 567                                 .append(entryName)
 568                                 .append("\"; Flags: uninsdeletevalue\r\n");
 569                     }
 570                 }
 571             }
 572 
 573             if (extensions != null && !extensions.isEmpty()) {
 574                 String ext = extensions.get(0);
 575                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 576                 for (String mime : mimeTypes) {
 577                     if (isSystemWide) {
 578                         // "Root: HKCR; Subkey: HKCR\\Mime\\Database\\Content Type\\application/chaos; ValueType: string; ValueName: Extension; ValueData: .chaos; Flags: uninsdeletevalue"
 579                         registryEntries.append("Root: HKCR; Subkey: \"Mime\\Database\\Content Type\\")
 580                             .append(mime)
 581                             .append("\"; ValueType: string; ValueName: \"Extension\"; ValueData: \".")
 582                             .append(ext)
 583                             .append("\"; Flags: uninsdeletevalue\r\n");
 584                     } else {
 585                         registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\Mime\\Database\\Content Type\\")
 586                                 .append(mime)
 587                                 .append("\"; ValueType: string; ValueName: \"Extension\"; ValueData: \".")
 588                                 .append(ext)
 589                                 .append("\"; Flags: uninsdeletevalue\r\n");
 590                     }
 591                 }
 592             }
 593 
 594             if (isSystemWide) {
 595                 //"Root: HKCR; Subkey: \"MyProgramFile\"; ValueType: string; ValueName: \"\"; ValueData: \"My Program File\"; Flags: uninsdeletekey"
 596                 registryEntries.append("Root: HKCR; Subkey: \"")
 597                     .append(entryName)
 598                     .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 599                     .append(description)
 600                     .append("\"; Flags: uninsdeletekey\r\n");
 601             } else {
 602                 registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 603                     .append(entryName)
 604                     .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 605                     .append(description)
 606                     .append("\"; Flags: uninsdeletekey\r\n");
 607 
 608             }
 609 
 610             if (icon != null && icon.exists()) {
 611                 if (isSystemWide) {
 612                     // "Root: HKCR; Subkey: \"MyProgramFile\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
 613                     registryEntries.append("Root: HKCR; Subkey: \"")
 614                         .append(entryName)
 615                         .append("\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\")
 616                         .append(icon.getName())
 617                         .append("\"\r\n");
 618                 } else {
 619                     registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 620                             .append(entryName)
 621                             .append("\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\")
 622                             .append(icon.getName())
 623                             .append("\"\r\n");
 624                 }
 625             }
 626 
 627             if (isSystemWide) {
 628                 //"Root: HKCR; Subkey: \"MyProgramFile\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
 629                 registryEntries.append("Root: HKCR; Subkey: \"")
 630                         .append(entryName)
 631                         .append("\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 632                         .append(APP_NAME.fetchFrom(params))
 633                         .append("\"\" \"\"%1\"\"\"\r\n");
 634             } else {
 635                 registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 636                         .append(entryName)
 637                         .append("\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 638                         .append(APP_NAME.fetchFrom(params))
 639                         .append("\"\" \"\"%1\"\"\"\r\n");
 640             }
 641         }
 642         if (registryEntries.length() > 0) {
 643             data.put("FILE_ASSOCIATIONS", "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + registryEntries.toString());
 644         } else {
 645             data.put("FILE_ASSOCIATIONS", "");
 646         }
 647 
 648         Writer w = new BufferedWriter(new FileWriter(getConfig_ExeProjectFile(params)));
 649         String content = preprocessTextResource(
 650                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ExeProjectFile(params).getName(),
 651                 I18N.getString("resource.inno-setup-project-file"), DEFAULT_EXE_PROJECT_TEMPLATE, data,
 652                 VERBOSE.fetchFrom(params),
 653                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 654         w.write(content);
 655         w.close();
 656         return true;
 657     }
 658 
 659     private final static String DEFAULT_INNO_SETUP_ICON = "icon_inno_setup.bmp";
 660 
 661     private boolean prepareProjectConfig(Map<String, ? super Object> params) throws IOException {
 662         prepareMainProjectFile(params);
 663 
 664         //prepare installer icon
 665         File iconTarget = getConfig_SmallInnoSetupIcon(params);
 666         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(),
 667                 I18N.getString("resource.setup-icon"),
 668                 DEFAULT_INNO_SETUP_ICON,
 669                 iconTarget,
 670                 VERBOSE.fetchFrom(params),
 671                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 672 
 673         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 674                 I18N.getString("resource.post-install-script"),
 675                 (String) null,
 676                 getConfig_Script(params),
 677                 VERBOSE.fetchFrom(params),
 678                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 679         return true;
 680     }
 681 
 682     private File getConfig_SmallInnoSetupIcon(Map<String, ? super Object> params) {
 683         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 684                 APP_NAME.fetchFrom(params) + "-setup-icon.bmp");
 685     }
 686 
 687     private File getConfig_ExeProjectFile(Map<String, ? super Object> params) {
 688         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 689                 APP_NAME.fetchFrom(params) + ".iss");
 690     }
 691 
 692 
 693     private File buildEXE(Map<String, ? super Object> params, File outdir) throws IOException {
 694         Log.verbose(MessageFormat.format(I18N.getString("message.outputting-to-location"), outdir.getAbsolutePath()));
 695 
 696         outdir.mkdirs();
 697 
 698         //run candle
 699         ProcessBuilder pb = new ProcessBuilder(
 700                 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params),
 701                 "/o"+outdir.getAbsolutePath(),
 702                 getConfig_ExeProjectFile(params).getAbsolutePath());
 703         pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(params));
 704         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 705 
 706         Log.info(MessageFormat.format(I18N.getString("message.output-location"), outdir.getAbsolutePath()));
 707 
 708         // presume the result is the ".exe" file with the newest modified time
 709         // not the best solution, but it is the most reliable
 710         File result = null;
 711         long lastModified = 0;
 712         File[] list = outdir.listFiles();
 713         if (list != null) {
 714             for (File f : list) {
 715                 if (f.getName().endsWith(".exe") && f.lastModified() > lastModified) {
 716                     result = f;
 717                     lastModified = f.lastModified();
 718                 }
 719             }
 720         }
 721 
 722         return result;
 723     }
 724 }