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