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 jdk.incubator.jpackage.internal;
27
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.Writer;
31 import java.nio.charset.Charset;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.text.MessageFormat;
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.UUID;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44
45 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
46 import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
47 import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
48 import static jdk.incubator.jpackage.internal.StandardBundlerParam.DESCRIPTION;
49 import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
50 import static jdk.incubator.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
51 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR;
52 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
53
54 /**
55 * WinMsiBundler
56 *
57 * Produces .msi installer from application image. Uses WiX Toolkit to build
58 * .msi installer.
59 * <p>
60 * {@link #execute} method creates a number of source files with the description
61 * of installer to be processed by WiX tools. Generated source files are stored
62 * in "config" subdirectory next to "app" subdirectory in the root work
63 * directory. The following WiX source files are generated:
64 * <ul>
65 * <li>main.wxs. Main source file with the installer description
66 * <li>bundle.wxf. Source file with application and Java run-time directory tree
67 * description.
68 * </ul>
69 * <p>
70 * main.wxs file is a copy of main.wxs resource from
71 * jdk.incubator.jpackage.internal.resources package. It is parametrized with the
72 * following WiX variables:
399 if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
400 data.put("JpIsSystemWide", "yes");
401 }
402
403 String licenseFile = LICENSE_FILE.fetchFrom(params);
404 if (licenseFile != null) {
405 String lname = Path.of(licenseFile).getFileName().toString();
406 Path destFile = CONFIG_ROOT.fetchFrom(params).resolve(lname);
407 data.put("JpLicenseRtf", destFile.toAbsolutePath().toString());
408 }
409
410 // Copy CA dll to include with installer
411 if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
412 data.put("JpInstallDirChooser", "yes");
413 String fname = "wixhelper.dll";
414 try (InputStream is = OverridableResource.readDefault(fname)) {
415 Files.copy(is, CONFIG_ROOT.fetchFrom(params).resolve(fname));
416 }
417 }
418
419 // Copy l10n files.
420 for (String loc : Arrays.asList("en", "ja", "zh_CN")) {
421 String fname = "MsiInstallerStrings_" + loc + ".wxl";
422 try (InputStream is = OverridableResource.readDefault(fname)) {
423 Files.copy(is, CONFIG_ROOT.fetchFrom(params).resolve(fname));
424 }
425 }
426
427 createResource("main.wxs", params)
428 .setCategory(I18N.getString("resource.main-wix-file"))
429 .saveToFile(configDir.resolve("main.wxs"));
430
431 createResource("overrides.wxi", params)
432 .setCategory(I18N.getString("resource.overrides-wix-file"))
433 .saveToFile(configDir.resolve("overrides.wxi"));
434
435 return data;
436 }
437
438 private Path buildMSI(Map<String, ? super Object> params,
439 Map<String, String> wixVars, Path outdir)
453 .setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
454 .setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
455 .addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"), wixVars)
456 .addSource(CONFIG_ROOT.fetchFrom(params).resolve("bundle.wxf"), null);
457
458 Log.verbose(MessageFormat.format(I18N.getString(
459 "message.generating-msi"), msiOut.toAbsolutePath().toString()));
460
461 boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
462 boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
463
464 wixPipeline.addLightOptions("-sice:ICE27");
465
466 if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
467 wixPipeline.addLightOptions("-sice:ICE91");
468 }
469 if (enableLicenseUI || enableInstalldirUI) {
470 wixPipeline.addLightOptions("-ext", "WixUIExtension");
471 }
472
473 wixPipeline.addLightOptions("-loc",
474 CONFIG_ROOT.fetchFrom(params).resolve(I18N.getString(
475 "resource.wxl-file-name")).toAbsolutePath().toString());
476
477 // Only needed if we using CA dll, so Wix can find it
478 if (enableInstalldirUI) {
479 wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params)
480 .toAbsolutePath().toString());
481 }
482
483 wixPipeline.buildMsi(msiOut.toAbsolutePath());
484
485 return msiOut;
486 }
487
488 private static void ensureByMutationFileIsRTF(Path f) {
489 if (f == null || !Files.isRegularFile(f)) return;
490
491 try {
492 boolean existingLicenseIsRTF = false;
493
494 try (InputStream fin = Files.newInputStream(f)) {
495 byte[] firstBits = new byte[7];
496
497 if (fin.read(firstBits) == firstBits.length) {
498 String header = new String(firstBits);
499 existingLicenseIsRTF = "{\\rtf1\\".equals(header);
500 }
501 }
502
503 if (!existingLicenseIsRTF) {
504 List<String> oldLicense = Files.readAllLines(f);
505 try (Writer w = Files.newBufferedWriter(
|
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 jdk.incubator.jpackage.internal;
27
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.Writer;
31 import java.nio.charset.Charset;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.FileSystems;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.nio.file.PathMatcher;
37 import java.text.MessageFormat;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.LinkedHashSet;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.UUID;
47 import java.util.stream.Collectors;
48 import java.util.stream.Stream;
49 import javax.xml.parsers.DocumentBuilder;
50 import javax.xml.parsers.DocumentBuilderFactory;
51 import javax.xml.parsers.ParserConfigurationException;
52 import javax.xml.xpath.XPath;
53 import javax.xml.xpath.XPathConstants;
54 import javax.xml.xpath.XPathExpressionException;
55 import javax.xml.xpath.XPathFactory;
56
57 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
58 import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
59 import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
60 import static jdk.incubator.jpackage.internal.StandardBundlerParam.DESCRIPTION;
61 import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
62 import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
63 import static jdk.incubator.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
64 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR;
65 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
66 import org.w3c.dom.Document;
67 import org.w3c.dom.NodeList;
68 import org.xml.sax.SAXException;
69
70 /**
71 * WinMsiBundler
72 *
73 * Produces .msi installer from application image. Uses WiX Toolkit to build
74 * .msi installer.
75 * <p>
76 * {@link #execute} method creates a number of source files with the description
77 * of installer to be processed by WiX tools. Generated source files are stored
78 * in "config" subdirectory next to "app" subdirectory in the root work
79 * directory. The following WiX source files are generated:
80 * <ul>
81 * <li>main.wxs. Main source file with the installer description
82 * <li>bundle.wxf. Source file with application and Java run-time directory tree
83 * description.
84 * </ul>
85 * <p>
86 * main.wxs file is a copy of main.wxs resource from
87 * jdk.incubator.jpackage.internal.resources package. It is parametrized with the
88 * following WiX variables:
415 if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
416 data.put("JpIsSystemWide", "yes");
417 }
418
419 String licenseFile = LICENSE_FILE.fetchFrom(params);
420 if (licenseFile != null) {
421 String lname = Path.of(licenseFile).getFileName().toString();
422 Path destFile = CONFIG_ROOT.fetchFrom(params).resolve(lname);
423 data.put("JpLicenseRtf", destFile.toAbsolutePath().toString());
424 }
425
426 // Copy CA dll to include with installer
427 if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
428 data.put("JpInstallDirChooser", "yes");
429 String fname = "wixhelper.dll";
430 try (InputStream is = OverridableResource.readDefault(fname)) {
431 Files.copy(is, CONFIG_ROOT.fetchFrom(params).resolve(fname));
432 }
433 }
434
435 // Copy standard l10n files.
436 for (String loc : Arrays.asList("en", "ja", "zh_CN")) {
437 String fname = "MsiInstallerStrings_" + loc + ".wxl";
438 try (InputStream is = OverridableResource.readDefault(fname)) {
439 Files.copy(is, CONFIG_ROOT.fetchFrom(params).resolve(fname));
440 }
441 }
442
443 createResource("main.wxs", params)
444 .setCategory(I18N.getString("resource.main-wix-file"))
445 .saveToFile(configDir.resolve("main.wxs"));
446
447 createResource("overrides.wxi", params)
448 .setCategory(I18N.getString("resource.overrides-wix-file"))
449 .saveToFile(configDir.resolve("overrides.wxi"));
450
451 return data;
452 }
453
454 private Path buildMSI(Map<String, ? super Object> params,
455 Map<String, String> wixVars, Path outdir)
469 .setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
470 .setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
471 .addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"), wixVars)
472 .addSource(CONFIG_ROOT.fetchFrom(params).resolve("bundle.wxf"), null);
473
474 Log.verbose(MessageFormat.format(I18N.getString(
475 "message.generating-msi"), msiOut.toAbsolutePath().toString()));
476
477 boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
478 boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
479
480 wixPipeline.addLightOptions("-sice:ICE27");
481
482 if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
483 wixPipeline.addLightOptions("-sice:ICE91");
484 }
485 if (enableLicenseUI || enableInstalldirUI) {
486 wixPipeline.addLightOptions("-ext", "WixUIExtension");
487 }
488
489 final Path primaryWxlFile = CONFIG_ROOT.fetchFrom(params).resolve(
490 I18N.getString("resource.wxl-file-name")).toAbsolutePath();
491
492 wixPipeline.addLightOptions("-loc", primaryWxlFile.toString());
493
494 List<String> cultures = new ArrayList<>();
495 for (var wxl : getCustomWxlFiles(params)) {
496 wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().toString());
497 cultures.add(getCultureFromWxlFile(wxl));
498 }
499 cultures.add(getCultureFromWxlFile(primaryWxlFile));
500
501 // Build ordered list of unique cultures.
502 Set<String> uniqueCultures = new LinkedHashSet<>();
503 uniqueCultures.addAll(cultures);
504 wixPipeline.addLightOptions(uniqueCultures.stream().collect(
505 Collectors.joining(";", "-cultures:", "")));
506
507 // Only needed if we using CA dll, so Wix can find it
508 if (enableInstalldirUI) {
509 wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params)
510 .toAbsolutePath().toString());
511 }
512
513 wixPipeline.buildMsi(msiOut.toAbsolutePath());
514
515 return msiOut;
516 }
517
518 private static List<Path> getCustomWxlFiles(Map<String, ? super Object> params)
519 throws IOException {
520 Path resourceDir = RESOURCE_DIR.fetchFrom(params);
521 if (resourceDir == null) {
522 return Collections.emptyList();
523 }
524
525 final String glob = "glob:**/*.wxl";
526 final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(
527 glob);
528
529 try (var walk = Files.walk(resourceDir, 1)) {
530 return walk
531 .filter(Files::isReadable)
532 .filter(pathMatcher::matches)
533 .sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
534 .collect(Collectors.toList());
535 }
536 }
537
538 private static String getCultureFromWxlFile(Path wxlPath) throws IOException {
539 try {
540 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
541 factory.setNamespaceAware(false);
542 DocumentBuilder builder = factory.newDocumentBuilder();
543
544 Document doc = builder.parse(wxlPath.toFile());
545
546 XPath xPath = XPathFactory.newInstance().newXPath();
547 NodeList nodes = (NodeList) xPath.evaluate(
548 "//WixLocalization/@Culture", doc,
549 XPathConstants.NODESET);
550 if (nodes.getLength() != 1) {
551 throw new IOException(MessageFormat.format(I18N.getString(
552 "error.extract-culture-from-wix-l10n-file"),
553 wxlPath.toAbsolutePath()));
554 }
555
556 return nodes.item(0).getNodeValue();
557 } catch (XPathExpressionException | ParserConfigurationException
558 | SAXException ex) {
559 throw new IOException(MessageFormat.format(I18N.getString(
560 "error.read-wix-l10n-file"), wxlPath.toAbsolutePath()), ex);
561 }
562 }
563
564 private static void ensureByMutationFileIsRTF(Path f) {
565 if (f == null || !Files.isRegularFile(f)) return;
566
567 try {
568 boolean existingLicenseIsRTF = false;
569
570 try (InputStream fin = Files.newInputStream(f)) {
571 byte[] firstBits = new byte[7];
572
573 if (fin.read(firstBits) == firstBits.length) {
574 String header = new String(firstBits);
575 existingLicenseIsRTF = "{\\rtf1\\".equals(header);
576 }
577 }
578
579 if (!existingLicenseIsRTF) {
580 List<String> oldLicense = Files.readAllLines(f);
581 try (Writer w = Files.newBufferedWriter(
|