/* * 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.text.MessageFormat; import java.util.*; import java.util.regex.Pattern; import static jdk.jpackage.internal.WindowsBundlerParam.*; 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"; public static final BundlerParamInfo TOOL_CANDLE_EXECUTABLE = new WindowsBundlerParam<>( "win.msi.candle.exe", String.class, params -> { 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; }, null); public static final BundlerParamInfo TOOL_LIGHT_EXECUTABLE = new WindowsBundlerParam<>( "win.msi.light.exe", String.class, params -> { 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; }, 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 getDescription() { return I18N.getString("msi.bundler.description"); } @Override public String getID() { return "msi"; } @Override public String getBundleType() { return "INSTALLER"; } @Override public Collection> getBundleParameters() { Collection> results = new LinkedHashSet<>(); results.addAll(WinAppBundler.getAppBundleParameters()); results.addAll(getMsiBundleParameters()); return results; } public static Collection> getMsiBundleParameters() { return Arrays.asList( DESCRIPTION, MENU_GROUP, MENU_HINT, PRODUCT_VERSION, SHORTCUT_HINT, MSI_SYSTEM_WIDE, VENDOR, LICENSE_FILE, INSTALLDIR_CHOOSER ); } @Override public File execute(Map params, File outputParentDir) throws PackagerException { return bundle(params, outputParentDir); } @Override public boolean supported(boolean platformInstaller) { return (Platform.getPlatform() == Platform.WINDOWS); } 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, Log.isDebug(), true, ve); String version = ve.getVersion(); Log.verbose(MessageFormat.format( I18N.getString("message.tool-version"), toolName, version)); return version; } catch (Exception e) { if (Log.isDebug()) { Log.verbose(e); } return null; } } @Override public boolean validate(Map p) throws UnsupportedPlatformException, ConfigException { try { if (p == 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 APP_BUNDLER.fetchFrom(p).validate(p); String candleVersion = findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p)); String lightVersion = findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p)); // WiX 3.0+ is required String minVersion = "3.0"; boolean bad = false; if (VersionExtractor.isLessThan(candleVersion, minVersion)) { Log.verbose(MessageFormat.format( I18N.getString("message.wrong-tool-version"), TOOL_CANDLE, candleVersion, minVersion)); bad = true; } if (VersionExtractor.isLessThan(lightVersion, minVersion)) { Log.verbose(MessageFormat.format( I18N.getString("message.wrong-tool-version"), TOOL_LIGHT, lightVersion, minVersion)); bad = true; } if (bad){ throw new ConfigException( I18N.getString("error.no-wix-tools"), I18N.getString("error.no-wix-tools.advice")); } if (!VersionExtractor.isLessThan(lightVersion, "3.6")) { Log.verbose(I18N.getString("message.use-wix36-features")); p.put(CAN_USE_WIX36.getID(), Boolean.TRUE); } /********* validate bundle parameters *************/ String version = PRODUCT_VERSION.fetchFrom(p); 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(p); 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 p) throws PackagerException, IOException { File appImage = StandardBundlerParam.getPredefinedAppImage(p); File appDir = null; // we either have an application image or need to build one if (appImage != null) { appDir = new File( MSI_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p)); // copy everything from appImage dir into appDir/name IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); } else { appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, MSI_IMAGE_DIR.fetchFrom(p), true); } p.put(WIN_APP_IMAGE.getID(), appDir); String licenseFile = LICENSE_FILE.fetchFrom(p); 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(p), lfile.getName()); IOUtils.copyFile(lfile, destFile); ensureByMutationFileIsRTF(destFile); } // copy file association icons List> fileAssociations = FILE_ASSOCIATIONS.fetchFrom(p); for (Map fa : fileAssociations) { File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO 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 p, File outdir) throws PackagerException { if (!outdir.isDirectory() && !outdir.mkdirs()) { throw new PackagerException("error.cannot-create-output-dir", outdir.getAbsolutePath()); } if (!outdir.canWrite()) { throw new PackagerException("error.cannot-write-to-output-dir", outdir.getAbsolutePath()); } // validate we have valid tools before continuing String light = TOOL_LIGHT_EXECUTABLE.fetchFrom(p); String candle = TOOL_CANDLE_EXECUTABLE.fetchFrom(p); 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"); } File imageDir = MSI_IMAGE_DIR.fetchFrom(p); try { imageDir.mkdirs(); boolean menuShortcut = MENU_HINT.fetchFrom(p); boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p); if (!menuShortcut && !desktopShortcut) { // both can not be false - user will not find the app Log.verbose(I18N.getString("message.one-shortcut-required")); p.put(MENU_HINT.getID(), true); } if (prepareProto(p) && prepareWiXConfig(p) && prepareBasicProjectConfig(p)) { File configScriptSrc = getConfig_Script(p); 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, false); } return buildMSI(p, 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 boolean 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)); return true; } private String relativePath(File basedir, File file) { return file.getAbsolutePath().substring( basedir.getAbsolutePath().length() + 1); } boolean 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("PRODUCT_GUID", productGUID.toString()); data.put("PRODUCT_UPGRADE_GUID", 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)); String launcherPath = relativePath(imageRootDir, launcher); data.put("APPLICATION_LAUNCHER", launcherPath); String iconPath = launcherPath.replace(".exe", ".ico"); data.put("APPLICATION_ICON", iconPath); data.put("REGISTRY_ROOT", getRegistryRoot(params)); boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params); data.put("WIX36_ONLY_START", canUseWix36Features ? "" : ""); if (MSI_SYSTEM_WIDE.fetchFrom(params)) { data.put("INSTALL_SCOPE", "perMachine"); } else { data.put("INSTALL_SCOPE", "perUser"); } data.put("PLATFORM", "x64"); data.put("WIN64", "yes"); data.put("UI_BLOCK", getUIBlock(params)); // Add CA to check install dir 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", ""); } List> addLaunchers = ADD_LAUNCHERS.fetchFrom(params); StringBuilder addLauncherIcons = new StringBuilder(); 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.getLauncherName(sl)); String addLauncherPath = relativePath(imageRootDir, addLauncher); String addLauncherIconPath = addLauncherPath.replace(".exe", ".ico"); addLauncherIcons.append(" \r\n"); } } data.put("ADD_LAUNCHER_ICONS", addLauncherIcons.toString()); String wxs = StandardBundlerParam.isRuntimeInstaller(params) ? MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE; Writer w = new BufferedWriter( new FileWriter(getConfig_ProjectFile(params))); 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); w.close(); return true; } private int id; private int compId; private final static String LAUNCHER_ID = "LauncherId"; private static final String CA_BLOCK = "\n" + ""; private static final String INVALID_INSTALL_DIR_DLG_BLOCK = "\n" + "\n" + "1\n" + "\n" + "\n" + "1\n" + "\n" + "\n" + "" + I18N.getString("message.install.dir.exist") + "\n" + "\n" + ""; /** * Overrides the dialog sequence in built-in dialog set "WixUI_InstallDir" * to exclude license dialog */ private static final String TWEAK_FOR_EXCLUDING_LICENSE = " 1\n" + " 1\n"; private static final String CHECK_INSTALL_DLG_CTRL = " 1\n" + " INSTALLDIR_VALID=\"0\"\n" + " INSTALLDIR_VALID=\"1\"\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 = ""; private String getUpgradeBlock(Map 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 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 = " \n" + " \n" + (enableTweakForExcludingLicense ? TWEAK_FOR_EXCLUDING_LICENSE : "") + CHECK_INSTALL_DLG_CTRL; } else if (getLicenseFile(params) != null) { uiBlock = " \n"; } return uiBlock; } 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.getLauncherName(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.getLauncherName(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); String regName = APP_REGISTRY_NAME.fetchFrom(params); Set defaultedMimes = new TreeSet<>(); int count = 0; 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); // TODO FA_ICON_ICO String mime = (mimeTypes == null || mimeTypes.isEmpty()) ? null : mimeTypes.get(0); if (extensions == null) { Log.verbose(I18N.getString( "message.creating-association-with-null-extension")); String entryName = regName + "File"; if (count > 0) { entryName += "." + count; } count++; out.print(prefix + " "); } else { for (String ext : extensions) { String entryName = regName + "File"; if (count > 0) { entryName += "." + count; } count++; out.print(prefix + " "); if (extensions == null) { Log.verbose(I18N.getString( "message.creating-association-with-null-extension")); } else { 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 + " "); } } String getRegistryRoot(Map params) { if (MSI_SYSTEM_WIDE.fetchFrom(params)) { return "HKLM"; } else { return "HKCU"; } } boolean prepareContentList(Map params) throws FileNotFoundException { File f = new File( CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE); 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( " "); } // 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(" "); } out.println(" "); // dynamic part id = 0; compId = 0; // reset counters 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 of them 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 template.wsx out.println(" "); out.println(" "); out.println(""); out.close(); return true; } private File getConfig_ProjectFile(Map params) { return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".wxs"); } private String getLicenseFile(Map p) { String licenseFile = LICENSE_FILE.fetchFrom(p); if (licenseFile != null) { File lfile = new File(licenseFile); File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); String filePath = destFile.getAbsolutePath(); if (filePath.contains(" ")) { return "\"" + filePath + "\""; } else { return filePath; } } return null; } private boolean prepareWiXConfig( Map 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 params, 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(); // run candle ProcessBuilder pb = new ProcessBuilder( TOOL_CANDLE_EXECUTABLE.fetchFrom(params), "-nologo", getConfig_ProjectFile(params).getAbsolutePath(), "-ext", "WixUtilExtension", "-out", candleOut.getAbsolutePath()); pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params)); IOUtils.exec(pb, false); Log.verbose(MessageFormat.format(I18N.getString( "message.generating-msi"), msiOut.getAbsolutePath())); boolean enableLicenseUI = (getLicenseFile(params) != null); boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params); List 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"); // ignore warnings due to "missing launcguage info" (ICE60) commandLine.add(candleOut.getAbsolutePath()); commandLine.add("-ext"); commandLine.add("WixUtilExtension"); if (enableLicenseUI || enableInstalldirUI) { commandLine.add("-ext"); commandLine.add("WixUIExtension.dll"); } // 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, false); 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); } } }