8229979: jpackage cleanup src files, help text, and javadoc
Reviewed-by: asemenyuk, kcr
/*
* Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
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(
"jdk.jpackage.internal.resources.WinResources");
public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
new WindowsBundlerParam<>(
"win.app.bundler",
WinAppBundler.class,
params -> new WinAppBundler(),
null);
public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 =
new WindowsBundlerParam<>(
"win.msi.canUseWix36",
Boolean.class,
params -> false,
(s, p) -> Boolean.valueOf(s));
public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
new WindowsBundlerParam<>(
"win.msi.imageDir",
File.class,
params -> {
File imagesRoot = IMAGES_ROOT.fetchFrom(params);
if (!imagesRoot.exists()) imagesRoot.mkdirs();
return new File(imagesRoot, "win-msi.image");
},
(s, p) -> null);
public static final BundlerParamInfo<File> WIN_APP_IMAGE =
new WindowsBundlerParam<>(
"win.app.image",
File.class,
null,
(s, p) -> null);
public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE =
new StandardBundlerParam<>(
Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
Boolean.class,
params -> true, // MSIs default to system wide
// valueOf(null) is false,
// and we actually do want null
(s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
: Boolean.valueOf(s)
);
public static final StandardBundlerParam<String> PRODUCT_VERSION =
new StandardBundlerParam<>(
"win.msi.productVersion",
String.class,
VERSION::fetchFrom,
(s, p) -> s
);
public static final BundlerParamInfo<UUID> UPGRADE_UUID =
new WindowsBundlerParam<>(
Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
UUID.class,
params -> UUID.randomUUID(),
(s, p) -> UUID.fromString(s));
private static final String TOOL_CANDLE = "candle.exe";
private static final String TOOL_LIGHT = "light.exe";
// autodetect just v3.7, v3.8, 3.9, 3.10 and 3.11
private static final String AUTODETECT_DIRS =
";C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;"
+ "C:\\Program Files\\WiX Toolset v3.11\\bin;"
+ "C:\\Program Files (x86)\\WiX Toolset v3.10\\bin;"
+ "C:\\Program Files\\WiX Toolset v3.10\\bin;"
+ "C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;"
+ "C:\\Program Files\\WiX Toolset v3.9\\bin;"
+ "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";
private static String getCandlePath() {
for (String dirString : (System.getenv("PATH")
+ AUTODETECT_DIRS).split(";")) {
File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
if (f.isFile()) {
return f.toString();
}
}
return null;
}
private static String getLightPath() {
for (String dirString : (System.getenv("PATH")
+ AUTODETECT_DIRS).split(";")) {
File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
if (f.isFile()) {
return f.toString();
}
}
return null;
}
public static final StandardBundlerParam<Boolean> MENU_HINT =
new WindowsBundlerParam<>(
Arguments.CLIOptions.WIN_MENU_HINT.getId(),
Boolean.class,
params -> false,
// valueOf(null) is false,
// and we actually do want null in some cases
(s, p) -> (s == null ||
"null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
);
public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
new WindowsBundlerParam<>(
Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
Boolean.class,
params -> false,
// valueOf(null) is false,
// and we actually do want null in some cases
(s, p) -> (s == null ||
"null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
);
@Override
public String getName() {
return I18N.getString("msi.bundler.name");
}
@Override
public String getID() {
return "msi";
}
@Override
public String getBundleType() {
return "INSTALLER";
}
@Override
public File execute(Map<String, ? super Object> params,
File outputParentDir) throws PackagerException {
return bundle(params, outputParentDir);
}
@Override
public boolean supported(boolean platformInstaller) {
return isSupported();
}
public static boolean isSupported() {
try {
return validateWixTools();
} catch (Exception e) {
return false;
}
}
private static String findToolVersion(String toolName) {
try {
if (toolName == null || "".equals(toolName)) return null;
ProcessBuilder pb = new ProcessBuilder(
toolName,
"/?");
VersionExtractor ve = new VersionExtractor("version (\\d+.\\d+)");
// not interested in the output
IOUtils.exec(pb, true, ve);
String version = ve.getVersion();
Log.verbose(MessageFormat.format(
I18N.getString("message.tool-version"),
toolName, version));
return version;
} catch (Exception e) {
Log.verbose(e);
return null;
}
}
public static boolean validateWixTools() {
String candleVersion = findToolVersion(getCandlePath());
String lightVersion = findToolVersion(getLightPath());
// WiX 3.0+ is required
String minVersion = "3.0";
if (VersionExtractor.isLessThan(candleVersion, minVersion)) {
Log.verbose(MessageFormat.format(
I18N.getString("message.wrong-tool-version"),
TOOL_CANDLE, candleVersion, minVersion));
return false;
}
if (VersionExtractor.isLessThan(lightVersion, minVersion)) {
Log.verbose(MessageFormat.format(
I18N.getString("message.wrong-tool-version"),
TOOL_LIGHT, lightVersion, minVersion));
return false;
}
return true;
}
@Override
public boolean validate(Map<String, ? super Object> params)
throws ConfigException {
try {
if (params == null) throw new ConfigException(
I18N.getString("error.parameters-null"),
I18N.getString("error.parameters-null.advice"));
// run basic validation to ensure requirements are met
// we are not interested in return code, only possible exception
if (!validateWixTools()){
throw new ConfigException(
I18N.getString("error.no-wix-tools"),
I18N.getString("error.no-wix-tools.advice"));
}
String lightVersion = findToolVersion(getLightPath());
if (!VersionExtractor.isLessThan(lightVersion, "3.6")) {
Log.verbose(I18N.getString("message.use-wix36-features"));
params.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
}
/********* validate bundle parameters *************/
String version = PRODUCT_VERSION.fetchFrom(params);
if (!isVersionStringValid(version)) {
throw new ConfigException(
MessageFormat.format(I18N.getString(
"error.version-string-wrong-format"), version),
MessageFormat.format(I18N.getString(
"error.version-string-wrong-format.advice"),
PRODUCT_VERSION.getID()));
}
// only one mime type per association, at least one file extension
List<Map<String, ? super Object>> associations =
FILE_ASSOCIATIONS.fetchFrom(params);
if (associations != null) {
for (int i = 0; i < associations.size(); i++) {
Map<String, ? super Object> assoc = associations.get(i);
List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
if (mimes.size() > 1) {
throw new ConfigException(MessageFormat.format(
I18N.getString("error.too-many-content-"
+ "types-for-file-association"), i),
I18N.getString("error.too-many-content-"
+ "types-for-file-association.advice"));
}
}
}
return true;
} catch (RuntimeException re) {
if (re.getCause() instanceof ConfigException) {
throw (ConfigException) re.getCause();
} else {
throw new ConfigException(re);
}
}
}
// http://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
// The format of the string is as follows:
// major.minor.build
// The first field is the major version and has a maximum value of 255.
// The second field is the minor version and has a maximum value of 255.
// The third field is called the build version or the update version and
// has a maximum value of 65,535.
static boolean isVersionStringValid(String v) {
if (v == null) {
return true;
}
String p[] = v.split("\\.");
if (p.length > 3) {
Log.verbose(I18N.getString(
"message.version-string-too-many-components"));
return false;
}
try {
int val = Integer.parseInt(p[0]);
if (val < 0 || val > 255) {
Log.verbose(I18N.getString(
"error.version-string-major-out-of-range"));
return false;
}
if (p.length > 1) {
val = Integer.parseInt(p[1]);
if (val < 0 || val > 255) {
Log.verbose(I18N.getString(
"error.version-string-minor-out-of-range"));
return false;
}
}
if (p.length > 2) {
val = Integer.parseInt(p[2]);
if (val < 0 || val > 65535) {
Log.verbose(I18N.getString(
"error.version-string-build-out-of-range"));
return false;
}
}
} catch (NumberFormatException ne) {
Log.verbose(I18N.getString("error.version-string-part-not-number"));
Log.verbose(ne);
return false;
}
return true;
}
private boolean prepareProto(Map<String, ? super Object> params)
throws PackagerException, IOException {
File appImage = StandardBundlerParam.getPredefinedAppImage(params);
File appDir = null;
// we either have an application image or need to build one
if (appImage != null) {
appDir = new File(MSI_IMAGE_DIR.fetchFrom(params),
APP_NAME.fetchFrom(params));
// copy everything from appImage dir into appDir/name
IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
} else {
appDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
MSI_IMAGE_DIR.fetchFrom(params), true);
}
params.put(WIN_APP_IMAGE.getID(), appDir);
String licenseFile = LICENSE_FILE.fetchFrom(params);
if (licenseFile != null) {
// need to copy license file to the working directory
// and convert to rtf if needed
File lfile = new File(licenseFile);
File destFile = new File(CONFIG_ROOT.fetchFrom(params),
lfile.getName());
IOUtils.copyFile(lfile, destFile);
destFile.setWritable(true);
ensureByMutationFileIsRTF(destFile);
}
// copy file association icons
List<Map<String, ? super Object>> fileAssociations =
FILE_ASSOCIATIONS.fetchFrom(params);
for (Map<String, ? super Object> fa : fileAssociations) {
File icon = FA_ICON.fetchFrom(fa);
if (icon == null) {
continue;
}
File faIconFile = new File(appDir, icon.getName());
if (icon.exists()) {
try {
IOUtils.copyFile(icon, faIconFile);
} catch (IOException e) {
Log.verbose(e);
}
}
}
return appDir != null;
}
public File bundle(Map<String, ? super Object> params, File outdir)
throws PackagerException {
IOUtils.writableOutputDir(outdir.toPath());
// validate we have valid tools before continuing
String light = getLightPath();
String candle = getCandlePath();
if (light == null || !new File(light).isFile() ||
candle == null || !new File(candle).isFile()) {
Log.verbose(MessageFormat.format(
I18N.getString("message.light-file-string"), light));
Log.verbose(MessageFormat.format(
I18N.getString("message.candle-file-string"), candle));
throw new PackagerException("error.no-wix-tools");
}
Map<String, String> wixVars = null;
File imageDir = MSI_IMAGE_DIR.fetchFrom(params);
try {
imageDir.mkdirs();
boolean menuShortcut = MENU_HINT.fetchFrom(params);
boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
if (!menuShortcut && !desktopShortcut) {
// both can not be false - user will not find the app
Log.verbose(I18N.getString("message.one-shortcut-required"));
params.put(MENU_HINT.getID(), true);
}
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
// NOTE: Would it be better to generate it to the image
// folder and save only if "verbose" is requested?
// for now we replicate it
File configScript =
new File(imageDir, configScriptSrc.getName());
IOUtils.copyFile(configScriptSrc, configScript);
Log.verbose(MessageFormat.format(
I18N.getString("message.running-wsh-script"),
configScript.getAbsolutePath()));
IOUtils.run("wscript", configScript);
}
return buildMSI(params, wixVars, outdir);
}
return null;
} catch (IOException ex) {
Log.verbose(ex);
throw new PackagerException(ex);
}
}
// name of post-image script
private File getConfig_Script(Map<String, ? super Object> params) {
return new File(CONFIG_ROOT.fetchFrom(params),
APP_NAME.fetchFrom(params) + "-post-image.wsf");
}
private void prepareBasicProjectConfig(
Map<String, ? super Object> params) throws IOException {
fetchResource(getConfig_Script(params).getName(),
I18N.getString("resource.post-install-script"),
(String) null,
getConfig_Script(params),
VERBOSE.fetchFrom(params),
RESOURCE_DIR.fetchFrom(params));
}
private static String relativePath(File basedir, File file) {
return file.getAbsolutePath().substring(
basedir.getAbsolutePath().length() + 1);
}
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.getLauncherRelativePath(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.getLauncherRelativePath(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<>();
UUID productGUID = UUID.randomUUID();
Log.verbose(MessageFormat.format(
I18N.getString("message.generated-product-guid"),
productGUID.toString()));
// we use random GUID for product itself but
// user provided for upgrade guid
// 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("JpProductCode", productGUID.toString());
data.put("JpProductUpgradeCode",
UPGRADE_UUID.fetchFrom(params).toString());
if (!UPGRADE_UUID.getIsDefaultValue()) {
data.put("JpAllowDowngrades", "yes");
}
if (CAN_USE_WIX36.fetchFrom(params)) {
data.put("JpWixVersion36OrNewer", "yes");
}
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));
data.put("JpConfigDir",
CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
data.put("JpIsSystemWide", "yes");
}
String licenseFile = LICENSE_FILE.fetchFrom(params);
if (licenseFile != null) {
String lname = new File(licenseFile).getName();
File destFile = new File(CONFIG_ROOT.fetchFrom(params), lname);
data.put("JpLicenseRtf", destFile.getAbsolutePath());
}
// Copy CA dll to include with installer
if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
data.put("JpInstallDirChooser", "yes");
String fname = "wixhelper.dll";
try (InputStream is = getResourceAsStream(fname)) {
Files.copy(is, Paths.get(
CONFIG_ROOT.fetchFrom(params).getAbsolutePath(),
fname));
}
}
// 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));
}
}
try (InputStream is = getResourceAsStream("main.wxs")) {
Files.copy(is, Paths.get(
getConfig_ProjectFile(params).getAbsolutePath()));
}
return data;
}
private int id;
private int compId;
private final static String LAUNCHER_ID = "LauncherId";
private void walkFileTree(Map<String, ? super Object> params,
File root, PrintStream out, String prefix) {
List<File> dirs = new ArrayList<>();
List<File> files = new ArrayList<>();
if (!root.isDirectory()) {
throw new RuntimeException(
MessageFormat.format(
I18N.getString("error.cannot-walk-directory"),
root.getAbsolutePath()));
}
// sort to files and dirs
File[] children = root.listFiles();
if (children != null) {
for (File f : children) {
if (f.isDirectory()) {
dirs.add(f);
} else {
files.add(f);
}
}
}
// have files => need to output component
out.println(prefix + " <Component Id=\"comp" + (compId++)
+ "\" DiskId=\"1\""
+ " Guid=\"" + UUID.randomUUID().toString() + "\""
+ " Win64=\"yes\""
+ ">");
out.println(prefix + " <CreateFolder/>");
out.println(prefix + " <RemoveFolder Id=\"RemoveDir"
+ (id++) + "\" On=\"uninstall\" />");
boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
File launcherFile = new File(imageRootDir,
WinAppBundler.getLauncherRelativePath(params));
// Find out if we need to use registry. We need it if
// - we doing user level install as file can not serve as KeyPath
// - if we adding shortcut in this component
for (File f: files) {
boolean isLauncher = f.equals(launcherFile);
if (isLauncher) {
needRegistryKey = true;
}
}
if (needRegistryKey) {
// has to be under HKCU to make WiX happy
out.println(prefix + " <RegistryKey Root=\"HKCU\" "
+ " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
+ APP_NAME.fetchFrom(params) + "\""
+ (CAN_USE_WIX36.fetchFrom(params) ?
">" : " Action=\"createAndRemoveOnUninstall\">"));
out.println(prefix
+ " <RegistryValue Name=\"Version\" Value=\""
+ VERSION.fetchFrom(params)
+ "\" Type=\"string\" KeyPath=\"yes\"/>");
out.println(prefix + " </RegistryKey>");
}
boolean menuShortcut = MENU_HINT.fetchFrom(params);
boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
Map<String, String> idToFileMap = new TreeMap<>();
boolean launcherSet = false;
for (File f : files) {
boolean isLauncher = f.equals(launcherFile);
launcherSet = launcherSet || isLauncher;
boolean doShortcuts =
isLauncher && (menuShortcut || desktopShortcut);
String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++));
idToFileMap.put(f.getName(), thisFileId);
out.println(prefix + " <File Id=\"" +
thisFileId + "\""
+ " Name=\"" + f.getName() + "\" "
+ " Source=\"" + relativePath(imageRootDir, f) + "\""
+ " ProcessorArchitecture=\"x64\"" + ">");
if (doShortcuts && desktopShortcut) {
out.println(prefix
+ " <Shortcut Id=\"desktopShortcut\" Directory="
+ "\"DesktopFolder\""
+ " Name=\"" + APP_NAME.fetchFrom(params)
+ "\" WorkingDirectory=\"INSTALLDIR\""
+ " Advertise=\"no\" Icon=\"DesktopIcon.exe\""
+ " IconIndex=\"0\" />");
}
if (doShortcuts && menuShortcut) {
out.println(prefix
+ " <Shortcut Id=\"ExeShortcut\" Directory="
+ "\"ProgramMenuDir\""
+ " Name=\"" + APP_NAME.fetchFrom(params)
+ "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\""
+ " IconIndex=\"0\" />");
}
List<Map<String, ? super Object>> addLaunchers =
ADD_LAUNCHERS.fetchFrom(params);
for (int i = 0; i < addLaunchers.size(); i++) {
Map<String, ? super Object> sl = addLaunchers.get(i);
File addLauncherFile = new File(imageRootDir,
WinAppBundler.getLauncherRelativePath(sl));
if (f.equals(addLauncherFile)) {
if (SHORTCUT_HINT.fetchFrom(sl)) {
out.println(prefix
+ " <Shortcut Id=\"desktopShortcut"
+ i + "\" Directory=\"DesktopFolder\""
+ " Name=\"" + APP_NAME.fetchFrom(sl)
+ "\" WorkingDirectory=\"INSTALLDIR\""
+ " Advertise=\"no\" Icon=\"Launcher"
+ i + ".exe\" IconIndex=\"0\" />");
}
if (MENU_HINT.fetchFrom(sl)) {
out.println(prefix
+ " <Shortcut Id=\"ExeShortcut"
+ i + "\" Directory=\"ProgramMenuDir\""
+ " Name=\"" + APP_NAME.fetchFrom(sl)
+ "\" Advertise=\"no\" Icon=\"Launcher"
+ i + ".exe\" IconIndex=\"0\" />");
// Should we allow different menu groups? Not for now.
}
}
}
out.println(prefix + " </File>");
}
if (launcherSet) {
List<Map<String, ? super Object>> fileAssociations =
FILE_ASSOCIATIONS.fetchFrom(params);
Set<String> defaultedMimes = new TreeSet<>();
for (Map<String, ? super Object> fa : fileAssociations) {
String description = FA_DESCRIPTION.fetchFrom(fa);
List<String> extensions = FA_EXTENSIONS.fetchFrom(fa);
List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa);
File icon = FA_ICON.fetchFrom(fa);
String mime = (mimeTypes == null ||
mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
String entryName = APP_REGISTRY_NAME.fetchFrom(params) + "File";
if (extensions == null) {
Log.verbose(I18N.getString(
"message.creating-association-with-null-extension"));
out.print(prefix + " <ProgId Id='" + entryName
+ "' Description='" + description + "'");
if (icon != null && icon.exists()) {
out.print(" Icon='" + idToFileMap.get(icon.getName())
+ "' IconIndex='0'");
}
out.println(" />");
} else {
for (String ext : extensions) {
entryName = ext.toUpperCase() + "File";
out.print(prefix + " <ProgId Id='" + entryName
+ "' Description='" + description + "'");
if (icon != null && icon.exists()) {
out.print(" Icon='"
+ idToFileMap.get(icon.getName())
+ "' IconIndex='0'");
}
out.println(">");
out.print(prefix + " <Extension Id='"
+ ext + "' Advertise='no'");
if (mime == null) {
out.println(">");
} else {
out.println(" ContentType='" + mime + "'>");
if (!defaultedMimes.contains(mime)) {
out.println(prefix
+ " <MIME ContentType='"
+ mime + "' Default='yes' />");
defaultedMimes.add(mime);
}
}
out.println(prefix
+ " <Verb Id='open' Command='Open' "
+ "TargetFile='" + LAUNCHER_ID
+ "' Argument='\"%1\"' />");
out.println(prefix + " </Extension>");
out.println(prefix + " </ProgId>");
}
}
}
}
out.println(prefix + " </Component>");
for (File d : dirs) {
out.println(prefix + " <Directory Id=\"dirid" + (id++)
+ "\" Name=\"" + d.getName() + "\">");
walkFileTree(params, d, out, prefix + " ");
out.println(prefix + " </Directory>");
}
}
void prepareContentList(Map<String, ? super Object> params)
throws FileNotFoundException {
File f = new File(
CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
try (PrintStream out = new PrintStream(f)) {
// opening
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
out.println("<Include>");
out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
// install to programfiles
out.println(" <Directory Id=\"ProgramFiles64Folder\" "
+ "Name=\"PFiles\">");
} else {
// install to user folder
out.println(
" <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
}
// reset counters
compId = 0;
id = 0;
// We should get valid folder or subfolders
String installDir = WINDOWS_INSTALL_DIR.fetchFrom(params);
String [] installDirs = installDir.split(Pattern.quote("\\"));
for (int i = 0; i < (installDirs.length - 1); i++) {
out.println(" <Directory Id=\"SUBDIR" + i + "\" Name=\""
+ installDirs[i] + "\">");
if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
out.println(" <Component Id=\"comp" + (compId++)
+ "\" DiskId=\"1\""
+ " Guid=\"" + UUID.randomUUID().toString() + "\""
+ " Win64=\"yes\""
+ ">");
out.println("<CreateFolder/>");
// has to be under HKCU to make WiX happy
out.println(" <RegistryKey Root=\"HKCU\" "
+ " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
+ APP_NAME.fetchFrom(params) + "\""
+ (CAN_USE_WIX36.fetchFrom(params) ?
">" : " Action=\"createAndRemoveOnUninstall\">"));
out.println(" <RegistryValue Name=\"Version\" Value=\""
+ VERSION.fetchFrom(params)
+ "\" Type=\"string\" KeyPath=\"yes\"/>");
out.println(" </RegistryKey>");
out.println(" <RemoveFolder Id=\"RemoveDir"
+ (id++) + "\" Directory=\"SUBDIR" + i
+ "\" On=\"uninstall\" />");
out.println("</Component>");
}
}
out.println(" <Directory Id=\"APPLICATIONFOLDER\" Name=\""
+ installDirs[installDirs.length - 1] + "\">");
// dynamic part
walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, " ");
// closing
for (int i = 0; i < installDirs.length; i++) {
out.println(" </Directory>");
}
out.println(" </Directory>");
// for shortcuts
if (SHORTCUT_HINT.fetchFrom(params)) {
out.println(" <Directory Id=\"DesktopFolder\" />");
}
if (MENU_HINT.fetchFrom(params)) {
out.println(" <Directory Id=\"ProgramMenuFolder\">");
out.println(" <Directory Id=\"ProgramMenuDir\" Name=\""
+ MENU_GROUP.fetchFrom(params) + "\">");
out.println(" <Component Id=\"comp" + (compId++) + "\""
+ " Guid=\"" + UUID.randomUUID().toString() + "\""
+ " Win64=\"yes\""
+ ">");
out.println(" <RemoveFolder Id=\"ProgramMenuDir\" "
+ "On=\"uninstall\" />");
// This has to be under HKCU to make WiX happy.
// There are numberous discussions on this amoung WiX users
// (if user A installs and user B uninstalls key is left behind)
// there are suggested workarounds but none are appealing.
// Leave it for now
out.println(
" <RegistryValue Root=\"HKCU\" Key=\"Software\\"
+ VENDOR.fetchFrom(params) + "\\"
+ APP_NAME.fetchFrom(params)
+ "\" Type=\"string\" Value=\"\" />");
out.println(" </Component>");
out.println(" </Directory>");
out.println(" </Directory>");
}
out.println(" </Directory>");
out.println(" <Feature Id=\"DefaultFeature\" "
+ "Title=\"Main Feature\" Level=\"1\">");
for (int j = 0; j < compId; j++) {
out.println(" <ComponentRef Id=\"comp" + j + "\" />");
}
// component is defined in the main.wsx
out.println(
" <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
out.println(" </Feature>");
out.println("</Include>");
}
}
private File getConfig_ProjectFile(Map<String, ? super Object> params) {
return new File(CONFIG_ROOT.fetchFrom(params),
APP_NAME.fetchFrom(params) + ".wxs");
}
private Map<String, String> prepareWiXConfig(
Map<String, ? super Object> params) throws IOException {
prepareContentList(params);
prepareIconsFile(params);
return prepareMainProjectFile(params);
}
private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
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");
File msiOut = new File(
outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
Log.verbose(MessageFormat.format(I18N.getString(
"message.preparing-msi-config"), msiOut.getAbsolutePath()));
msiOut.getParentFile().mkdirs();
List<String> commandLine = new ArrayList<>(Arrays.asList(
getCandlePath(),
"-nologo",
getConfig_ProjectFile(params).getAbsolutePath(),
"-ext", "WixUtilExtension",
"-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 = (LICENSE_FILE.fetchFrom(params) != null);
boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
commandLine = new ArrayList<>();
commandLine.add(getLightPath());
commandLine.add("-nologo");
commandLine.add("-spdb");
if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
commandLine.add("-sice:ICE91");
}
commandLine.add(candleOut.getAbsolutePath());
commandLine.add("-ext");
commandLine.add("WixUtilExtension");
if (enableLicenseUI || enableInstalldirUI) {
commandLine.add("-ext");
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");
commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
}
commandLine.add("-out");
commandLine.add(msiOut.getAbsolutePath());
// create .msi
pb = new ProcessBuilder(commandLine);
pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
IOUtils.exec(pb);
candleOut.delete();
IOUtils.deleteRecursive(tmpDir);
return msiOut;
}
public static void ensureByMutationFileIsRTF(File f) {
if (f == null || !f.isFile()) return;
try {
boolean existingLicenseIsRTF = false;
try (FileInputStream fin = new FileInputStream(f)) {
byte[] firstBits = new byte[7];
if (fin.read(firstBits) == firstBits.length) {
String header = new String(firstBits);
existingLicenseIsRTF = "{\\rtf1\\".equals(header);
}
}
if (!existingLicenseIsRTF) {
List<String> oldLicense = Files.readAllLines(f.toPath());
try (Writer w = Files.newBufferedWriter(
f.toPath(), Charset.forName("Windows-1252"))) {
w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
+ "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
+ "\\viewkind4\\uc1\\pard\\sa200\\sl276"
+ "\\slmult1\\lang9\\fs20 ");
oldLicense.forEach(l -> {
try {
for (char c : l.toCharArray()) {
// 0x00 <= ch < 0x20 Escaped (\'hh)
// 0x20 <= ch < 0x80 Raw(non - escaped) char
// 0x80 <= ch <= 0xFF Escaped(\ 'hh)
// 0x5C, 0x7B, 0x7D (special RTF characters
// \,{,})Escaped(\'hh)
// ch > 0xff Escaped (\\ud###?)
if (c < 0x10) {
w.write("\\'0");
w.write(Integer.toHexString(c));
} else if (c > 0xff) {
w.write("\\ud");
w.write(Integer.toString(c));
// \\uc1 is in the header and in effect
// so we trail with a replacement char if
// the font lacks that character - '?'
w.write("?");
} else if ((c < 0x20) || (c >= 0x80) ||
(c == 0x5C) || (c == 0x7B) ||
(c == 0x7D)) {
w.write("\\'");
w.write(Integer.toHexString(c));
} else {
w.write(c);
}
}
// blank lines are interpreted as paragraph breaks
if (l.length() < 1) {
w.write("\\par");
} else {
w.write(" ");
}
w.write("\r\n");
} catch (IOException e) {
Log.verbose(e);
}
});
w.write("}\r\n");
}
}
} catch (IOException e) {
Log.verbose(e);
}
}
}