modules/fxpackager/src/main/java/com/sun/javafx/tools/packager/bundlers/WinMsiBundler.java

Print this page




  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() {


 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) {


 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 


 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 


 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.


 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 }


  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.StandardBundlerParam.DESCRIPTION;
  42 import static com.oracle.bundlers.windows.WindowsBundlerParam.*;
  43 
  44 public class WinMsiBundler  extends AbstractBundler {
  45 
  46     private static final ResourceBundle I18N =
  47             ResourceBundle.getBundle("com.oracle.bundlers.windows.WinMsiBundler");
  48 
  49     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
  50             I18N.getString("param.app-bundler.name"),
  51             I18N.getString("param.app-bundler.description"),
  52             "win.app.bundler",
  53             WinAppBundler.class, null, params -> new WinAppBundler(), false, null);
  54 
  55     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 = new WindowsBundlerParam<>(
  56             I18N.getString("param.can-use-wix36.name"),
  57             I18N.getString("param.can-use-wix36.description"),
  58             "win.msi.canUseWix36",
  59             Boolean.class, null, params -> false, false, (s, p) -> Boolean.valueOf(s));






  60 
  61     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  62             I18N.getString("param.config-root.name"),
  63             I18N.getString("param.config-root.description"),
  64             "configRoot",
  65             File.class, null,params -> {
  66                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  67                 imagesRoot.mkdirs();
  68                 return imagesRoot;
  69             }, false, (s, p) -> null);
  70 
  71     public static final BundlerParamInfo<File> MSI_IMAGE_DIR = new WindowsBundlerParam<>(
  72             I18N.getString("param.image-dir.name"),
  73             I18N.getString("param.image-dir.description"),
  74             "win.msi.imageDir",
  75             File.class, null, params -> {
  76                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  77                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  78                 return new File(imagesRoot, "win-msi.image");
  79             }, false, (s, p) -> null);
  80 
  81     public static final BundlerParamInfo<File> WIN_APP_IMAGE = new WindowsBundlerParam<>(
  82             I18N.getString("param.app-dir.name"),
  83             I18N.getString("param.app-dir.description"),
  84             "win.app.image",
  85             File.class, null, null, false, (s, p) -> null);
  86 
  87     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
  88             new StandardBundlerParam<>(
  89                     I18N.getString("param.system-wide.name"),
  90                     I18N.getString("param.system-wide.description"),
  91                     "win.msi." + BundleParams.PARAM_SYSTEM_WIDE,
  92                     Boolean.class,
  93                     new String[] {BundleParams.PARAM_SYSTEM_WIDE},
  94                     params -> true, // MSIs default to system wide
  95                     false,
  96                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
  97             );
  98 
  99 
 100     public static final BundlerParamInfo<UUID> UPGRADE_UUID = new WindowsBundlerParam<>(
 101             I18N.getString("param.upgrade-uuid.name"),
 102             I18N.getString("param.upgrade-uuid.description"),
 103             "win.msi.upgradeUUID",
 104             UUID.class, null, params -> UUID.randomUUID(), // TODO check to see if identifier is a valid UUID during default 
 105             false, (s, p) -> UUID.fromString(s));
 106 
 107     private static final String TOOL_CANDLE = "candle.exe";
 108     private static final String TOOL_LIGHT = "light.exe";
 109     // autodetect just v3.7 and v3.8
 110     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";
 111 
 112     public static final BundlerParamInfo<String> TOOL_CANDLE_EXECUTABLE = new WindowsBundlerParam<>(
 113             I18N.getString("param.candle-path.name"),
 114             I18N.getString("param.candle-path.description"),
 115             "win.msi.candle.exe",
 116             String.class, null, params -> {
 117                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 118                     File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
 119                     if (f.isFile()) {
 120                         return f.toString();
 121                     }
 122                 }
 123                 return null;
 124             }, false, null);
 125 
 126     public static final BundlerParamInfo<String> TOOL_LIGHT_EXECUTABLE = new WindowsBundlerParam<>(
 127             I18N.getString("param.light-path.name"),
 128             I18N.getString("param.light-path.descrption"),
 129             "win.msi.light.exe",
 130             String.class, null, params -> {
 131                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
 132                     File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
 133                     if (f.isFile()) {
 134                         return f.toString();
 135                     }
 136                 }
 137                 return null;
 138             }, false, null);
 139 
 140     public WinMsiBundler() {
 141         super();
 142         baseResourceLoader = WinResources.class;
 143     }
 144 
 145 
 146     @Override
 147     public String getName() {
 148         return I18N.getString("bundler.name");
 149     }
 150 
 151     @Override
 152     public String getDescription() {
 153         return I18N.getString("bundler.description");
 154     }
 155 
 156     @Override
 157     public String getID() {
 158         return "msi";
 159     }
 160 
 161     @Override
 162     public BundleType getBundleType() {
 163         return BundleType.INSTALLER;
 164     }
 165 
 166     @Override
 167     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 168         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 169         results.addAll(WinAppBundler.getAppBundleParameters());
 170         results.addAll(getMsiBundleParameters());
 171         return results;
 172     }
 173 
 174     public static Collection<BundlerParamInfo<?>> getMsiBundleParameters() {
 175         return Arrays.asList(
 176                 APP_BUNDLER,
 177                 WIN_APP_IMAGE,
 178                 BUILD_ROOT,
 179                 CAN_USE_WIX36,
 180                 //CONFIG_ROOT, // duplicate from getAppBundleParameters
 181                 DESCRIPTION,
 182                 MSI_IMAGE_DIR,
 183                 IMAGES_ROOT,
 184                 MENU_GROUP,
 185                 MENU_HINT,
 186                 MSI_SYSTEM_WIDE,
 187                 SHORTCUT_HINT,
 188                 UPGRADE_UUID,
 189                 VENDOR,
 190                 VERSION
 191         );
 192     }
 193 
 194     @Override
 195     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 196         return bundle(params, outputParentDir);
 197     }
 198 
 199     static class VersionExtractor extends PrintStream {
 200         double version = 0f;
 201 
 202         public VersionExtractor() {


 222             if (toolName == null || "".equals(toolName)) return 0f;
 223             
 224             ProcessBuilder pb = new ProcessBuilder(
 225                     toolName,
 226                     "/?");
 227             VersionExtractor ve = new VersionExtractor();
 228             IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output
 229             double version = ve.getVersion();
 230             Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version));
 231             return version;
 232         } catch (Exception e) {
 233             if (Log.isDebug()) {
 234                 Log.verbose(e);
 235             }
 236             return 0f;
 237         }
 238     }
 239 
 240     @Override
 241     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 242         try {
 243             if (p == null) throw new ConfigException(
 244                     I18N.getString("error.parameters-null"),
 245                     I18N.getString("error.parameters-null.advice"));
 246 
 247             //run basic validation to ensure requirements are met
 248             //we are not interested in return code, only possible exception
 249             APP_BUNDLER.fetchFrom(p).doValidate(p);
 250 
 251             double candleVersion = findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p));
 252             double lightVersion = findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p));
 253 
 254             //WiX 3.0+ is required
 255             double minVersion = 3.0f;
 256             boolean bad = false;
 257 
 258             if (candleVersion < minVersion) {
 259                 Log.verbose(MessageFormat.format(I18N.getString("message.wrong-tool-version"), TOOL_CANDLE, candleVersion, minVersion));
 260                 bad = true;
 261             }
 262             if (lightVersion < minVersion) {


 268                 throw new ConfigException(
 269                         I18N.getString("error.no-wix-tools"),
 270                         I18N.getString("error.no-wix-tools.advice"));
 271             }
 272 
 273             if (lightVersion >= 3.6f) {
 274                 Log.verbose(I18N.getString("message.use-wix36-features"));
 275                 p.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
 276             }
 277 
 278             /********* validate bundle parameters *************/
 279 
 280             String version = VERSION.fetchFrom(p);
 281             if (!isVersionStringValid(version)) {
 282                 throw new ConfigException(
 283                         MessageFormat.format(I18N.getString("error.version-string-wrong-format"), version),
 284                         I18N.getString("error.version-string-wrong-format.advice"));
 285             }
 286 
 287             return true;
 288         } catch (RuntimeException re) {
 289             throw new ConfigException(re);
 290         }
 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 


 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 = MSI_IMAGE_DIR.fetchFrom(p);
 342         File appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, bundleRoot, true);
 343         p.put(WIN_APP_IMAGE.getID(), appDir);
 344         return appDir != null;
 345     }
 346 
 347     public File bundle(Map<String, ? super Object> p, File outdir) {
 348         // validate we have valid tools before continuing
 349         String light = TOOL_LIGHT_EXECUTABLE.fetchFrom(p);
 350         String candle = TOOL_CANDLE_EXECUTABLE.fetchFrom(p);
 351         if (light == null || !new File(light).isFile() ||
 352             candle == null || !new File(candle).isFile()) {
 353             Log.info(I18N.getString("error.no-wix-tools"));
 354             Log.info(MessageFormat.format(I18N.getString("message.light-file-string"), light));
 355             Log.info(MessageFormat.format(I18N.getString("message.candle-file-string"), candle));
 356             return null;
 357         }
 358 
 359         File imageDir = MSI_IMAGE_DIR.fetchFrom(p);
 360         try {
 361             imageDir.mkdirs();
 362 
 363             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 364             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 365             if (!menuShortcut && !desktopShortcut) {
 366                 //both can not be false - user will not find the app
 367                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 368                 p.put(MENU_HINT.getID(), true);
 369             }
 370 
 371             if (prepareProto(p) && prepareWiXConfig(p)
 372                     && prepareBasicProjectConfig(p)) {
 373                 File configScriptSrc = getConfig_Script(p);
 374                 if (configScriptSrc.exists()) {
 375                     //we need to be running post script in the image folder
 376 
 377                     // NOTE: Would it be better to generate it to the image folder
 378                     // and save only if "verbose" is requested?
 379 
 380                     // for now we replicate it
 381                     File configScript = new File(imageDir, configScriptSrc.getName());
 382                     IOUtils.copyFile(configScriptSrc, configScript);
 383                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
 384                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
 385                 }
 386                 return buildMSI(p, outdir);
 387             }
 388             return null;
 389         } catch (IOException ex) {
 390             Log.verbose(ex);
 391             return null;
 392         } finally {
 393             try {
 394                 if (imageDir != null && !Log.isDebug()) {
 395                     IOUtils.deleteRecursive(imageDir);
 396                 } else if (imageDir != null) {
 397                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 398                 }
 399                 if (VERBOSE.fetchFrom(p)) {
 400                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 401                 } else {
 402                     cleanupConfigFiles(p);
 403                 }
 404             } catch (FileNotFoundException ex) {
 405                 //noinspection ReturnInsideFinallyBlock
 406                 return null;
 407             }
 408         }
 409     }
 410 
 411     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 412         if (getConfig_ProjectFile(params) != null) {
 413             getConfig_ProjectFile(params).delete();
 414         }
 415         if (getConfig_Script(params) != null) {
 416             getConfig_Script(params).delete();
 417         }
 418     }
 419 
 420     //name of post-image script
 421     private File getConfig_Script(Map<String, ? super Object> params) {
 422         return new File(CONFIG_ROOT.fetchFrom(params),
 423                 WinAppBundler.getAppName(params) + "-post-image.wsf");
 424     }
 425 
 426     @Override
 427     public String toString() {
 428         return getName();
 429     }
 430 
 431     private boolean prepareBasicProjectConfig(Map<String, ? super Object> params) throws IOException {
 432         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 433                 I18N.getString("resource.post-install-script"),
 434                 (String) null,
 435                 getConfig_Script(params),
 436                 VERBOSE.fetchFrom(params));
 437         return true;
 438     }
 439 
 440     private String relativePath(File basedir, File file) {
 441         return file.getAbsolutePath().substring(
 442                 basedir.getAbsolutePath().length() + 1);
 443     }
 444 
 445     boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException {
 446         Map<String, String> data = new HashMap<>();
 447 
 448         UUID productGUID = UUID.randomUUID();
 449 
 450         Log.verbose(MessageFormat.format(I18N.getString("message.generated-product-guid"), productGUID.toString()));
 451 
 452         //we use random GUID for product itself but
 453         // user provided for upgrade guid
 454         // Upgrade guid is important to decide whether it is upgrade of installed
 455         //  app. I.e. we need it to be the same for 2 different versions of app if possible
 456         data.put("PRODUCT_GUID", productGUID.toString());
 457         data.put("PRODUCT_UPGRADE_GUID", UPGRADE_UUID.fetchFrom(params).toString());
 458 
 459         data.put("APPLICATION_NAME", WinAppBundler.getAppName(params));
 460         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 461         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 462         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
 463 
 464         //WinAppBundler will add application folder again => step out
 465         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 466         File launcher = WinAppBundler.getLauncher(
 467                 imageRootDir.getParentFile(), params);
 468 
 469         String launcherPath = relativePath(imageRootDir, launcher);
 470         data.put("APPLICATION_LAUNCHER", launcherPath);
 471 
 472         String iconPath = launcherPath.replace(".exe", ".ico");
 473 
 474         data.put("APPLICATION_ICON", iconPath);
 475 
 476         data.put("REGISTRY_ROOT", getRegistryRoot(params));
 477 
 478         boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
 479         data.put("WIX36_ONLY_START",
 480                 canUseWix36Features ? "" : "<!--");
 481         data.put("WIX36_ONLY_END",
 482                 canUseWix36Features ? "" : "-->");
 483 
 484         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 485             data.put("INSTALL_SCOPE", "perMachine");
 486         } else {
 487             data.put("INSTALL_SCOPE", "perUser");
 488         }
 489 
 490         if (BIT_ARCH_64.fetchFrom(params)) {
 491             data.put("PLATFORM", "x64");
 492             data.put("WIN64", "yes");
 493         } else {
 494             data.put("PLATFORM", "x86");
 495             data.put("WIN64", "no");
 496         }
 497 
 498         Writer w = new BufferedWriter(new FileWriter(getConfig_ProjectFile(params)));
 499         w.write(preprocessTextResource(
 500                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ProjectFile(params).getName(),
 501                 I18N.getString("resource.wix-config-file"), 
 502                 MSI_PROJECT_TEMPLATE, data, VERBOSE.fetchFrom(params)));
 503         w.close();
 504         return true;
 505     }
 506     private int id;
 507     private int compId;
 508     private final static String LAUNCHER_ID = "LauncherId";
 509     private final static String LAUNCHER_SVC_ID = "LauncherSvcId";
 510 
 511     private void walkFileTree(Map<String, ? super Object> params, File root, PrintStream out, String prefix) {
 512         List<File> dirs = new ArrayList<>();
 513         List<File> files = new ArrayList<>();
 514 
 515         if (!root.isDirectory()) {
 516             throw new RuntimeException(
 517                     MessageFormat.format(I18N.getString("error.cannot-walk-directory"), root.getAbsolutePath()));
 518         }
 519 
 520         //sort to files and dirs
 521         File[] children = root.listFiles();
 522         if (children != null) {
 523             for (File f : children) {
 524                 if (f.isDirectory()) {
 525                     dirs.add(f);
 526                 } else {
 527                     files.add(f);
 528                 }
 529             }
 530         }
 531 
 532         //have files => need to output component
 533         out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
 534                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
 535                 + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 536         out.println("  <CreateFolder/>");
 537         out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 538 
 539         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
 540         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 541         File launcherFile = WinAppBundler.getLauncher(
 542                 /* Step up as WinAppBundler will add app folder */
 543                 imageRootDir.getParentFile(), params);
 544         File launcherSvcFile = WinAppBundler.getLauncherSvc(
 545                                 imageRootDir.getParentFile(), params);
 546 
 547         //Find out if we need to use registry. We need it if
 548         //  - we doing user level install as file can not serve as KeyPath
 549         //  - if we adding shortcut in this component
 550 
 551         for (File f: files) {
 552             boolean isLauncher = f.equals(launcherFile);
 553             if (isLauncher) {
 554                 needRegistryKey = true;
 555             }
 556         }
 557 
 558         if (needRegistryKey) {
 559             //has to be under HKCU to make WiX happy
 560             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
 561                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
 562                     + WinAppBundler.getAppName(params) + "\""
 563                     + (CAN_USE_WIX36.fetchFrom(params)
 564                     ? ">" : " Action=\"createAndRemoveOnUninstall\">"));
 565             out.println(prefix + "     <RegistryValue Name=\"Version\" Value=\""
 566                     + VERSION.fetchFrom(params) + "\" Type=\"string\" KeyPath=\"yes\"/>");
 567             out.println(prefix + "   </RegistryKey>");
 568         }
 569 
 570         boolean menuShortcut = MENU_HINT.fetchFrom(params);
 571         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 572         for (File f : files) {
 573             boolean isLauncher = f.equals(launcherFile);
 574             boolean isLauncherSvc = f.equals(launcherSvcFile);
 575 
 576             // skip executable for service, will be covered by new component entry
 577             if (isLauncherSvc) {
 578                 continue;
 579             }
 580 
 581             boolean doShortcuts = isLauncher && (menuShortcut || desktopShortcut);
 582 
 583             out.println(prefix + "   <File Id=\"" +
 584                     (isLauncher ? LAUNCHER_ID : ("FileId" + (id++))) + "\""
 585                     + " Name=\"" + f.getName() + "\" "
 586                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
 587                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "") + ">");
 588             if (doShortcuts && desktopShortcut) {
 589                 out.println(prefix + "  <Shortcut Id=\"desktopShortcut\" Directory=\"DesktopFolder\""
 590                         + " Name=\"" + WinAppBundler.getAppName(params) + "\" WorkingDirectory=\"INSTALLDIR\""
 591                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe\" IconIndex=\"0\" />");
 592             }
 593             if (doShortcuts && menuShortcut) {
 594                 out.println(prefix + "     <Shortcut Id=\"ExeShortcut\" Directory=\"ProgramMenuDir\""
 595                         + " Name=\"" + WinAppBundler.getAppName(params)
 596                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\" IconIndex=\"0\" />");
 597             }
 598             out.println(prefix + "   </File>");
 599         }
 600         out.println(prefix + " </Component>");
 601 
 602         // Two components cannot share the same key path value.
 603         // We already have HKCU created with key path set and
 604         // we need to create separate component for ServiceInstall element
 605         // to ensure that key path is also set to the service executable.
 606         //
 607         // http://wixtoolset.org/documentation/manual/v3/xsd/wix/serviceinstall.html
 608 
 609         boolean needServiceEntries = false;
 610         for (File f: files) {
 611             boolean isLauncherSvc = f.equals(launcherSvcFile);
 612             if (isLauncherSvc && SERVICE_HINT.fetchFrom(params)) {
 613                 needServiceEntries = true;
 614             }
 615         }
 616 
 617         if (needServiceEntries) {
 618             out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
 619                     + " Guid=\"" + UUID.randomUUID().toString() + "\""
 620                     + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 621             out.println("  <CreateFolder/>");
 622             out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 623 
 624             out.println(prefix + "   <File Id=\"" + LAUNCHER_SVC_ID + "\""
 625                     + " Name=\"" + launcherSvcFile.getName() + "\" "
 626                     + " Source=\"" + relativePath(imageRootDir, launcherSvcFile) + "\""
 627                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "")
 628                     + " KeyPath=\"yes\">");
 629             out.println(prefix + "   </File>");
 630             out.println(prefix + "   <ServiceInstall Id=\"" + WinAppBundler.getAppName(params) + "\""
 631                     + " Name=\"" + WinAppBundler.getAppName(params) + "\""
 632                     + " Description=\"" + DESCRIPTION.fetchFrom(params) + "\""
 633                     + " ErrorControl=\"normal\""
 634                     + " Start=\"" + (RUN_AT_STARTUP.fetchFrom(params) ? "auto" : "demand") + "\""
 635                     + " Type=\"ownProcess\" Vital=\"yes\" Account=\"LocalSystem\""
 636                     + " Arguments=\"-mainExe " + launcherFile.getName() + "\"/>");
 637 
 638             out.println(prefix + "   <ServiceControl Id=\""+ WinAppBundler.getAppName(params) + "\""
 639                     + " Name=\"" + WinAppBundler.getAppName(params) + "\""
 640                     + (START_ON_INSTALL.fetchFrom(params) ? " Start=\"install\"" : "")
 641                     + (STOP_ON_UNINSTALL.fetchFrom(params) ? " Stop=\"uninstall\"" : "")
 642                     + " Remove=\"uninstall\""
 643                     + " Wait=\"yes\" />");
 644 
 645             out.println(prefix + " </Component>");
 646         }
 647 
 648         for (File d : dirs) {
 649             out.println(prefix + " <Directory Id=\"dirid" + (id++)
 650                     + "\" Name=\"" + d.getName() + "\">");
 651             walkFileTree(params, d, out, prefix + " ");
 652             out.println(prefix + " </Directory>");
 653         }
 654     }
 655 
 656     String getRegistryRoot(Map<String, ? super Object> params) {
 657         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 658             return "HKLM";
 659         } else {
 660             return "HKCU";
 661         }
 662     }
 663 
 664     boolean prepareContentList(Map<String, ? super Object> params) throws FileNotFoundException {
 665         File f = new File(CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
 666         PrintStream out = new PrintStream(f);
 667 


 670         out.println("<Include>");
 671 
 672         out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
 673         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 674             //install to programfiles
 675             if (BIT_ARCH_64.fetchFrom(params)) {
 676                 out.println("  <Directory Id=\"ProgramFiles64Folder\" Name=\"PFiles\">");
 677             } else {
 678                 out.println("  <Directory Id=\"ProgramFilesFolder\" Name=\"PFiles\">");
 679             }
 680         } else {
 681             //install to user folder
 682             out.println("  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
 683         }
 684         out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
 685                 + WinAppBundler.getAppName(params) + "\">");
 686 
 687         //dynamic part
 688         id = 0;
 689         compId = 0; //reset counters
 690         walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
 691 
 692         //closing
 693         out.println("   </Directory>");
 694         out.println("  </Directory>");
 695 
 696         //for shortcuts
 697         if (SHORTCUT_HINT.fetchFrom(params)) {
 698             out.println("  <Directory Id=\"DesktopFolder\" />");
 699         }
 700         if (MENU_HINT.fetchFrom(params)) {
 701             out.println("  <Directory Id=\"ProgramMenuFolder\">");
 702             out.println("    <Directory Id=\"ProgramMenuDir\" Name=\"" + MENU_GROUP.fetchFrom(params) + "\">");
 703             out.println("      <Component Id=\"comp" + (compId++) + "\""
 704                     + " Guid=\"" + UUID.randomUUID().toString() + "\""
 705                     + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
 706             out.println("        <RemoveFolder Id=\"ProgramMenuDir\" On=\"uninstall\" />");
 707             //This has to be under HKCU to make WiX happy.
 708             //There are numberous discussions on this amoung WiX users
 709             // (if user A installs and user B uninstalls then key is left behind)
 710             //and there are suggested workarounds but none of them are appealing.


 743     private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
 744     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
 745 
 746     private File buildMSI(Map<String, ? super Object> params, File outdir) throws IOException {
 747         File tmpDir = new File(BUILD_ROOT.fetchFrom(params), "tmp");
 748         File candleOut = new File(tmpDir, WinAppBundler.getAppName(params)+".wixobj");
 749         File msiOut = new File(outdir, WinAppBundler.getAppName(params)
 750                 + "-" + VERSION.fetchFrom(params) + ".msi");
 751 
 752         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-msi-config"), msiOut.getAbsolutePath()));
 753 
 754         msiOut.getParentFile().mkdirs();
 755 
 756         //run candle
 757         ProcessBuilder pb = new ProcessBuilder(
 758                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
 759                 "-nologo",
 760                 getConfig_ProjectFile(params).getAbsolutePath(),
 761                 "-ext", "WixUtilExtension",
 762                 "-out", candleOut.getAbsolutePath());
 763         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
 764         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 765 
 766         Log.verbose(MessageFormat.format(I18N.getString("message.generating-msi"), msiOut.getAbsolutePath()));
 767 
 768         //create .msi
 769         pb = new ProcessBuilder(
 770                 TOOL_LIGHT_EXECUTABLE.fetchFrom(params),
 771                 "-nologo",
 772                 "-spdb",
 773                 "-sice:60", //ignore warnings due to "missing launcguage info" (ICE60)
 774                 candleOut.getAbsolutePath(),
 775                 "-ext", "WixUtilExtension",
 776                 "-out", msiOut.getAbsolutePath());
 777         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
 778         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 779 
 780         candleOut.delete();
 781         IOUtils.deleteRecursive(tmpDir);
 782 
 783         return msiOut;
 784     }
 785 }