--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Thu Jun 13 19:04:29 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Thu Jun 13 19:32:24 2019 -0400
@@ -28,12 +28,66 @@
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
+import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.*;
import java.util.regex.Pattern;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
import static jdk.jpackage.internal.WindowsBundlerParam.*;
+/**
+ * WinMsiBundler
+ *
+ * Produces .msi installer from application image. Uses WiX Toolkit to build
+ * .msi installer.
+ * <p>
+ * {@link #execute} method creates a number of source files with the description
+ * of installer to be processed by WiX tools. Generated source files are stored
+ * in "config" subdirectory next to "app" subdirectory in the root work
+ * directory. The following WiX source files are generated:
+ * <ul>
+ * <li>main.wxs. Main source file with the installer description
+ * <li>bundle.wxi. Source file with application and Java run-time directory tree
+ * description. This source file is included from main.wxs
+ * <li>icons.wxi. Source file with the list of icons used by the application.
+ * This source file is included from main.wxs
+ * </ul>
+ * <p>
+ * main.wxs file is a copy of main.wxs resource from
+ * jdk.jpackage.internal.resources package. It is parametrized with the
+ * following WiX variables:
+ * <ul>
+ * <li>JpAppName. Name of the application. Set to the value of --name command
+ * line option
+ * <li>JpAppVersion. Version of the application. Set to the value of
+ * --app-version command line option
+ * <li>JpAppVendor. Vendor of the application. Set to the value of --vendor
+ * command line option
+ * <li>JpAppDescription. Description of the application. Set to the value of
+ * --description command line option
+ * <li>JpProductCode. Set to product code UUID of the application. Random value
+ * generated by jpackage every time {@link #execute} method is called
+ * <li>JpProductUpgradeCode. Set to upgrade code UUID of the application. Random
+ * value generated by jpackage every time {@link #execute} method is called if
+ * --win-upgrade-uuid command line option is not specified. Otherwise this
+ * variable is set to the value of --win-upgrade-uuid command line option
+ * <li>JpAllowDowngrades. Set to "yes" if --win-upgrade-uuid command line option
+ * was specified. Undefined otherwise
+ * <li>JpLicenseRtf. Set to the value of --license-file command line option.
+ * Undefined is --license-file command line option was not specified
+ * <li>JpInstallDirChooser. Set to "yes" if --win-dir-chooser command line
+ * option was specified. Undefined otherwise
+ * <li>JpConfigDir. Absolute path to the directory with generated WiX source
+ * files.
+ * <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
+ * option was not specified. Undefined otherwise
+ * <li>JpWixVersion36OrNewer. Set to "yes" if WiX Toolkit v3.6 or newer is used.
+ * Undefined otherwise
+ * </ul>
+ */
public class WinMsiBundler extends AbstractBundler {
private static final ResourceBundle I18N = ResourceBundle.getBundle(
@@ -403,6 +457,7 @@
File destFile = new File(CONFIG_ROOT.fetchFrom(params),
lfile.getName());
IOUtils.copyFile(lfile, destFile);
+ destFile.setWritable(true);
ensureByMutationFileIsRTF(destFile);
}
@@ -452,6 +507,8 @@
throw new PackagerException("error.no-wix-tools");
}
+ Map<String, String> wixVars = null;
+
File imageDir = MSI_IMAGE_DIR.fetchFrom(params);
try {
imageDir.mkdirs();
@@ -464,8 +521,10 @@
params.put(MENU_HINT.getID(), true);
}
- if (prepareProto(params) && prepareWiXConfig(params)
- && prepareBasicProjectConfig(params)) {
+ prepareBasicProjectConfig(params);
+ if (prepareProto(params)) {
+ wixVars = prepareWiXConfig(params);
+
File configScriptSrc = getConfig_Script(params);
if (configScriptSrc.exists()) {
// we need to be running post script in the image folder
@@ -482,7 +541,7 @@
configScript.getAbsolutePath()));
IOUtils.run("wscript", configScript);
}
- return buildMSI(params, outdir);
+ return buildMSI(params, wixVars, outdir);
}
return null;
} catch (IOException ex) {
@@ -497,7 +556,7 @@
APP_NAME.fetchFrom(params) + "-post-image.wsf");
}
- private boolean prepareBasicProjectConfig(
+ private void prepareBasicProjectConfig(
Map<String, ? super Object> params) throws IOException {
fetchResource(getConfig_Script(params).getName(),
I18N.getString("resource.post-install-script"),
@@ -505,15 +564,74 @@
getConfig_Script(params),
VERBOSE.fetchFrom(params),
RESOURCE_DIR.fetchFrom(params));
- return true;
}
- private String relativePath(File basedir, File file) {
+ private static String relativePath(File basedir, File file) {
return file.getAbsolutePath().substring(
basedir.getAbsolutePath().length() + 1);
}
- boolean prepareMainProjectFile(
+ private void prepareIconsFile(
+ Map<String, ? super Object> params) throws IOException {
+
+ File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
+
+ List<Map<String, ? super Object>> addLaunchers =
+ ADD_LAUNCHERS.fetchFrom(params);
+
+ XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
+ try (Writer w = new BufferedWriter(new FileWriter(new File(
+ CONFIG_ROOT.fetchFrom(params), "icons.wxi")))) {
+ XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
+
+ xml.writeStartDocument();
+ xml.writeStartElement("Include");
+
+ File launcher = new File(imageRootDir, WinAppBundler.getLauncherName(params));
+ if (launcher.exists()) {
+ String iconPath = launcher.getAbsolutePath().replace(".exe", ".ico");
+ if (MENU_HINT.fetchFrom(params)) {
+ xml.writeStartElement("Icon");
+ xml.writeAttribute("Id", "StartMenuIcon.exe");
+ xml.writeAttribute("SourceFile", iconPath);
+ xml.writeEndElement();
+ }
+ if (SHORTCUT_HINT.fetchFrom(params)) {
+ xml.writeStartElement("Icon");
+ xml.writeAttribute("Id", "DesktopIcon.exe");
+ xml.writeAttribute("SourceFile", iconPath);
+ xml.writeEndElement();
+ }
+ }
+
+ for (int i = 0; i < addLaunchers.size(); i++) {
+ Map<String, ? super Object> sl = addLaunchers.get(i);
+ if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
+ File addLauncher = new File(imageRootDir,
+ WinAppBundler.getLauncherName(sl));
+ String addLauncherPath
+ = relativePath(imageRootDir, addLauncher);
+ String addLauncherIconPath
+ = addLauncherPath.replace(".exe", ".ico");
+
+ xml.writeStartElement("Icon");
+ xml.writeAttribute("Id", "Launcher" + i + ".exe");
+ xml.writeAttribute("SourceFile", addLauncherIconPath);
+ xml.writeEndElement();
+ }
+ }
+
+ xml.writeEndElement();
+ xml.writeEndDocument();
+ xml.flush();
+ xml.close();
+ } catch (XMLStreamException ex) {
+ Log.verbose(ex);
+ throw new IOException(ex);
+ }
+ }
+
+ Map<String, String> prepareMainProjectFile(
Map<String, ? super Object> params) throws IOException {
Map<String, String> data = new HashMap<>();
@@ -528,193 +646,65 @@
// Upgrade guid is important to decide whether it is an upgrade of
// installed app. I.e. we need it to be the same for
// 2 different versions of app if possible
- data.put("PRODUCT_GUID", productGUID.toString());
- data.put("PRODUCT_UPGRADE_GUID",
+ data.put("JpProductCode", productGUID.toString());
+ data.put("JpProductUpgradeCode",
UPGRADE_UUID.fetchFrom(params).toString());
- data.put("UPGRADE_BLOCK", getUpgradeBlock(params));
- data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
- data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
- data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
- data.put("APPLICATION_VERSION", PRODUCT_VERSION.fetchFrom(params));
-
- // WinAppBundler will add application folder again => step out
- File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
- File launcher = new File(imageRootDir,
- WinAppBundler.getLauncherName(params));
+ if (!UPGRADE_UUID.getIsDefaultValue()) {
+ data.put("JpAllowDowngrades", "yes");
+ }
- String launcherPath = relativePath(imageRootDir, launcher);
- data.put("APPLICATION_LAUNCHER", launcherPath);
-
- String iconPath = launcherPath.replace(".exe", ".ico");
-
- data.put("APPLICATION_ICON", iconPath);
+ if (CAN_USE_WIX36.fetchFrom(params)) {
+ data.put("JpWixVersion36OrNewer", "yes");
+ }
- data.put("REGISTRY_ROOT", getRegistryRoot(params));
+ data.put("JpAppName", APP_NAME.fetchFrom(params));
+ data.put("JpAppDescription", DESCRIPTION.fetchFrom(params));
+ data.put("JpAppVendor", VENDOR.fetchFrom(params));
+ data.put("JpAppVersion", PRODUCT_VERSION.fetchFrom(params));
- boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
- data.put("WIX36_ONLY_START",
- canUseWix36Features ? "" : "<!--");
- data.put("WIX36_ONLY_END",
- canUseWix36Features ? "" : "-->");
+ data.put("JpConfigDir", CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
+
+ File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
- data.put("INSTALL_SCOPE", "perMachine");
- } else {
- data.put("INSTALL_SCOPE", "perUser");
+ data.put("JpIsSystemWide", "yes");
}
- data.put("PLATFORM", "x64");
- data.put("WIN64", "yes");
-
- data.put("UI_BLOCK", getUIBlock(params));
+ if (LICENSE_FILE.fetchFrom(params) != null) {
+ data.put("JpLicenseRtf", LICENSE_FILE.fetchFrom(params));
+ }
- // Add CA to check install dir
+ // Copy CA dll to include with installer
if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
- data.put("CA_BLOCK", CA_BLOCK);
- data.put("INVALID_INSTALL_DIR_DLG_BLOCK", INVALID_INSTALL_DIR_DLG_BLOCK);
- } else {
- data.put("CA_BLOCK", "");
- data.put("INVALID_INSTALL_DIR_DLG_BLOCK", "");
+ data.put("JpInstallDirChooser", "yes");
+ String fname = "wixhelper.dll";
+ try (InputStream is = getResourceAsStream(fname)) {
+ Files.copy(is, Paths.get(
+ CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), fname));
+ }
}
- List<Map<String, ? super Object>> addLaunchers =
- ADD_LAUNCHERS.fetchFrom(params);
-
- StringBuilder addLauncherIcons = new StringBuilder();
- for (int i = 0; i < addLaunchers.size(); i++) {
- Map<String, ? super Object> sl = addLaunchers.get(i);
- // <Icon Id="DesktopIcon.exe" SourceFile="APPLICATION_ICON" />
- if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
- File addLauncher = new File(imageRootDir,
- WinAppBundler.getLauncherName(sl));
- String addLauncherPath =
- relativePath(imageRootDir, addLauncher);
- String addLauncherIconPath =
- addLauncherPath.replace(".exe", ".ico");
-
- addLauncherIcons.append(" <Icon Id=\"Launcher");
- addLauncherIcons.append(i);
- addLauncherIcons.append(".exe\" SourceFile=\"");
- addLauncherIcons.append(addLauncherIconPath);
- addLauncherIcons.append("\" />\r\n");
+ // Copy l10n files.
+ for (String loc : Arrays.asList("en", "ja", "zh_CN")) {
+ String fname = "MsiInstallerStrings_" + loc + ".wxl";
+ try (InputStream is = getResourceAsStream(fname)) {
+ Files.copy(is, Paths.get(
+ CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), fname));
}
}
- data.put("ADD_LAUNCHER_ICONS", addLauncherIcons.toString());
-
- String wxs = StandardBundlerParam.isRuntimeInstaller(params) ?
- MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE;
-
- try (Writer w = Files.newBufferedWriter(
- getConfig_ProjectFile(params).toPath())) {
- String content = preprocessTextResource(
- getConfig_ProjectFile(params).getName(),
- I18N.getString("resource.wix-config-file"),
- wxs, data, VERBOSE.fetchFrom(params),
- RESOURCE_DIR.fetchFrom(params));
- w.write(content);
+ try (InputStream is = getResourceAsStream("main.wxs")) {
+ Files.copy(is, Paths.get(
+ getConfig_ProjectFile(params).getAbsolutePath()));
}
- return true;
+
+ return data;
}
private int id;
private int compId;
private final static String LAUNCHER_ID = "LauncherId";
- private static final String CA_BLOCK =
- "<Binary Id=\"CustomActionDLL\" SourceFile=\"wixhelper.dll\" />\n" +
- "<CustomAction Id=\"CHECK_INSTALLDIR\" BinaryKey=\"CustomActionDLL\" " +
- "DllEntry=\"CheckInstallDir\" />";
-
- private static final String INVALID_INSTALL_DIR_DLG_BLOCK =
- "<Dialog Id=\"InvalidInstallDir\" Width=\"300\" Height=\"85\" " +
- "Title=\"[ProductName] Setup\" NoMinimize=\"yes\">\n" +
- "<Control Id=\"InvalidInstallDirYes\" Type=\"PushButton\" X=\"100\" Y=\"55\" " +
- "Width=\"50\" Height=\"15\" Default=\"no\" Cancel=\"no\" Text=\"Yes\">\n" +
- "<Publish Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n" +
- "</Control>\n" +
- "<Control Id=\"InvalidInstallDirNo\" Type=\"PushButton\" X=\"150\" Y=\"55\" " +
- "Width=\"50\" Height=\"15\" Default=\"yes\" Cancel=\"yes\" Text=\"No\">\n" +
- "<Publish Event=\"NewDialog\" Value=\"InstallDirDlg\">1</Publish>\n" +
- "</Control>\n" +
- "<Control Id=\"Text\" Type=\"Text\" X=\"25\" Y=\"15\" Width=\"250\" Height=\"30\" " +
- "TabSkip=\"no\">\n" +
- "<Text>" + I18N.getString("message.install.dir.exist") + "</Text>\n" +
- "</Control>\n" +
- "</Dialog>";
-
- /**
- * Overrides the dialog sequence in built-in dialog set "WixUI_InstallDir"
- * to exclude license dialog
- */
- private static final String TWEAK_FOR_EXCLUDING_LICENSE =
- " <Publish Dialog=\"WelcomeDlg\" Control=\"Next\""
- + " Event=\"NewDialog\" Value=\"InstallDirDlg\""
- + " Order=\"2\">1</Publish>\n"
- + " <Publish Dialog=\"InstallDirDlg\" Control=\"Back\""
- + " Event=\"NewDialog\" Value=\"WelcomeDlg\""
- + " Order=\"2\">1</Publish>\n";
-
- private static final String CHECK_INSTALL_DLG_CTRL =
- " <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
- + " Event=\"DoAction\" Value=\"CHECK_INSTALLDIR\""
- + " Order=\"3\">1</Publish>\n"
- + " <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
- + " Event=\"NewDialog\" Value=\"InvalidInstallDir\""
- + " Order=\"5\">INSTALLDIR_VALID=\"0\"</Publish>\n"
- + " <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
- + " Event=\"NewDialog\" Value=\"VerifyReadyDlg\""
- + " Order=\"5\">INSTALLDIR_VALID=\"1\"</Publish>\n";
-
- // Required upgrade element for installers which support major upgrade (when user
- // specifies --win-upgrade-uuid). We will allow downgrades.
- private static final String UPGRADE_BLOCK =
- "<MajorUpgrade AllowDowngrades=\"yes\"/>";
-
- private String getUpgradeBlock(Map<String, ? super Object> params) {
- if (UPGRADE_UUID.getIsDefaultValue()) {
- return "";
- } else {
- return UPGRADE_BLOCK;
- }
- }
-
- /**
- * Creates UI element using WiX built-in dialog sets
- * - WixUI_InstallDir/WixUI_Minimal.
- * The dialog sets are the closest to what we want to implement.
- *
- * WixUI_Minimal for license dialog only
- * WixUI_InstallDir for installdir dialog only or for both
- * installdir/license dialogs
- */
- private String getUIBlock(Map<String, ? super Object> params) throws IOException {
- String uiBlock = ""; // UI-less element
-
- // Copy CA dll to include with installer
- if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
- File helper = new File(CONFIG_ROOT.fetchFrom(params), "wixhelper.dll");
- try (InputStream is_lib = getResourceAsStream("wixhelper.dll")) {
- Files.copy(is_lib, helper.toPath());
- }
- }
-
- if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
- boolean enableTweakForExcludingLicense =
- (getLicenseFile(params) == null);
- uiBlock = " <Property Id=\"WIXUI_INSTALLDIR\""
- + " Value=\"APPLICATIONFOLDER\" />\n"
- + " <UIRef Id=\"WixUI_InstallDir\" />\n"
- + (enableTweakForExcludingLicense ?
- TWEAK_FOR_EXCLUDING_LICENSE : "")
- + CHECK_INSTALL_DLG_CTRL;
- } else if (getLicenseFile(params) != null) {
- uiBlock = " <UIRef Id=\"WixUI_Minimal\" />\n";
- }
-
- return uiBlock;
- }
-
private void walkFileTree(Map<String, ? super Object> params,
File root, PrintStream out, String prefix) {
List<File> dirs = new ArrayList<>();
@@ -936,15 +926,7 @@
}
}
- String getRegistryRoot(Map<String, ? super Object> params) {
- if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
- return "HKLM";
- } else {
- return "HKCU";
- }
- }
-
- boolean prepareContentList(Map<String, ? super Object> params)
+ void prepareContentList(Map<String, ? super Object> params)
throws FileNotFoundException {
File f = new File(
CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
@@ -1024,14 +1006,13 @@
for (int j = 0; j < compId; j++) {
out.println(" <ComponentRef Id=\"comp" + j + "\" />");
}
- // component is defined in the template.wsx
+ // component is defined in the main.wsx
out.println(
" <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
out.println(" </Feature>");
out.println("</Include>");
}
- return true;
}
private File getConfig_ProjectFile(Map<String, ? super Object> params) {
@@ -1039,38 +1020,21 @@
APP_NAME.fetchFrom(params) + ".wxs");
}
- private String getLicenseFile(Map<String, ? super Object> params) {
- String licenseFile = LICENSE_FILE.fetchFrom(params);
- if (licenseFile != null) {
- File lfile = new File(licenseFile);
- File destFile = new File(CONFIG_ROOT.fetchFrom(params),
- lfile.getName());
- String filePath = destFile.getAbsolutePath();
- if (filePath.contains(" ")) {
- return "\"" + filePath + "\"";
- } else {
- return filePath;
- }
- }
-
- return null;
+ private Map<String, String> prepareWiXConfig(
+ Map<String, ? super Object> params) throws IOException {
+ prepareContentList(params);
+ prepareIconsFile(params);
+ return prepareMainProjectFile(params);
}
- private boolean prepareWiXConfig(
- Map<String, ? super Object> params) throws IOException {
- return prepareMainProjectFile(params) && prepareContentList(params);
-
- }
- private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
- private final static String MSI_PROJECT_TEMPLATE_SERVER_JRE =
- "template.jre.wxs";
private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
- private File buildMSI(Map<String, ? super Object> params, File outdir)
+ private File buildMSI(Map<String, ? super Object> params,
+ Map<String, String> wixVars, File outdir)
throws IOException {
File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp");
File candleOut = new File(
- tmpDir, APP_NAME.fetchFrom(params) +".wixobj");
+ tmpDir, APP_NAME.fetchFrom(params) + ".wixobj");
File msiOut = new File(
outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
@@ -1079,28 +1043,30 @@
msiOut.getParentFile().mkdirs();
- // run candle
- ProcessBuilder pb = new ProcessBuilder(
+ List<String> commandLine = new ArrayList<>(Arrays.asList(
TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
"-nologo",
getConfig_ProjectFile(params).getAbsolutePath(),
"-ext", "WixUtilExtension",
- "-out", candleOut.getAbsolutePath());
+ "-out", candleOut.getAbsolutePath()));
+ for(Map.Entry<String, String> wixVar: wixVars.entrySet()) {
+ String v = "-d" + wixVar.getKey() + "=" + wixVar.getValue();
+ commandLine.add(v);
+ }
+ ProcessBuilder pb = new ProcessBuilder(commandLine);
pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
IOUtils.exec(pb);
Log.verbose(MessageFormat.format(I18N.getString(
"message.generating-msi"), msiOut.getAbsolutePath()));
- boolean enableLicenseUI = (getLicenseFile(params) != null);
+ boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
- List<String> commandLine = new ArrayList<>();
+ commandLine = new ArrayList<>();
commandLine.add(TOOL_LIGHT_EXECUTABLE.fetchFrom(params));
- if (enableLicenseUI) {
- commandLine.add("-dWixUILicenseRtf="+getLicenseFile(params));
- }
+
commandLine.add("-nologo");
commandLine.add("-spdb");
commandLine.add("-sice:60");
@@ -1110,9 +1076,13 @@
commandLine.add("WixUtilExtension");
if (enableLicenseUI || enableInstalldirUI) {
commandLine.add("-ext");
- commandLine.add("WixUIExtension.dll");
+ commandLine.add("WixUIExtension");
}
+ commandLine.add("-loc");
+ commandLine.add(new File(CONFIG_ROOT.fetchFrom(params), I18N.getString(
+ "resource.wxl-file-name")).getAbsolutePath());
+
// Only needed if we using CA dll, so Wix can find it
if (enableInstalldirUI) {
commandLine.add("-b");