1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.tools.packager.bundlers;
  27 
  28 import com.oracle.bundlers.AbstractBundler;
  29 import com.oracle.bundlers.BundlerParamInfo;
  30 import com.oracle.bundlers.StandardBundlerParam;
  31 import com.oracle.bundlers.windows.WindowsBundlerParam;
  32 import com.sun.javafx.tools.packager.Log;
  33 import com.sun.javafx.tools.resource.windows.WinResources;
  34 
  35 import java.io.*;
  36 import java.text.MessageFormat;
  37 import java.util.*;
  38 import java.util.regex.Matcher;
  39 import java.util.regex.Pattern;
  40 
  41 import static com.oracle.bundlers.windows.WindowsBundlerParam.*;
  42 
  43 public class WinMsiBundler  extends AbstractBundler {
  44 
  45     private static final ResourceBundle I18N =
  46             ResourceBundle.getBundle("com.oracle.bundlers.windows.WinMsiBundler");
  47 
  48     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
  49             I18N.getString("param.app-bundler.name"),
  50             I18N.getString("param.app-bundler.description"),
  51             "winAppBundler", //KEY
  52             WinAppBundler.class, null, params -> new WinAppBundler(), false, null);
  53 
  54     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 = new WindowsBundlerParam<>(
  55             I18N.getString("param.can-use-wix36.name"),
  56             I18N.getString("param.can-use-wix36.description"),
  57             "canUseWix36", //KEY
  58             Boolean.class, null, params -> false, false, Boolean::valueOf);
  59 
  60     public static final BundlerParamInfo<File> OUT_DIR = new WindowsBundlerParam<>(
  61             I18N.getString("param.out-dir.name"),
  62             I18N.getString("param.out-dir.description"),
  63             "outDir", //KEY
  64             File.class, null, params -> null, false, s -> null);
  65 
  66     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  67             I18N.getString("param.config-root.name"),
  68             I18N.getString("param.config-root.description"),
  69             "configRoot", //KEY
  70             File.class, null,params -> {
  71                 File imagesRoot = new File(StandardBundlerParam.BUILD_ROOT.fetchFrom(params), "windows");
  72                 imagesRoot.mkdirs();
  73                 return imagesRoot;
  74             }, false, s -> null);
  75 
  76     public static final BundlerParamInfo<File> IMAGE_DIR = new WindowsBundlerParam<>(
  77             I18N.getString("param.image-dir.name"),
  78             I18N.getString("param.image-dir.description"),
  79             "imageDir", //KEY
  80             File.class, null, params -> {
  81                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  82                 return new File(imagesRoot, "win-msi");
  83             }, false, s -> null);
  84 
  85     public static final BundlerParamInfo<File> APP_DIR = new WindowsBundlerParam<>(
  86             I18N.getString("param.app-dir.name"),
  87             I18N.getString("param.app-dir.description"),
  88             "appDir",
  89             File.class, null, null, false, s -> null);
  90 
  91     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
  92             new StandardBundlerParam<>(
  93                     I18N.getString("param.system-wide.name"),
  94                     I18N.getString("param.system-wide.description"),
  95                     "winmsi" + BundleParams.PARAM_SYSTEM_WIDE, //KEY
  96                     Boolean.class,
  97                     new String[] {BundleParams.PARAM_SYSTEM_WIDE},
  98                     params -> true, // MSIs default to system wide
  99                     false,
 100                     s -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
 101             );
 102 
 103 
 104     public static final BundlerParamInfo<UUID> UPGRADE_UUID = new WindowsBundlerParam<>(
 105             I18N.getString("param.upgrade-uuid.name"),
 106             I18N.getString("param.upgrade-uuid.description"),
 107             "upgradeUUID", //KEY
 108             UUID.class, null, params -> UUID.randomUUID(), // TODO check to see if identifier is a valid UUID during default 
 109             false, UUID::fromString);
 110 
 111     private static final String TOOL_CANDLE = "candle.exe";
 112     private static final String TOOL_LIGHT = "light.exe";
 113     // autodetect just v3.7 and v3.8
 114     private static final String AUTODETECT_DIRS = ";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";
 115 
 116     public static final BundlerParamInfo<String> TOOL_CANDLE_EXECUTABLE = new WindowsBundlerParam<>(
 117             I18N.getString("param.candle-path.name"),
 118             I18N.getString("param.candle-path.description"),
 119             "win.candle.exe", //KEY
 120             String.class, null, params -> {
 121                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 122                     File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
 123                     if (f.isFile()) {
 124                         return f.toString();
 125                     }
 126                 }
 127                 return null;
 128             }, false, null);
 129 
 130     public static final BundlerParamInfo<String> TOOL_LIGHT_EXECUTABLE = new WindowsBundlerParam<>(
 131             I18N.getString("param.light-path.name"),
 132             I18N.getString("param.light-path.descrption"),
 133             "win.light.exe", //KEY
 134             String.class, null, params -> {
 135                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 136                     File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
 137                     if (f.isFile()) {
 138                         return f.toString();
 139                     }
 140                 }
 141                 return null;
 142             }, false, null);
 143 
 144     public WinMsiBundler() {
 145         super();
 146         baseResourceLoader = WinResources.class;
 147     }
 148 
 149 
 150     @Override
 151     public String getName() {
 152         return I18N.getString("bundler.name");
 153     }
 154 
 155     @Override
 156     public String getDescription() {
 157         return I18N.getString("bundler.description");
 158     }
 159 
 160     @Override
 161     public String getID() {
 162         return "msi"; //KEY
 163     }
 164 
 165     @Override
 166     public BundleType getBundleType() {
 167         return BundleType.INSTALLER;
 168     }
 169 
 170     @Override
 171     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 172         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 173         results.addAll(WinAppBundler.getAppBundleParameters());
 174         results.addAll(getMsiBundleParameters());
 175         return results;
 176     }
 177 
 178     public static Collection<BundlerParamInfo<?>> getMsiBundleParameters() {
 179         return Arrays.asList(
 180                 APP_BUNDLER,
 181                 APP_DIR,
 182                 BUILD_ROOT,
 183                 CAN_USE_WIX36,
 184                 //CONFIG_ROOT, // duplicate from getAppBundleParameters
 185                 DESCRIPTION,
 186                 IMAGE_DIR,
 187                 IMAGES_ROOT,
 188                 MENU_GROUP,
 189                 MENU_HINT,
 190                 MSI_SYSTEM_WIDE,
 191                 SHORTCUT_HINT,
 192                 UPGRADE_UUID,
 193                 VENDOR,
 194                 VERSION
 195         );
 196     }
 197 
 198     @Override
 199     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 200         return bundle(params, outputParentDir);
 201     }
 202 
 203     static class VersionExtractor extends PrintStream {
 204         double version = 0f;
 205 
 206         public VersionExtractor() {
 207             super(new ByteArrayOutputStream());
 208         }
 209 
 210         double getVersion() {
 211             if (version == 0f) {
 212                 String content = new String(((ByteArrayOutputStream) out).toByteArray());
 213                 Pattern pattern = Pattern.compile("version (\\d+.\\d+)");
 214                 Matcher matcher = pattern.matcher(content);
 215                 if (matcher.find()) {
 216                     String v = matcher.group(1);
 217                     version = new Double(v);
 218                 }
 219             }
 220             return version;
 221         }
 222     }
 223 
 224     private static double findToolVersion(String toolName) {
 225         try {
 226             if (toolName == null || "".equals(toolName)) return 0f;
 227             
 228             ProcessBuilder pb = new ProcessBuilder(
 229                     toolName,
 230                     "/?");
 231             VersionExtractor ve = new VersionExtractor();
 232             IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output
 233             double version = ve.getVersion();
 234             Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version));
 235             return version;
 236         } catch (Exception e) {
 237             if (Log.isDebug()) {
 238                 Log.verbose(e);
 239             }
 240             return 0f;
 241         }
 242     }
 243 
 244     @Override
 245     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 246         if (p == null) throw new ConfigException(
 247                 I18N.getString("error.parameters-null"), 
 248                 I18N.getString("error.parameters-null.advice"));
 249 
 250         //run basic validation to ensure requirements are met
 251         //we are not interested in return code, only possible exception
 252         APP_BUNDLER.fetchFrom(p).doValidate(p);
 253 
 254         double candleVersion = findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p));
 255         double lightVersion = findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p));
 256 
 257         //WiX 3.0+ is required
 258         double minVersion = 3.0f;
 259         boolean bad = false;
 260 
 261         if (candleVersion < minVersion) {
 262             Log.verbose(MessageFormat.format(I18N.getString("message.wrong-tool-version"), TOOL_CANDLE, candleVersion, minVersion));
 263             bad = true;
 264         }
 265         if (lightVersion < minVersion) {
 266             Log.verbose(MessageFormat.format(I18N.getString("message.wrong-tool-version"), TOOL_LIGHT, lightVersion, minVersion));
 267             bad = true;
 268         }
 269 
 270         if (bad){
 271             throw new ConfigException(
 272                     I18N.getString("error.no-wix-tools"),
 273                     I18N.getString("error.no-wix-tools.advice"));
 274         }
 275 
 276         if (lightVersion >= 3.6f) {
 277             Log.verbose(I18N.getString("message.use-wix36-features"));
 278             p.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
 279         }
 280 
 281         /********* validate bundle parameters *************/
 282 
 283         String version = VERSION.fetchFrom(p);
 284         if (!isVersionStringValid(version)) {
 285             throw new ConfigException(
 286                     MessageFormat.format(I18N.getString("error.version-string-wrong-format"), version),
 287                     I18N.getString("error.version-string-wrong-format.advice"));
 288         }
 289 
 290         return true;
 291     }
 292 
 293     //http://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
 294     //The format of the string is as follows:
 295     //    major.minor.build
 296     //The first field is the major version and has a maximum value of 255.
 297     //The second field is the minor version and has a maximum value of 255.
 298     //The third field is called the build version or the update version and
 299     // has a maximum value of 65,535.
 300     static boolean isVersionStringValid(String v) {
 301         if (v == null) {
 302             return true;
 303         }
 304 
 305         String p[] = v.split("\\.");
 306         if (p.length > 3) {
 307             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 308             return false;
 309         }
 310 
 311         try {
 312             int val = Integer.parseInt(p[0]);
 313             if (val < 0 || val > 255) {
 314                 Log.verbose(I18N.getString("error.version-string-major-out-of-range"));
 315                 return false;
 316             }
 317             if (p.length > 1) {
 318                 val = Integer.parseInt(p[1]);
 319                 if (val < 0 || val > 255) {
 320                     Log.verbose(I18N.getString("error.version-string-minor-out-of-range"));
 321                     return false;
 322                 }
 323             }
 324             if (p.length > 2) {
 325                 val = Integer.parseInt(p[2]);
 326                 if (val < 0 || val > 65535) {
 327                     Log.verbose(I18N.getString("error.version-string-build-out-of-range"));
 328                     return false;
 329                 }
 330             }
 331         } catch (NumberFormatException ne) {
 332             Log.verbose(I18N.getString("error.version-string-part-not-number"));
 333             Log.verbose(ne);
 334             return false;
 335         }
 336 
 337         return true;
 338     }
 339 
 340     private boolean prepareProto(Map<String, ? super Object> p) {
 341         File bundleRoot = IMAGE_DIR.fetchFrom(p);
 342         File appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, bundleRoot, true);
 343         p.put(APP_DIR.getID(), appDir);
 344         return appDir != null;
 345     }
 346 
 347     public File bundle(Map<String, ? super Object> p, File outdir) {
 348         File appDir = APP_DIR.fetchFrom(p);
 349         File imageDir = IMAGE_DIR.fetchFrom(p);
 350         try {
 351             imageDir.mkdirs();
 352 
 353             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 354             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 355             if (!menuShortcut && !desktopShortcut) {
 356                 //both can not be false - user will not find the app
 357                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 358                 p.put(MENU_HINT.getID(), true);
 359             }
 360 
 361             if (prepareProto(p) && prepareWiXConfig(p)
 362                     && prepareBasicProjectConfig(p)) {
 363                 File configScriptSrc = getConfig_Script(p);
 364                 if (configScriptSrc.exists()) {
 365                     //we need to be running post script in the image folder
 366 
 367                     // NOTE: Would it be better to generate it to the image folder
 368                     // and save only if "verbose" is requested?
 369 
 370                     // for now we replicate it
 371                     File configScript = new File(imageDir, configScriptSrc.getName());
 372                     IOUtils.copyFile(configScriptSrc, configScript);
 373                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
 374                     IOUtils.run("wscript", configScript, verbose);
 375                 }
 376                 return buildMSI(p, outdir);
 377             }
 378             return null;
 379         } catch (IOException ex) {
 380             Log.verbose(ex);
 381             return null;
 382         } finally {
 383             try {
 384                 if (imageDir != null && !Log.isDebug()) {
 385                     IOUtils.deleteRecursive(imageDir);
 386                 } else if (imageDir != null) {
 387                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 388                 }
 389                 if (verbose) {
 390                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 391                 } else {
 392                     cleanupConfigFiles(p);
 393                 }
 394             } catch (FileNotFoundException ex) {
 395                 //noinspection ReturnInsideFinallyBlock
 396                 return null;
 397             }
 398         }
 399     }
 400 
 401     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 402         if (getConfig_ProjectFile(params) != null) {
 403             getConfig_ProjectFile(params).delete();
 404         }
 405         if (getConfig_Script(params) != null) {
 406             getConfig_Script(params).delete();
 407         }
 408     }
 409 
 410     //name of post-image script
 411     private File getConfig_Script(Map<String, ? super Object> params) {
 412         return new File(CONFIG_ROOT.fetchFrom(params),
 413                 WinAppBundler.getAppName(params) + "-post-image.wsf");
 414     }
 415 
 416     @Override
 417     public String toString() {
 418         return getName();
 419     }
 420 
 421     private boolean prepareBasicProjectConfig(Map<String, ? super Object> params) throws IOException {
 422         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 423                 I18N.getString("resource.post-install-script"),
 424                 (String) null,
 425                 getConfig_Script(params));
 426         return true;
 427     }
 428 
 429     private String relativePath(File basedir, File file) {
 430         return file.getAbsolutePath().substring(
 431                 basedir.getAbsolutePath().length() + 1);
 432     }
 433 
 434     boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException {
 435         Map<String, String> data = new HashMap<>();
 436 
 437         UUID productGUID = UUID.randomUUID();
 438 
 439         Log.verbose(MessageFormat.format(I18N.getString("message.generated-product-guid"), productGUID.toString()));
 440 
 441         //we use random GUID for product itself but
 442         // user provided for upgrade guid
 443         // Upgrade guid is important to decide whether it is upgrade of installed
 444         //  app. I.e. we need it to be the same for 2 different versions of app if possible
 445         data.put("PRODUCT_GUID", productGUID.toString());
 446         data.put("PRODUCT_UPGRADE_GUID", UPGRADE_UUID.fetchFrom(params).toString());
 447 
 448         data.put("APPLICATION_NAME", WinAppBundler.getAppName(params));
 449         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 450         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 451         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
 452 
 453         //WinAppBundler will add application folder again => step out
 454         File imageRootDir = APP_DIR.fetchFrom(params);
 455         File launcher = WinAppBundler.getLauncher(
 456                 imageRootDir.getParentFile(), params);
 457 
 458         String launcherPath = relativePath(imageRootDir, launcher);
 459         data.put("APPLICATION_LAUNCHER", launcherPath);
 460 
 461         String iconPath = launcherPath.replace(".exe", ".ico");
 462 
 463         data.put("APPLICATION_ICON", iconPath);
 464 
 465         data.put("REGISTRY_ROOT", getRegistryRoot(params));
 466 
 467         boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
 468         data.put("WIX36_ONLY_START",
 469                 canUseWix36Features ? "" : "<!--");
 470         data.put("WIX36_ONLY_END",
 471                 canUseWix36Features ? "" : "-->");
 472 
 473         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 474             data.put("INSTALL_SCOPE", "perMachine");
 475         } else {
 476             data.put("INSTALL_SCOPE", "perUser");
 477         }
 478 
 479         if (BIT_ARCH_64.fetchFrom(params)) {
 480             data.put("PLATFORM", "x64");
 481             data.put("WIN64", "yes");
 482         } else {
 483             data.put("PLATFORM", "x86");
 484             data.put("WIN64", "no");
 485         }
 486 
 487         Writer w = new BufferedWriter(new FileWriter(getConfig_ProjectFile(params)));
 488         w.write(preprocessTextResource(
 489                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ProjectFile(params).getName(),
 490                 I18N.getString("resource.wix-config-file"), 
 491                 MSI_PROJECT_TEMPLATE, data));
 492         w.close();
 493         return true;
 494     }
 495     private int id;
 496     private int compId;
 497     private final static String LAUNCHER_ID = "LauncherId";
 498 
 499     private void walkFileTree(Map<String, ? super Object> params, File root, PrintStream out, String prefix) {
 500         List<File> dirs = new ArrayList<>();
 501         List<File> files = new ArrayList<>();
 502 
 503         if (!root.isDirectory()) {
 504             throw new RuntimeException(
 505                     MessageFormat.format(I18N.getString("error.cannot-walk-directory"), root.getAbsolutePath()));
 506         }
 507 
 508         //sort to files and dirs
 509         File[] children = root.listFiles();
 510         if (children != null) {
 511             for (File f : children) {
 512                 if (f.isDirectory()) {
 513                     dirs.add(f);
 514                 } else {
 515                     files.add(f);
 516                 }
 517             }
 518         }
 519 
 520         //have files => need to output component
 521         out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
 522                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
 523                 + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 524         out.println("  <CreateFolder/>");
 525         out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 526 
 527         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
 528         File imageRootDir = APP_DIR.fetchFrom(params);
 529         File launcherFile = WinAppBundler.getLauncher(
 530                 /* Step up as WinAppBundler will add app folder */
 531                 imageRootDir.getParentFile(), params);
 532         //Find out if we need to use registry. We need it if
 533         //  - we doing user level install as file can not serve as KeyPath
 534         //  - if we adding shortcut in this component
 535         for (File f: files) {
 536             boolean isLauncher = f.equals(launcherFile);
 537             if (isLauncher) {
 538                 needRegistryKey = true;
 539             }
 540         }
 541 
 542         if (needRegistryKey) {
 543             //has to be under HKCU to make WiX happy
 544             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
 545                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
 546                     + WinAppBundler.getAppName(params) + "\""
 547                     + (CAN_USE_WIX36.fetchFrom(params)
 548                     ? ">" : " Action=\"createAndRemoveOnUninstall\">"));
 549             out.println(prefix + "     <RegistryValue Name=\"Version\" Value=\""
 550                     + VERSION.fetchFrom(params) + "\" Type=\"string\" KeyPath=\"yes\"/>");
 551             out.println(prefix + "   </RegistryKey>");
 552         }
 553 
 554         boolean menuShortcut = MENU_HINT.fetchFrom(params);
 555         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 556         for (File f : files) {
 557             boolean isLauncher = f.equals(launcherFile);
 558             boolean doShortcuts = isLauncher && (menuShortcut || desktopShortcut);
 559             out.println(prefix + "   <File Id=\"" +
 560                     (isLauncher ? LAUNCHER_ID : ("FileId" + (id++))) + "\""
 561                     + " Name=\"" + f.getName() + "\" "
 562                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
 563                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "") + ">");
 564             if (doShortcuts && desktopShortcut) {
 565                 out.println(prefix + "  <Shortcut Id=\"desktopShortcut\" Directory=\"DesktopFolder\""
 566                         + " Name=\"" + WinAppBundler.getAppName(params) + "\" WorkingDirectory=\"INSTALLDIR\""
 567                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe\" IconIndex=\"0\" />");
 568             }
 569             if (doShortcuts && menuShortcut) {
 570                 out.println(prefix + "     <Shortcut Id=\"ExeShortcut\" Directory=\"ProgramMenuDir\""
 571                         + " Name=\"" + WinAppBundler.getAppName(params)
 572                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\" IconIndex=\"0\" />");
 573             }
 574             out.println(prefix + "   </File>");
 575         }
 576         out.println(prefix + " </Component>");
 577 
 578         for (File d : dirs) {
 579             out.println(prefix + " <Directory Id=\"dirid" + (id++)
 580                     + "\" Name=\"" + d.getName() + "\">");
 581             walkFileTree(params, d, out, prefix + " ");
 582             out.println(prefix + " </Directory>");
 583         }
 584     }
 585 
 586     String getRegistryRoot(Map<String, ? super Object> params) {
 587         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 588             return "HKLM";
 589         } else {
 590             return "HKCU";
 591         }
 592     }
 593 
 594     boolean prepareContentList(Map<String, ? super Object> params) throws FileNotFoundException {
 595         File f = new File(CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
 596         PrintStream out = new PrintStream(f);
 597 
 598         //opening
 599         out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
 600         out.println("<Include>");
 601 
 602         out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
 603         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 604             //install to programfiles
 605             if (BIT_ARCH_64.fetchFrom(params)) {
 606                 out.println("  <Directory Id=\"ProgramFiles64Folder\" Name=\"PFiles\">");
 607             } else {
 608                 out.println("  <Directory Id=\"ProgramFilesFolder\" Name=\"PFiles\">");
 609             }
 610         } else {
 611             //install to user folder
 612             out.println("  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
 613         }
 614         out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
 615                 + WinAppBundler.getAppName(params) + "\">");
 616 
 617         //dynamic part
 618         id = 0;
 619         compId = 0; //reset counters
 620         walkFileTree(params, APP_DIR.fetchFrom(params), out, "    ");
 621 
 622         //closing
 623         out.println("   </Directory>");
 624         out.println("  </Directory>");
 625 
 626         //for shortcuts
 627         if (SHORTCUT_HINT.fetchFrom(params)) {
 628             out.println("  <Directory Id=\"DesktopFolder\" />");
 629         }
 630         if (MENU_HINT.fetchFrom(params)) {
 631             out.println("  <Directory Id=\"ProgramMenuFolder\">");
 632             out.println("    <Directory Id=\"ProgramMenuDir\" Name=\"" + MENU_GROUP.fetchFrom(params) + "\">");
 633             out.println("      <Component Id=\"comp" + (compId++) + "\""
 634                     + " Guid=\"" + UUID.randomUUID().toString() + "\""
 635                     + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 636             out.println("        <RemoveFolder Id=\"ProgramMenuDir\" On=\"uninstall\" />");
 637             //This has to be under HKCU to make WiX happy.
 638             //There are numberous discussions on this amoung WiX users
 639             // (if user A installs and user B uninstalls then key is left behind)
 640             //and there are suggested workarounds but none of them are appealing.
 641             //Leave it for now
 642             out.println("         <RegistryValue Root=\"HKCU\" Key=\"Software\\"
 643                     + VENDOR.fetchFrom(params) + "\\" + WinAppBundler.getAppName(params)
 644                     + "\" Type=\"string\" Value=\"\" />");
 645             out.println("      </Component>");
 646             out.println("    </Directory>");
 647             out.println(" </Directory>");
 648         }
 649 
 650         out.println(" </Directory>");
 651 
 652         out.println(" <Feature Id=\"DefaultFeature\" Title=\"Main Feature\" Level=\"1\">");
 653         for (int j = 0; j < compId; j++) {
 654             out.println("    <ComponentRef Id=\"comp" + j + "\" />");
 655         }
 656         //component is defined in the template.wsx
 657         out.println("    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
 658         out.println(" </Feature>");
 659         out.println("</Include>");
 660 
 661         out.close();
 662         return true;
 663     }
 664 
 665     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
 666         return new File(CONFIG_ROOT.fetchFrom(params), WinAppBundler.getAppName(params) + ".wxs");
 667     }
 668 
 669     private boolean prepareWiXConfig(Map<String, ? super Object> params) throws IOException {
 670         return prepareMainProjectFile(params) && prepareContentList(params);
 671 
 672     }
 673     private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
 674     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
 675 
 676     private File buildMSI(Map<String, ? super Object> params, File outdir) throws IOException {
 677         File tmpDir = new File(BUILD_ROOT.fetchFrom(params), "tmp");
 678         File candleOut = new File(tmpDir, WinAppBundler.getAppName(params)+".wixobj");
 679         File msiOut = new File(outdir, WinAppBundler.getAppName(params)
 680                 + "-" + VERSION.fetchFrom(params) + ".msi");
 681 
 682         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-msi-config"), msiOut.getAbsolutePath()));
 683 
 684         msiOut.getParentFile().mkdirs();
 685 
 686         //run candle
 687         ProcessBuilder pb = new ProcessBuilder(
 688                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
 689                 "-nologo",
 690                 getConfig_ProjectFile(params).getAbsolutePath(),
 691                 "-ext", "WixUtilExtension",
 692                 "-out", candleOut.getAbsolutePath());
 693         pb = pb.directory(APP_DIR.fetchFrom(params));
 694         IOUtils.exec(pb, verbose);
 695 
 696         Log.verbose(MessageFormat.format(I18N.getString("message.generating-msi"), msiOut.getAbsolutePath()));
 697 
 698         //create .msi
 699         pb = new ProcessBuilder(
 700                 TOOL_LIGHT_EXECUTABLE.fetchFrom(params),
 701                 "-nologo",
 702                 "-spdb",
 703                 "-sice:60", //ignore warnings due to "missing launcguage info" (ICE60)
 704                 candleOut.getAbsolutePath(),
 705                 "-ext", "WixUtilExtension",
 706                 "-out", msiOut.getAbsolutePath());
 707         pb = pb.directory(APP_DIR.fetchFrom(params));
 708         IOUtils.exec(pb, verbose);
 709 
 710         candleOut.delete();
 711         IOUtils.deleteRecursive(tmpDir);
 712 
 713         return msiOut;
 714     }
 715 }