1 /*
   2  * Copyright (c) 2012, 2015, 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.oracle.tools.packager.ConfigException;
  30 import com.oracle.tools.packager.UnsupportedPlatformException;
  31 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  32 
  33 import java.io.*;
  34 import java.nio.charset.Charset;
  35 import java.nio.file.Files;
  36 import java.text.MessageFormat;
  37 import java.util.*;
  38 import java.util.regex.Matcher;
  39 import java.util.regex.Pattern;
  40 
  41 import static com.oracle.tools.packager.StandardBundlerParam.DESCRIPTION;
  42 import static com.oracle.tools.packager.StandardBundlerParam.RUN_AT_STARTUP;
  43 import static com.oracle.tools.packager.StandardBundlerParam.SERVICE_HINT;
  44 import static com.oracle.tools.packager.StandardBundlerParam.START_ON_INSTALL;
  45 import static com.oracle.tools.packager.StandardBundlerParam.STOP_ON_UNINSTALL;
  46 import static com.oracle.tools.packager.windows.WindowsBundlerParam.*;
  47 
  48 public class WinMsiBundler  extends AbstractBundler {
  49 
  50     private static final ResourceBundle I18N =
  51             ResourceBundle.getBundle(WinMsiBundler.class.getName());
  52 
  53     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
  54             I18N.getString("param.app-bundler.name"),
  55             I18N.getString("param.app-bundler.description"),
  56             "win.app.bundler",
  57             WinAppBundler.class,
  58             params -> new WinAppBundler(),
  59             null);
  60 
  61     public static final BundlerParamInfo<WinServiceBundler> SERVICE_BUNDLER = new WindowsBundlerParam<>(
  62             I18N.getString("param.service-bundler.name"),
  63             I18N.getString("param.service-bundler.description"),
  64             "win.service.bundler",
  65             WinServiceBundler.class,
  66             params -> new WinServiceBundler(),
  67             null);
  68 
  69     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 = new WindowsBundlerParam<>(
  70             I18N.getString("param.can-use-wix36.name"),
  71             I18N.getString("param.can-use-wix36.description"),
  72             "win.msi.canUseWix36",
  73             Boolean.class,
  74             params -> false,
  75             (s, p) -> Boolean.valueOf(s));
  76 
  77     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  78             I18N.getString("param.config-root.name"),
  79             I18N.getString("param.config-root.description"),
  80             "configRoot",
  81             File.class,
  82             params -> {
  83                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  84                 imagesRoot.mkdirs();
  85                 return imagesRoot;
  86             },
  87             (s, p) -> null);
  88 
  89     public static final BundlerParamInfo<File> MSI_IMAGE_DIR = new WindowsBundlerParam<>(
  90             I18N.getString("param.image-dir.name"),
  91             I18N.getString("param.image-dir.description"),
  92             "win.msi.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-msi.image");
  98             },
  99             (s, p) -> null);
 100 
 101     public static final BundlerParamInfo<File> WIN_APP_IMAGE = new WindowsBundlerParam<>(
 102             I18N.getString("param.app-dir.name"),
 103             I18N.getString("param.app-dir.description"),
 104             "win.app.image",
 105             File.class,
 106             null,
 107             (s, p) -> null);
 108 
 109     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
 110             new StandardBundlerParam<>(
 111                     I18N.getString("param.system-wide.name"),
 112                     I18N.getString("param.system-wide.description"),
 113                     "win.msi." + BundleParams.PARAM_SYSTEM_WIDE,
 114                     Boolean.class,
 115                     params -> params.containsKey(SYSTEM_WIDE.getID())
 116                             ? SYSTEM_WIDE.fetchFrom(params)
 117                             : true, // MSIs default to system wide
 118                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
 119             );
 120 
 121 
 122     public static final StandardBundlerParam<String> PRODUCT_VERSION =
 123             new StandardBundlerParam<>(
 124                     I18N.getString("param.product-version.name"),
 125                     I18N.getString("param.product-version.description"),
 126                     "win.msi.productVersion",
 127                     String.class,
 128                     VERSION::fetchFrom,
 129                     (s, p) -> s
 130             );
 131 
 132     public static final BundlerParamInfo<UUID> UPGRADE_UUID = new WindowsBundlerParam<>(
 133             I18N.getString("param.upgrade-uuid.name"),
 134             I18N.getString("param.upgrade-uuid.description"),
 135             "win.msi.upgradeUUID",
 136             UUID.class,
 137             params -> UUID.randomUUID(), // TODO check to see if identifier is a valid UUID during default
 138             (s, p) -> UUID.fromString(s));
 139 
 140     private static final String TOOL_CANDLE = "candle.exe";
 141     private static final String TOOL_LIGHT = "light.exe";
 142     // autodetect just v3.7, v3.8, and 3.9
 143     private static final String AUTODETECT_DIRS = ";C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;C:\\Program Files\\WiX Toolset v3.9\\bin;C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;C:\\Program Files\\WiX Toolset v3.8\\bin;C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;C:\\Program Files\\WiX Toolset v3.7\\bin";
 144 
 145     public static final BundlerParamInfo<String> TOOL_CANDLE_EXECUTABLE = new WindowsBundlerParam<>(
 146             I18N.getString("param.candle-path.name"),
 147             I18N.getString("param.candle-path.description"),
 148             "win.msi.candle.exe",
 149             String.class,
 150             params -> {
 151                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 152                     File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
 153                     if (f.isFile()) {
 154                         return f.toString();
 155                     }
 156                 }
 157                 return null;
 158             },
 159             null);
 160 
 161     public static final BundlerParamInfo<String> TOOL_LIGHT_EXECUTABLE = new WindowsBundlerParam<>(
 162             I18N.getString("param.light-path.name"),
 163             I18N.getString("param.light-path.description"),
 164             "win.msi.light.exe",
 165             String.class,
 166             params -> {
 167                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 168                     File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
 169                     if (f.isFile()) {
 170                         return f.toString();
 171                     }
 172                 }
 173                 return null;
 174             },
 175             null);
 176 
 177     public WinMsiBundler() {
 178         super();
 179         baseResourceLoader = WinResources.class;
 180     }
 181 
 182 
 183     @Override
 184     public String getName() {
 185         return I18N.getString("bundler.name");
 186     }
 187 
 188     @Override
 189     public String getDescription() {
 190         return I18N.getString("bundler.description");
 191     }
 192 
 193     @Override
 194     public String getID() {
 195         return "msi";
 196     }
 197 
 198     @Override
 199     public String getBundleType() {
 200         return "INSTALLER";
 201     }
 202 
 203     @Override
 204     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 205         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 206         results.addAll(WinAppBundler.getAppBundleParameters());
 207         results.addAll(getMsiBundleParameters());
 208         return results;
 209     }
 210 
 211     public static Collection<BundlerParamInfo<?>> getMsiBundleParameters() {
 212         return Arrays.asList(
 213                 DESCRIPTION,
 214                 MENU_GROUP,
 215                 MENU_HINT,
 216                 PRODUCT_VERSION,
 217 //                RUN_AT_STARTUP,
 218                 SHORTCUT_HINT,
 219 //                SERVICE_HINT,
 220 //                START_ON_INSTALL,
 221 //                STOP_ON_UNINSTALL,
 222                 SYSTEM_WIDE,
 223                 //UPGRADE_UUID,
 224                 VENDOR,
 225                 LICENSE_FILE,
 226                 INSTALLDIR_CHOOSER
 227         );
 228     }
 229 
 230     @Override
 231     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 232         return bundle(params, outputParentDir);
 233     }
 234 
 235     static class VersionExtractor extends PrintStream {
 236         double version = 0f;
 237 
 238         public VersionExtractor() {
 239             super(new ByteArrayOutputStream());
 240         }
 241 
 242         double getVersion() {
 243             if (version == 0f) {
 244                 String content = new String(((ByteArrayOutputStream) out).toByteArray());
 245                 Pattern pattern = Pattern.compile("version (\\d+.\\d+)");
 246                 Matcher matcher = pattern.matcher(content);
 247                 if (matcher.find()) {
 248                     String v = matcher.group(1);
 249                     version = new Double(v);
 250                 }
 251             }
 252             return version;
 253         }
 254     }
 255 
 256     private static double findToolVersion(String toolName) {
 257         try {
 258             if (toolName == null || "".equals(toolName)) return 0f;
 259 
 260             ProcessBuilder pb = new ProcessBuilder(
 261                     toolName,
 262                     "/?");
 263             VersionExtractor ve = new VersionExtractor();
 264             IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output
 265             double version = ve.getVersion();
 266             Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version));
 267             return version;
 268         } catch (Exception e) {
 269             if (Log.isDebug()) {
 270                 Log.verbose(e);
 271             }
 272             return 0f;
 273         }
 274     }
 275 
 276     @Override
 277     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 278         try {
 279             if (p == null) throw new ConfigException(
 280                     I18N.getString("error.parameters-null"),
 281                     I18N.getString("error.parameters-null.advice"));
 282 
 283             //run basic validation to ensure requirements are met
 284             //we are not interested in return code, only possible exception
 285             APP_BUNDLER.fetchFrom(p).doValidate(p);
 286 
 287             if (SERVICE_HINT.fetchFrom(p)) {
 288                 SERVICE_BUNDLER.fetchFrom(p).validate(p);
 289             }
 290 
 291             double candleVersion = findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p));
 292             double lightVersion = findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p));
 293 
 294             //WiX 3.0+ is required
 295             double minVersion = 3.0f;
 296             boolean bad = false;
 297 
 298             if (candleVersion < minVersion) {
 299                 Log.verbose(MessageFormat.format(I18N.getString("message.wrong-tool-version"), TOOL_CANDLE, candleVersion, minVersion));
 300                 bad = true;
 301             }
 302             if (lightVersion < minVersion) {
 303                 Log.verbose(MessageFormat.format(I18N.getString("message.wrong-tool-version"), TOOL_LIGHT, lightVersion, minVersion));
 304                 bad = true;
 305             }
 306 
 307             if (bad){
 308                 throw new ConfigException(
 309                         I18N.getString("error.no-wix-tools"),
 310                         I18N.getString("error.no-wix-tools.advice"));
 311             }
 312 
 313             if (lightVersion >= 3.6f) {
 314                 Log.verbose(I18N.getString("message.use-wix36-features"));
 315                 p.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
 316             }
 317 
 318             /********* validate bundle parameters *************/
 319 
 320             String version = PRODUCT_VERSION.fetchFrom(p);
 321             if (!isVersionStringValid(version)) {
 322                 throw new ConfigException(
 323                         MessageFormat.format(I18N.getString("error.version-string-wrong-format"), version),
 324                         MessageFormat.format(I18N.getString("error.version-string-wrong-format.advice"), PRODUCT_VERSION.getID()));
 325             }
 326 
 327             // only one mime type per association, at least one file extension
 328             List<Map<String, ? super Object>> associations = FILE_ASSOCIATIONS.fetchFrom(p);
 329             if (associations != null) {
 330                 for (int i = 0; i < associations.size(); i++) {
 331                     Map<String, ? super Object> assoc = associations.get(i);
 332                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 333                     if (mimes.size() > 1) {
 334                         throw new ConfigException(
 335                                 MessageFormat.format(I18N.getString("error.too-many-content-types-for-file-association"), i),
 336                                 I18N.getString("error.too-many-content-types-for-file-association.advice"));
 337                     }
 338                 }
 339             }
 340 
 341             // validate license file, if used, exists in the proper place
 342             if (p.containsKey(LICENSE_FILE.getID())) {
 343                 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(p);
 344                 for (String license : LICENSE_FILE.fetchFrom(p)) {
 345                     boolean found = false;
 346                     for (RelativeFileSet appResources : appResourcesList) {
 347                         found = found || appResources.contains(license);
 348                     }
 349                     if (!found) {
 350                         throw new ConfigException(
 351                                 I18N.getString("error.license-missing"),
 352                                 MessageFormat.format(I18N.getString("error.license-missing.advice"),
 353                                         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     //http://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
 369     //The format of the string is as follows:
 370     //    major.minor.build
 371     //The first field is the major version and has a maximum value of 255.
 372     //The second field is the minor version and has a maximum value of 255.
 373     //The third field is called the build version or the update version and
 374     // has a maximum value of 65,535.
 375     static boolean isVersionStringValid(String v) {
 376         if (v == null) {
 377             return true;
 378         }
 379 
 380         String p[] = v.split("\\.");
 381         if (p.length > 3) {
 382             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 383             return false;
 384         }
 385 
 386         try {
 387             int val = Integer.parseInt(p[0]);
 388             if (val < 0 || val > 255) {
 389                 Log.verbose(I18N.getString("error.version-string-major-out-of-range"));
 390                 return false;
 391             }
 392             if (p.length > 1) {
 393                 val = Integer.parseInt(p[1]);
 394                 if (val < 0 || val > 255) {
 395                     Log.verbose(I18N.getString("error.version-string-minor-out-of-range"));
 396                     return false;
 397                 }
 398             }
 399             if (p.length > 2) {
 400                 val = Integer.parseInt(p[2]);
 401                 if (val < 0 || val > 65535) {
 402                     Log.verbose(I18N.getString("error.version-string-build-out-of-range"));
 403                     return false;
 404                 }
 405             }
 406         } catch (NumberFormatException ne) {
 407             Log.verbose(I18N.getString("error.version-string-part-not-number"));
 408             Log.verbose(ne);
 409             return false;
 410         }
 411 
 412         return true;
 413     }
 414 
 415     private boolean prepareProto(Map<String, ? super Object> p) throws IOException {
 416         File bundleRoot = MSI_IMAGE_DIR.fetchFrom(p);
 417         File appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, bundleRoot, true);
 418         p.put(WIN_APP_IMAGE.getID(), appDir);
 419 
 420         if (SERVICE_HINT.fetchFrom(p) && appDir != null) {
 421             // just copies the service launcher to the app root folder
 422             appDir = SERVICE_BUNDLER.fetchFrom(p).doBundle(p, appDir, true);
 423         }
 424 
 425         List<String> licenseFiles = LICENSE_FILE.fetchFrom(p);
 426         if (licenseFiles != null) {
 427             //need to copy license file to the root of win.app.image
 428             outerLoop:
 429             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(p)) {
 430                 for (String s : licenseFiles) {
 431                     if (rfs.contains(s)) {
 432                         File lfile = new File(rfs.getBaseDirectory(), s);
 433                         File destFile = new File(appDir, lfile.getName());
 434                         IOUtils.copyFile(lfile, destFile);
 435                         ensureByMutationFileIsRTF(destFile);
 436                         break outerLoop;
 437                     }
 438                 }
 439             }
 440         }
 441 
 442         // copy file association icons
 443         List<Map<String, ? super Object>> fileAssociations = FILE_ASSOCIATIONS.fetchFrom(p);
 444         for (Map<String, ? super Object> fileAssociation : fileAssociations) {
 445             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 446             if (icon == null) {
 447                 continue;
 448             }
 449 
 450             File faIconFile = new File(appDir, icon.getName());
 451 
 452             if (icon.exists()) {
 453                 try {
 454                     IOUtils.copyFile(icon, faIconFile);
 455                 } catch (IOException e) {
 456                     e.printStackTrace();
 457                 }
 458             }
 459         }
 460 
 461         return appDir != null;
 462     }
 463 
 464     public File bundle(Map<String, ? super Object> p, File outdir) {
 465         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 466             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outdir.getAbsolutePath()));
 467         }
 468         if (!outdir.canWrite()) {
 469             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outdir.getAbsolutePath()));
 470         }
 471 
 472         // validate we have valid tools before continuing
 473         String light = TOOL_LIGHT_EXECUTABLE.fetchFrom(p);
 474         String candle = TOOL_CANDLE_EXECUTABLE.fetchFrom(p);
 475         if (light == null || !new File(light).isFile() ||
 476             candle == null || !new File(candle).isFile()) {
 477             Log.info(I18N.getString("error.no-wix-tools"));
 478             Log.info(MessageFormat.format(I18N.getString("message.light-file-string"), light));
 479             Log.info(MessageFormat.format(I18N.getString("message.candle-file-string"), candle));
 480             return null;
 481         }
 482 
 483         File imageDir = MSI_IMAGE_DIR.fetchFrom(p);
 484         try {
 485             imageDir.mkdirs();
 486 
 487             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 488             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 489             if (!menuShortcut && !desktopShortcut) {
 490                 //both can not be false - user will not find the app
 491                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 492                 p.put(MENU_HINT.getID(), true);
 493             }
 494 
 495             if (prepareProto(p) && prepareWiXConfig(p)
 496                     && prepareBasicProjectConfig(p)) {
 497                 File configScriptSrc = getConfig_Script(p);
 498                 if (configScriptSrc.exists()) {
 499                     //we need to be running post script in the image folder
 500 
 501                     // NOTE: Would it be better to generate it to the image folder
 502                     // and save only if "verbose" is requested?
 503 
 504                     // for now we replicate it
 505                     File configScript = new File(imageDir, configScriptSrc.getName());
 506                     IOUtils.copyFile(configScriptSrc, configScript);
 507                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
 508                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
 509                 }
 510                 return buildMSI(p, outdir);
 511             }
 512             return null;
 513         } catch (IOException ex) {
 514             Log.verbose(ex);
 515             return null;
 516         } finally {
 517             try {
 518                 if (imageDir != null && !Log.isDebug()) {
 519                     IOUtils.deleteRecursive(imageDir);
 520                 } else if (imageDir != null) {
 521                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 522                 }
 523                 if (VERBOSE.fetchFrom(p)) {
 524                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 525                 } else {
 526                     cleanupConfigFiles(p);
 527                 }
 528             } catch (FileNotFoundException ex) {
 529                 //noinspection ReturnInsideFinallyBlock
 530                 return null;
 531             }
 532         }
 533     }
 534 
 535     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 536         if (getConfig_ProjectFile(params) != null) {
 537             getConfig_ProjectFile(params).delete();
 538         }
 539         if (getConfig_Script(params) != null) {
 540             getConfig_Script(params).delete();
 541         }
 542     }
 543 
 544     //name of post-image script
 545     private File getConfig_Script(Map<String, ? super Object> params) {
 546         return new File(CONFIG_ROOT.fetchFrom(params),
 547                 APP_FS_NAME.fetchFrom(params) + "-post-image.wsf");
 548     }
 549 
 550     private boolean prepareBasicProjectConfig(Map<String, ? super Object> params) throws IOException {
 551         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 552                 I18N.getString("resource.post-install-script"),
 553                 (String) null,
 554                 getConfig_Script(params),
 555                 VERBOSE.fetchFrom(params),
 556                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 557         return true;
 558     }
 559 
 560     private String relativePath(File basedir, File file) {
 561         return file.getAbsolutePath().substring(
 562                 basedir.getAbsolutePath().length() + 1);
 563     }
 564 
 565     boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException {
 566         Map<String, String> data = new HashMap<>();
 567 
 568         UUID productGUID = UUID.randomUUID();
 569 
 570         Log.verbose(MessageFormat.format(I18N.getString("message.generated-product-guid"), productGUID.toString()));
 571 
 572         //we use random GUID for product itself but
 573         // user provided for upgrade guid
 574         // Upgrade guid is important to decide whether it is upgrade of installed
 575         //  app. I.e. we need it to be the same for 2 different versions of app if possible
 576         data.put("PRODUCT_GUID", productGUID.toString());
 577         data.put("PRODUCT_UPGRADE_GUID", UPGRADE_UUID.fetchFrom(params).toString());
 578 
 579         data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
 580         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 581         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 582         data.put("APPLICATION_VERSION", PRODUCT_VERSION.fetchFrom(params));
 583 
 584         //WinAppBundler will add application folder again => step out
 585         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 586         File launcher = new File(imageRootDir, WinAppBundler.getLauncherName(params));
 587 
 588         String launcherPath = relativePath(imageRootDir, launcher);
 589         data.put("APPLICATION_LAUNCHER", launcherPath);
 590 
 591         String iconPath = launcherPath.replace(".exe", ".ico");
 592 
 593         data.put("APPLICATION_ICON", iconPath);
 594 
 595         data.put("REGISTRY_ROOT", getRegistryRoot(params));
 596 
 597         boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
 598         data.put("WIX36_ONLY_START",
 599                 canUseWix36Features ? "" : "<!--");
 600         data.put("WIX36_ONLY_END",
 601                 canUseWix36Features ? "" : "-->");
 602 
 603         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 604             data.put("INSTALL_SCOPE", "perMachine");
 605         } else {
 606             data.put("INSTALL_SCOPE", "perUser");
 607         }
 608 
 609         if (BIT_ARCH_64.fetchFrom(params)) {
 610             data.put("PLATFORM", "x64");
 611             data.put("WIN64", "yes");
 612         } else {
 613             data.put("PLATFORM", "x86");
 614             data.put("WIN64", "no");
 615         }
 616 
 617         data.put("UI_BLOCK", getUIBlock(params));
 618         data.put("APP_CDS_BLOCK", getAppCDSBlock(params));
 619 
 620         List<Map<String, ? super Object>> secondaryLaunchers = SECONDARY_LAUNCHERS.fetchFrom(params);
 621 
 622         StringBuilder secondaryLauncherIcons = new StringBuilder();
 623         for (int i = 0; i < secondaryLaunchers.size(); i++) {
 624             Map<String, ? super Object> sl = secondaryLaunchers.get(i);
 625             //        <Icon Id="DesktopIcon.exe" SourceFile="APPLICATION_ICON" />
 626             if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
 627                 File secondaryLauncher = new File(imageRootDir, WinAppBundler.getLauncherName(sl));
 628                 String secondaryLauncherPath = relativePath(imageRootDir, secondaryLauncher);
 629                 String secondaryLauncherIconPath = secondaryLauncherPath.replace(".exe", ".ico");
 630 
 631                 secondaryLauncherIcons.append("        <Icon Id=\"Launcher");
 632                 secondaryLauncherIcons.append(i);
 633                 secondaryLauncherIcons.append(".exe\" SourceFile=\"");
 634                 secondaryLauncherIcons.append(secondaryLauncherIconPath);
 635                 secondaryLauncherIcons.append("\" />\r\n");
 636             }
 637         }
 638         data.put("SECONDARY_LAUNCHER_ICONS", secondaryLauncherIcons.toString());
 639 
 640         Writer w = new BufferedWriter(new FileWriter(getConfig_ProjectFile(params)));
 641         w.write(preprocessTextResource(
 642                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ProjectFile(params).getName(),
 643                 I18N.getString("resource.wix-config-file"),
 644                 MSI_PROJECT_TEMPLATE, data, VERBOSE.fetchFrom(params),
 645                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 646         w.close();
 647         return true;
 648     }
 649     private int id;
 650     private int compId;
 651     private final static String LAUNCHER_ID = "LauncherId";
 652     private final static String LAUNCHER_SVC_ID = "LauncherSvcId";
 653 
 654     /**
 655      * Overrides the dialog sequence in built-in dialog set "WixUI_InstallDir"
 656      * to exclude license dialog
 657      */
 658     private static final String TWEAK_FOR_EXCLUDING_LICENSE =
 659         "     <Publish Dialog=\"WelcomeDlg\" Control=\"Next\"" +
 660         "              Event=\"NewDialog\" Value=\"InstallDirDlg\" Order=\"2\"> 1" +
 661         "     </Publish>\n" +
 662         "     <Publish Dialog=\"InstallDirDlg\" Control=\"Back\"" +
 663         "              Event=\"NewDialog\" Value=\"WelcomeDlg\" Order=\"2\"> 1" +
 664         "     </Publish>\n";
 665 
 666     /**
 667      * Creates UI element using WiX built-in dialog sets - WixUI_InstallDir/WixUI_Minimal.
 668      * The dialog sets are the closest to what we want to implement.
 669      *
 670      * WixUI_Minimal for license dialog only
 671      * WixUI_InstallDir for installdir dialog only or for both installdir/license dialogs
 672      */
 673     private String getUIBlock(Map<String, ? super Object> params) {
 674         String uiBlock = "     <UI/>\n"; // UI-less element
 675 
 676         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 677             boolean enableTweakForExcludingLicense = (getLicenseFile(params) == null);
 678             uiBlock =
 679                 "     <UI>\n" +
 680                 "     <Property Id=\"WIXUI_INSTALLDIR\" Value=\"APPLICATIONFOLDER\" />\n" +
 681                 "     <UIRef Id=\"WixUI_InstallDir\" />\n" +
 682                 (enableTweakForExcludingLicense ? TWEAK_FOR_EXCLUDING_LICENSE : "") +
 683                 "     </UI>\n";
 684         } else if (getLicenseFile(params) != null) {
 685             uiBlock =
 686                 "     <UI>\n" +
 687                 "     <UIRef Id=\"WixUI_Minimal\" />\n" +
 688                 "     </UI>\n";
 689         }
 690 
 691         return uiBlock;
 692     }
 693 
 694     private String getAppCDSBlock(Map<String, ? super Object> params) {
 695         String cdsBlock = "";
 696         if (UNLOCK_COMMERCIAL_FEATURES.fetchFrom(params) && ENABLE_APP_CDS.fetchFrom(params)
 697                         && ("install".equals(APP_CDS_CACHE_MODE.fetchFrom(params))
 698                         || "auto+install".equals(APP_CDS_CACHE_MODE.fetchFrom(params))))
 699         {
 700             cdsBlock =
 701                     "     <CustomAction Id=\"CACHE_CDS\"\n" +
 702                     "          Directory=\"APPLICATIONFOLDER\"\n" +
 703                     "          ExeCommand=\"[APPLICATIONFOLDER]" + WinAppBundler.getLauncherName(params) +" -Xappcds:generatecache\"\n" +
 704                     "          Execute=\"commit\"\n" +
 705                     "          Return=\"check\"/>\n" +
 706                     "\n" +
 707                     "     <InstallExecuteSequence>\n" +
 708                     "         <Custom Action=\"CACHE_CDS\" Before=\"InstallFinalize\" />\n" +
 709                     "     </InstallExecuteSequence>";
 710         }
 711         return cdsBlock;
 712     }
 713 
 714     private void walkFileTree(Map<String, ? super Object> params, File root, PrintStream out, String prefix) {
 715         List<File> dirs = new ArrayList<>();
 716         List<File> files = new ArrayList<>();
 717 
 718         if (!root.isDirectory()) {
 719             throw new RuntimeException(
 720                     MessageFormat.format(I18N.getString("error.cannot-walk-directory"), root.getAbsolutePath()));
 721         }
 722 
 723         //sort to files and dirs
 724         File[] children = root.listFiles();
 725         if (children != null) {
 726             for (File f : children) {
 727                 if (f.isDirectory()) {
 728                     dirs.add(f);
 729                 } else {
 730                     files.add(f);
 731                 }
 732             }
 733         }
 734 
 735         //have files => need to output component
 736         out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
 737                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
 738                 + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 739         out.println(prefix + "  <CreateFolder/>");
 740         out.println(prefix + "  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 741 
 742         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
 743         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 744         File launcherFile = new File(imageRootDir, WinAppBundler.getLauncherName(params));
 745         File launcherSvcFile = WinServiceBundler.getLauncherSvc(
 746                                 imageRootDir, params);
 747 
 748         //Find out if we need to use registry. We need it if
 749         //  - we doing user level install as file can not serve as KeyPath
 750         //  - if we adding shortcut in this component
 751 
 752         for (File f: files) {
 753             boolean isLauncher = f.equals(launcherFile);
 754             if (isLauncher) {
 755                 needRegistryKey = true;
 756             }
 757         }
 758 
 759         if (needRegistryKey) {
 760             //has to be under HKCU to make WiX happy
 761             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
 762                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
 763                     + APP_NAME.fetchFrom(params) + "\""
 764                     + (CAN_USE_WIX36.fetchFrom(params)
 765                     ? ">" : " Action=\"createAndRemoveOnUninstall\">"));
 766             out.println(prefix + "     <RegistryValue Name=\"Version\" Value=\""
 767                     + VERSION.fetchFrom(params) + "\" Type=\"string\" KeyPath=\"yes\"/>");
 768             out.println(prefix + "   </RegistryKey>");
 769         }
 770 
 771         boolean menuShortcut = MENU_HINT.fetchFrom(params);
 772         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 773 
 774         Map<String, String> idToFileMap = new TreeMap<>();
 775         boolean launcherSet = false;
 776 
 777         for (File f : files) {
 778             boolean isLauncher = f.equals(launcherFile);
 779             boolean isLauncherSvc = f.equals(launcherSvcFile);
 780 
 781             // skip executable for service, will be covered by new component entry
 782             if (isLauncherSvc) {
 783                 continue;
 784             }
 785             launcherSet = launcherSet || isLauncher;
 786 
 787             boolean doShortcuts = isLauncher && (menuShortcut || desktopShortcut);
 788 
 789             String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++));
 790             idToFileMap.put(f.getName(), thisFileId);
 791 
 792             out.println(prefix + "   <File Id=\"" +
 793                     thisFileId + "\""
 794                     + " Name=\"" + f.getName() + "\" "
 795                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
 796                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "") + ">");
 797             if (doShortcuts && desktopShortcut) {
 798                 out.println(prefix + "  <Shortcut Id=\"desktopShortcut\" Directory=\"DesktopFolder\""
 799                         + " Name=\"" + APP_NAME.fetchFrom(params) + "\" WorkingDirectory=\"INSTALLDIR\""
 800                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe\" IconIndex=\"0\" />");
 801             }
 802             if (doShortcuts && menuShortcut) {
 803                 out.println(prefix + "     <Shortcut Id=\"ExeShortcut\" Directory=\"ProgramMenuDir\""
 804                         + " Name=\"" + APP_NAME.fetchFrom(params)
 805                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\" IconIndex=\"0\" />");
 806             }
 807 
 808             List<Map<String, ? super Object>> secondaryLaunchers = SECONDARY_LAUNCHERS.fetchFrom(params);
 809             for (int i = 0; i < secondaryLaunchers.size(); i++) {
 810                 Map<String, ? super Object> sl = secondaryLaunchers.get(i);
 811                 File secondaryLauncherFile = new File(imageRootDir, WinAppBundler.getLauncherName(sl));
 812                 if (f.equals(secondaryLauncherFile)) {
 813                     if (SHORTCUT_HINT.fetchFrom(sl)) {
 814                         out.println(prefix + "  <Shortcut Id=\"desktopShortcut" + i + "\" Directory=\"DesktopFolder\""
 815                                 + " Name=\"" + APP_NAME.fetchFrom(sl) + "\" WorkingDirectory=\"INSTALLDIR\""
 816                                 + " Advertise=\"no\" Icon=\"Launcher" + i + ".exe\" IconIndex=\"0\" />");
 817                     }
 818                     if (MENU_HINT.fetchFrom(sl)) {
 819                         out.println(prefix + "     <Shortcut Id=\"ExeShortcut" + i + "\" Directory=\"ProgramMenuDir\""
 820                                 + " Name=\"" + APP_NAME.fetchFrom(sl)
 821                                 + "\" Advertise=\"no\" Icon=\"Launcher" + i + ".exe\" IconIndex=\"0\" />");
 822                         //Should we allow different menu groups?  Not for now.
 823                     }
 824                 }
 825             }
 826             out.println(prefix + "   </File>");
 827         }
 828 
 829         if (launcherSet) {
 830             List<Map<String, ? super Object>> fileAssociations = FILE_ASSOCIATIONS.fetchFrom(params);
 831             String regName = APP_REGISTRY_NAME.fetchFrom(params);
 832             Set<String> defaultedMimes = new TreeSet<>();
 833             int count = 0;
 834             for (Map<String, ? super Object> fileAssociation : fileAssociations) {
 835                 String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 836                 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 837                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 838                 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 839 
 840                 String mime = (mimeTypes == null || mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
 841 
 842                 if (extensions == null) {
 843                     Log.info(I18N.getString("message.creating-association-with-null-extension"));
 844 
 845                     String entryName = regName + "File";
 846                     if (count > 0) {
 847                         entryName += "." + count;
 848                     }
 849                     count++;
 850                     out.print(prefix + "   <ProgId Id='" + entryName + "' Description='" + description + "'");
 851                     if (icon != null && icon.exists()) {
 852                         out.print(" Icon='" + idToFileMap.get(icon.getName()) + "' IconIndex='0'");
 853                     }
 854                     out.println(" />");
 855                 } else {
 856                     for (String ext : extensions) {
 857                         String entryName = regName + "File";
 858                         if (count > 0) {
 859                             entryName += "." + count;
 860                         }
 861                         count++;
 862 
 863                         out.print(prefix + "   <ProgId Id='" + entryName + "' Description='" + description + "'");
 864                         if (icon != null && icon.exists()) {
 865                             out.print(" Icon='" + idToFileMap.get(icon.getName()) + "' IconIndex='0'");
 866                         }
 867                         out.println(">");
 868 
 869                         if (extensions == null) {
 870                             Log.info(I18N.getString("message.creating-association-with-null-extension"));
 871                         } else {
 872                             out.print(prefix + "    <Extension Id='" + ext + "' Advertise='no'");
 873                             if (mime == null) {
 874                                 out.println(">");
 875                             } else {
 876                                 out.println(" ContentType='" + mime + "'>");
 877                                 if (!defaultedMimes.contains(mime)) {
 878                                     out.println(prefix + "      <MIME ContentType='" + mime + "' Default='yes' />");
 879                                     defaultedMimes.add(mime);
 880                                 }
 881                             }
 882                             out.println(prefix + "      <Verb Id='open' Command='Open' TargetFile='" + LAUNCHER_ID + "' Argument='\"%1\"' />");
 883                             out.println(prefix + "    </Extension>");
 884                         }
 885                         out.println(prefix + "   </ProgId>");
 886                     }
 887                 }
 888             }
 889         }
 890 
 891         out.println(prefix + " </Component>");
 892 
 893         // Two components cannot share the same key path value.
 894         // We already have HKCU created with key path set and
 895         // we need to create separate component for ServiceInstall element
 896         // to ensure that key path is also set to the service executable.
 897         //
 898         // http://wixtoolset.org/documentation/manual/v3/xsd/wix/serviceinstall.html
 899 
 900         boolean needServiceEntries = false;
 901         for (File f: files) {
 902             boolean isLauncherSvc = f.equals(launcherSvcFile);
 903             if (isLauncherSvc && SERVICE_HINT.fetchFrom(params)) {
 904                 needServiceEntries = true;
 905             }
 906         }
 907 
 908         if (needServiceEntries) {
 909             out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
 910                     + " Guid=\"" + UUID.randomUUID().toString() + "\""
 911                     + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 912             out.println("  <CreateFolder/>");
 913             out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 914 
 915             out.println(prefix + "   <File Id=\"" + LAUNCHER_SVC_ID + "\""
 916                     + " Name=\"" + launcherSvcFile.getName() + "\" "
 917                     + " Source=\"" + relativePath(imageRootDir, launcherSvcFile) + "\""
 918                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "")
 919                     + " KeyPath=\"yes\">");
 920             out.println(prefix + "   </File>");
 921             out.println(prefix + "   <ServiceInstall Id=\"" + APP_FS_NAME.fetchFrom(params) + "\""
 922                     + " Name=\"" + APP_NAME.fetchFrom(params) + "\""
 923                     + " Description=\"" + DESCRIPTION.fetchFrom(params) + "\""
 924                     + " ErrorControl=\"normal\""
 925                     + " Start=\"" + (RUN_AT_STARTUP.fetchFrom(params) ? "auto" : "demand") + "\""
 926                     + " Type=\"ownProcess\" Vital=\"yes\" Account=\"LocalSystem\""
 927                     + " Arguments='-mainExe \"" + launcherFile.getName() + "\"'/>");
 928 
 929             out.println(prefix + "   <ServiceControl Id=\""+ APP_FS_NAME.fetchFrom(params) + "\""
 930                     + " Name=\"" + APP_NAME.fetchFrom(params) + "\""
 931                     + (START_ON_INSTALL.fetchFrom(params) ? " Start=\"install\"" : "")
 932                     + (STOP_ON_UNINSTALL.fetchFrom(params) ? " Stop=\"uninstall\"" : "")
 933                     + " Remove=\"uninstall\""
 934                     + " Wait=\"yes\" />");
 935 
 936             out.println(prefix + " </Component>");
 937         }
 938 
 939         for (File d : dirs) {
 940             out.println(prefix + " <Directory Id=\"dirid" + (id++)
 941                     + "\" Name=\"" + d.getName() + "\">");
 942             walkFileTree(params, d, out, prefix + " ");
 943             out.println(prefix + " </Directory>");
 944         }
 945     }
 946 
 947     String getRegistryRoot(Map<String, ? super Object> params) {
 948         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 949             return "HKLM";
 950         } else {
 951             return "HKCU";
 952         }
 953     }
 954 
 955     boolean prepareContentList(Map<String, ? super Object> params) throws FileNotFoundException {
 956         File f = new File(CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
 957         PrintStream out = new PrintStream(f);
 958 
 959         //opening
 960         out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
 961         out.println("<Include>");
 962 
 963         out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
 964         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 965             //install to programfiles
 966             if (BIT_ARCH_64.fetchFrom(params)) {
 967                 out.println("  <Directory Id=\"ProgramFiles64Folder\" Name=\"PFiles\">");
 968             } else {
 969                 out.println("  <Directory Id=\"ProgramFilesFolder\" Name=\"PFiles\">");
 970             }
 971         } else {
 972             //install to user folder
 973             out.println("  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
 974         }
 975         out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
 976                 + APP_NAME.fetchFrom(params) + "\">");
 977 
 978         //dynamic part
 979         id = 0;
 980         compId = 0; //reset counters
 981         walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
 982 
 983         //closing
 984         out.println("   </Directory>");
 985         out.println("  </Directory>");
 986 
 987         //for shortcuts
 988         if (SHORTCUT_HINT.fetchFrom(params)) {
 989             out.println("  <Directory Id=\"DesktopFolder\" />");
 990         }
 991         if (MENU_HINT.fetchFrom(params)) {
 992             out.println("  <Directory Id=\"ProgramMenuFolder\">");
 993             out.println("    <Directory Id=\"ProgramMenuDir\" Name=\"" + MENU_GROUP.fetchFrom(params) + "\">");
 994             out.println("      <Component Id=\"comp" + (compId++) + "\""
 995                     + " Guid=\"" + UUID.randomUUID().toString() + "\""
 996                     + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 997             out.println("        <RemoveFolder Id=\"ProgramMenuDir\" On=\"uninstall\" />");
 998             //This has to be under HKCU to make WiX happy.
 999             //There are numberous discussions on this amoung WiX users
1000             // (if user A installs and user B uninstalls then key is left behind)
1001             //and there are suggested workarounds but none of them are appealing.
1002             //Leave it for now
1003             out.println("         <RegistryValue Root=\"HKCU\" Key=\"Software\\"
1004                     + VENDOR.fetchFrom(params) + "\\" + APP_NAME.fetchFrom(params)
1005                     + "\" Type=\"string\" Value=\"\" />");
1006             out.println("      </Component>");
1007             out.println("    </Directory>");
1008             out.println(" </Directory>");
1009         }
1010 
1011         out.println(" </Directory>");
1012 
1013         out.println(" <Feature Id=\"DefaultFeature\" Title=\"Main Feature\" Level=\"1\">");
1014         for (int j = 0; j < compId; j++) {
1015             out.println("    <ComponentRef Id=\"comp" + j + "\" />");
1016         }
1017         //component is defined in the template.wsx
1018         out.println("    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
1019         out.println(" </Feature>");
1020         out.println("</Include>");
1021 
1022         out.close();
1023         return true;
1024     }
1025 
1026     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
1027         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".wxs");
1028     }
1029 
1030     private String getLicenseFile(Map<String, ? super Object> params) {
1031         List<String> licenseFiles = LICENSE_FILE.fetchFrom(params);
1032         if (licenseFiles == null || licenseFiles.isEmpty()) {
1033             return null;
1034         } else {
1035             return licenseFiles.get(0);
1036         }
1037     }
1038 
1039     private boolean prepareWiXConfig(Map<String, ? super Object> params) throws IOException {
1040         return prepareMainProjectFile(params) && prepareContentList(params);
1041 
1042     }
1043     private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
1044     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
1045 
1046     private File buildMSI(Map<String, ? super Object> params, File outdir) throws IOException {
1047         File tmpDir = new File(BUILD_ROOT.fetchFrom(params), "tmp");
1048         File candleOut = new File(tmpDir, APP_NAME.fetchFrom(params) +".wixobj");
1049         File msiOut = new File(outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
1050 
1051         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-msi-config"), msiOut.getAbsolutePath()));
1052 
1053         msiOut.getParentFile().mkdirs();
1054 
1055         //run candle
1056         ProcessBuilder pb = new ProcessBuilder(
1057                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
1058                 "-nologo",
1059                 getConfig_ProjectFile(params).getAbsolutePath(),
1060                 "-ext", "WixUtilExtension",
1061                 "-out", candleOut.getAbsolutePath());
1062         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1063         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
1064 
1065         Log.verbose(MessageFormat.format(I18N.getString("message.generating-msi"), msiOut.getAbsolutePath()));
1066 
1067         boolean enableLicenseUI = (getLicenseFile(params) != null);
1068         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
1069 
1070         List<String> commandLine = new ArrayList<>();
1071 
1072         commandLine.add(TOOL_LIGHT_EXECUTABLE.fetchFrom(params));
1073         if (enableLicenseUI) {
1074             commandLine.add("-dWixUILicenseRtf="+getLicenseFile(params));
1075         }
1076         commandLine.add("-nologo");
1077         commandLine.add("-spdb");
1078         commandLine.add("-sice:60"); //ignore warnings due to "missing launcguage info" (ICE60)
1079         commandLine.add(candleOut.getAbsolutePath());
1080         commandLine.add("-ext");
1081         commandLine.add("WixUtilExtension");
1082         if (enableLicenseUI || enableInstalldirUI) {
1083             commandLine.add("-ext");
1084             commandLine.add("WixUIExtension.dll");
1085         }
1086         commandLine.add("-out");
1087         commandLine.add(msiOut.getAbsolutePath());
1088 
1089         //create .msi
1090         pb = new ProcessBuilder(commandLine);
1091 
1092         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1093         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
1094 
1095         candleOut.delete();
1096         IOUtils.deleteRecursive(tmpDir);
1097 
1098         return msiOut;
1099     }
1100 
1101     public static void ensureByMutationFileIsRTF(File f) {
1102         if (f == null || !f.isFile()) return;
1103 
1104         try {
1105             boolean existingLicenseIsRTF = false;
1106 
1107             try (FileInputStream fin = new FileInputStream(f)) {
1108                 byte[] firstBits = new byte[7];
1109 
1110                 if (fin.read(firstBits) == firstBits.length) {
1111                     String header = new String(firstBits);
1112                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
1113                 }
1114             }
1115 
1116             if (!existingLicenseIsRTF) {
1117                 List<String> oldLicense = Files.readAllLines(f.toPath());
1118                 try (Writer w = Files.newBufferedWriter(f.toPath(), Charset.forName("Windows-1252"))) {
1119                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" +
1120                             "\\viewkind4\\uc1\\pard\\sa200\\sl276\\slmult1\\lang9\\fs20 ");
1121                     oldLicense.forEach(l -> {
1122                         try {
1123                             for (char c : l.toCharArray()) {
1124                                 //0x00 <= ch < 0x20 Escaped (\'hh)
1125                                 //0x20 <= ch < 0x80 Raw(non - escaped) character
1126                                 //0x80 <= ch <= 0xFF Escaped(\ 'hh)
1127                                 //0x5C, 0x7B, 0x7D (special RTF characters\,{,})Escaped(\'hh)
1128                                 // ch > 0xff Escaped (\\ud###?)
1129                                 if (c < 0x10) {
1130                                     w.write("\\'0");
1131                                     w.write(Integer.toHexString(c));
1132                                 } else if (c > 0xff) {
1133                                     w.write("\\ud");
1134                                     w.write(Integer.toString(c));
1135                                     // \\uc1 is in the header and in effect
1136                                     // so we trail with a replacement character if
1137                                     // the font lacks that character - '?'
1138                                     w.write("?");
1139                                 } else if ((c < 0x20)
1140                                         || (c >= 0x80)
1141                                         || (c == 0x5C) || (c == 0x7B) || (c == 0x7D)) {
1142                                     w.write("\\'");
1143                                     w.write(Integer.toHexString(c));
1144                                 } else {
1145                                     w.write(c);
1146                                 }
1147                             }
1148                             // blank lines are interpreted as paragraph breaks
1149                             if (l.length() < 1) {
1150                                 w.write("\\par");
1151                             } else {
1152                                 w.write(" ");
1153                             }
1154                             w.write("\r\n");
1155                         } catch (IOException e) {
1156                             Log.verbose(e);
1157                         }
1158                     });
1159                     w.write("}\r\n");
1160                 }
1161             }
1162         } catch (IOException e) {
1163             Log.verbose(e);
1164         }
1165 
1166     }
1167 }