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 }