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