/* * 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. *

* {@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: *

*

* main.wxs file is a copy of main.wxs resource from * jdk.jpackage.internal.resources package. It is parametrized with the * following WiX variables: *

*/ public class WinMsiBundler extends AbstractBundler { private static final ResourceBundle I18N = ResourceBundle.getBundle( "jdk.jpackage.internal.resources.WinResources"); public static final BundlerParamInfo APP_BUNDLER = new WindowsBundlerParam<>( "win.app.bundler", WinAppBundler.class, params -> new WinAppBundler(), null); public static final BundlerParamInfo CAN_USE_WIX36 = new WindowsBundlerParam<>( "win.msi.canUseWix36", Boolean.class, params -> false, (s, p) -> Boolean.valueOf(s)); public static final BundlerParamInfo 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 WIN_APP_IMAGE = new WindowsBundlerParam<>( "win.app.image", File.class, null, (s, p) -> null); public static final StandardBundlerParam 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 PRODUCT_VERSION = new StandardBundlerParam<>( "win.msi.productVersion", String.class, VERSION::fetchFrom, (s, p) -> s ); public static final BundlerParamInfo 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 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 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 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 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> associations = FILE_ASSOCIATIONS.fetchFrom(params); if (associations != null) { for (int i = 0; i < associations.size(); i++) { Map assoc = associations.get(i); List 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 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> fileAssociations = FILE_ASSOCIATIONS.fetchFrom(params); for (Map 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 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 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 params) { return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.wsf"); } private void prepareBasicProjectConfig( Map 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 params) throws IOException { File imageRootDir = WIN_APP_IMAGE.fetchFrom(params); List> 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 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 prepareMainProjectFile( Map params) throws IOException { Map 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 params, File root, PrintStream out, String prefix) { List dirs = new ArrayList<>(); List 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 + " "); out.println(prefix + " "); out.println(prefix + " "); 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 + " " : " Action=\"createAndRemoveOnUninstall\">")); out.println(prefix + " "); out.println(prefix + " "); } boolean menuShortcut = MENU_HINT.fetchFrom(params); boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params); Map 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 + " "); if (doShortcuts && desktopShortcut) { out.println(prefix + " "); } if (doShortcuts && menuShortcut) { out.println(prefix + " "); } List> addLaunchers = ADD_LAUNCHERS.fetchFrom(params); for (int i = 0; i < addLaunchers.size(); i++) { Map sl = addLaunchers.get(i); File addLauncherFile = new File(imageRootDir, WinAppBundler.getLauncherRelativePath(sl)); if (f.equals(addLauncherFile)) { if (SHORTCUT_HINT.fetchFrom(sl)) { out.println(prefix + " "); } if (MENU_HINT.fetchFrom(sl)) { out.println(prefix + " "); // Should we allow different menu groups? Not for now. } } } out.println(prefix + " "); } if (launcherSet) { List> fileAssociations = FILE_ASSOCIATIONS.fetchFrom(params); Set defaultedMimes = new TreeSet<>(); for (Map fa : fileAssociations) { String description = FA_DESCRIPTION.fetchFrom(fa); List extensions = FA_EXTENSIONS.fetchFrom(fa); List 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 + " "); } else { for (String ext : extensions) { entryName = ext.toUpperCase() + "File"; out.print(prefix + " "); out.print(prefix + " "); } else { out.println(" ContentType='" + mime + "'>"); if (!defaultedMimes.contains(mime)) { out.println(prefix + " "); defaultedMimes.add(mime); } } out.println(prefix + " "); out.println(prefix + " "); out.println(prefix + " "); } } } } out.println(prefix + " "); for (File d : dirs) { out.println(prefix + " "); walkFileTree(params, d, out, prefix + " "); out.println(prefix + " "); } } void prepareContentList(Map params) throws FileNotFoundException { File f = new File( CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE); try (PrintStream out = new PrintStream(f)) { // opening out.println(""); out.println(""); out.println(" "); if (MSI_SYSTEM_WIDE.fetchFrom(params)) { // install to programfiles out.println(" "); } else { // install to user folder out.println( " "); } // 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(" "); if (!MSI_SYSTEM_WIDE.fetchFrom(params)) { out.println(" "); out.println(""); // has to be under HKCU to make WiX happy out.println(" " : " Action=\"createAndRemoveOnUninstall\">")); out.println(" "); out.println(" "); out.println(" "); out.println(""); } } out.println(" "); // dynamic part walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, " "); // closing for (int i = 0; i < installDirs.length; i++) { out.println(" "); } out.println(" "); // for shortcuts if (SHORTCUT_HINT.fetchFrom(params)) { out.println(" "); } if (MENU_HINT.fetchFrom(params)) { out.println(" "); out.println(" "); out.println(" "); out.println(" "); // 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( " "); out.println(" "); out.println(" "); out.println(" "); } out.println(" "); out.println(" "); for (int j = 0; j < compId; j++) { out.println(" "); } // component is defined in the main.wsx out.println( " "); out.println(" "); out.println(""); } } private File getConfig_ProjectFile(Map params) { return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".wxs"); } private Map prepareWiXConfig( Map params) throws IOException { prepareContentList(params); prepareIconsFile(params); return prepareMainProjectFile(params); } private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi"; private File buildMSI(Map params, Map 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 commandLine = new ArrayList<>(Arrays.asList( getCandlePath(), "-nologo", getConfig_ProjectFile(params).getAbsolutePath(), "-ext", "WixUtilExtension", "-out", candleOut.getAbsolutePath())); for(Map.Entry 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 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); } } }