< prev index next >

src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java

Print this page




  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(


< prev index next >