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