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 }
|