--- /dev/null 2018-10-22 10:51:56.000000000 -0400 +++ new/src/jdk.packager/windows/classes/jdk/packager/internal/windows/WinMsiBundler.java 2018-10-22 10:51:54.094960300 -0400 @@ -0,0 +1,1265 @@ +/* + * Copyright (c) 2012, 2018, 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.packager.internal.windows; + +import jdk.packager.internal.*; +import jdk.packager.internal.ConfigException; +import jdk.packager.internal.Arguments; +import jdk.packager.internal.UnsupportedPlatformException; +import jdk.packager.internal.resources.windows.WinResources; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static jdk.packager.internal.windows.WindowsBundlerParam.*; + +public class WinMsiBundler extends AbstractBundler { + + private static final ResourceBundle I18N = + ResourceBundle.getBundle( + "jdk.packager.internal.resources.windows.WinMsiBundler"); + + public static final BundlerParamInfo APP_BUNDLER = + new WindowsBundlerParam<>( + I18N.getString("param.app-bundler.name"), + I18N.getString("param.app-bundler.description"), + "win.app.bundler", + WinAppBundler.class, + params -> new WinAppBundler(), + null); + + public static final BundlerParamInfo CAN_USE_WIX36 = + new WindowsBundlerParam<>( + I18N.getString("param.can-use-wix36.name"), + I18N.getString("param.can-use-wix36.description"), + "win.msi.canUseWix36", + Boolean.class, + params -> false, + (s, p) -> Boolean.valueOf(s)); + + public static final BundlerParamInfo CONFIG_ROOT = + new WindowsBundlerParam<>( + I18N.getString("param.config-root.name"), + I18N.getString("param.config-root.description"), + "configRoot", + File.class, + params -> { + File imagesRoot = + new File(BUILD_ROOT.fetchFrom(params), "windows"); + imagesRoot.mkdirs(); + return imagesRoot; + }, + (s, p) -> null); + + public static final BundlerParamInfo MSI_IMAGE_DIR = + new WindowsBundlerParam<>( + I18N.getString("param.image-dir.name"), + I18N.getString("param.image-dir.description"), + "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<>( + I18N.getString("param.app-dir.name"), + I18N.getString("param.app-dir.description"), + "win.app.image", + File.class, + null, + (s, p) -> null); + + public static final StandardBundlerParam MSI_SYSTEM_WIDE = + new StandardBundlerParam<>( + I18N.getString("param.system-wide.name"), + I18N.getString("param.system-wide.description"), + 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<>( + I18N.getString("param.product-version.name"), + I18N.getString("param.product-version.description"), + "win.msi.productVersion", + String.class, + VERSION::fetchFrom, + (s, p) -> s + ); + + public static final BundlerParamInfo UPGRADE_UUID = + new WindowsBundlerParam<>( + I18N.getString("param.upgrade-uuid.name"), + I18N.getString("param.upgrade-uuid.description"), + Arguments.CLIOptions.WIN_MSI_UPGRADE_UUID.getId(), + UUID.class, + params -> UUID.randomUUID(), // TODO check to see + // if identifier is a valid UUID during default + (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<>( + I18N.getString("param.candle-path.name"), + I18N.getString("param.candle-path.description"), + "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<>( + I18N.getString("param.light-path.name"), + I18N.getString("param.light-path.description"), + "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<>( + I18N.getString("param.menu-shortcut-hint.name"), + I18N.getString("param.menu-shortcut-hint.description"), + 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<>( + I18N.getString("param.desktop-shortcut-hint.name"), + I18N.getString("param.desktop-shortcut-hint.description"), + 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) + ); + + public WinMsiBundler() { + super(); + baseResourceLoader = WinResources.class; + } + + + @Override + public String getName() { + return I18N.getString("bundler.name"); + } + + @Override + public String getDescription() { + return I18N.getString("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) { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported() { + return (Platform.getPlatform() == Platform.WINDOWS); + } + + static class VersionExtractor extends PrintStream { + double version = 0f; + + public VersionExtractor() { + super(new ByteArrayOutputStream()); + } + + double getVersion() { + if (version == 0f) { + String content = + new String(((ByteArrayOutputStream) out).toByteArray()); + Pattern pattern = Pattern.compile("version (\\d+.\\d+)"); + Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + String v = matcher.group(1); + version = new Double(v); + } + } + return version; + } + } + + private static double findToolVersion(String toolName) { + try { + if (toolName == null || "".equals(toolName)) return 0f; + + ProcessBuilder pb = new ProcessBuilder( + toolName, + "/?"); + VersionExtractor ve = new VersionExtractor(); + // not interested in the output + IOUtils.exec(pb, Log.isDebug(), true, ve); + double 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 0f; + } + } + + @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).doValidate(p); + + double candleVersion = + findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p)); + double lightVersion = + findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p)); + + // WiX 3.0+ is required + double minVersion = 3.0f; + boolean bad = false; + + if (candleVersion < minVersion) { + Log.verbose(MessageFormat.format( + I18N.getString("message.wrong-tool-version"), + TOOL_CANDLE, candleVersion, minVersion)); + bad = true; + } + if (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 (lightVersion >= 3.6f) { + 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")); + } + } + } + + // validate license file, if used, exists in the proper place + if (p.containsKey(LICENSE_FILE.getID())) { + List appResourcesList = + APP_RESOURCES_LIST.fetchFrom(p); + for (String license : LICENSE_FILE.fetchFrom(p)) { + boolean found = false; + for (RelativeFileSet appResources : appResourcesList) { + found = found || appResources.contains(license); + } + if (!found) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "error.license-missing"), license), + MessageFormat.format(I18N.getString( + "error.license-missing.advice"), license)); + } + } + } + + 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 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); + + List licenseFiles = LICENSE_FILE.fetchFrom(p); + if (licenseFiles != null) { + // need to copy license file to the root of win.app.image + outerLoop: + for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(p)) { + for (String s : licenseFiles) { + if (rfs.contains(s)) { + File lfile = new File(rfs.getBaseDirectory(), s); + File destFile = new File(appDir, lfile.getName()); + IOUtils.copyFile(lfile, destFile); + ensureByMutationFileIsRTF(destFile); + break outerLoop; + } + } + } + } + + // 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) { + e.printStackTrace(); + } + } + } + + return appDir != null; + } + + public File bundle(Map p, File outdir) { + if (!outdir.isDirectory() && !outdir.mkdirs()) { + throw new RuntimeException(MessageFormat.format( + I18N.getString("error.cannot-create-output-dir"), + outdir.getAbsolutePath())); + } + if (!outdir.canWrite()) { + throw new RuntimeException(MessageFormat.format( + I18N.getString("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.info(I18N.getString("error.no-wix-tools")); + Log.info(MessageFormat.format( + I18N.getString("message.light-file-string"), light)); + Log.info(MessageFormat.format( + I18N.getString("message.candle-file-string"), candle)); + return null; + } + + 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.info(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); + return null; + } finally { + try { + if (imageDir != null && + PREDEFINED_APP_IMAGE.fetchFrom(p) == null && + (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null || + !Arguments.CREATE_JRE_INSTALLER.fetchFrom(p)) && + !Log.isDebug()) { + IOUtils.deleteRecursive(imageDir); + } else if (imageDir != null) { + Log.info(MessageFormat.format( + I18N.getString("message.debug-working-directory"), + imageDir.getAbsolutePath())); + } + + cleanupConfigFiles(p); + } catch (IOException ex) { + // noinspection ReturnInsideFinallyBlock + Log.debug(ex.getMessage()); + return null; + } + } + } + + protected void cleanupConfigFiles(Map params) { + if (getConfig_ProjectFile(params) != null) { + getConfig_ProjectFile(params).delete(); + } + if (getConfig_Script(params) != null) { + getConfig_Script(params).delete(); + } + } + + // name of post-image script + private File getConfig_Script(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_FS_NAME.fetchFrom(params) + "-post-image.wsf"); + } + + private boolean prepareBasicProjectConfig( + Map params) throws IOException { + fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + + getConfig_Script(params).getName(), + I18N.getString("resource.post-install-script"), + (String) null, + getConfig_Script(params), + VERBOSE.fetchFrom(params), + DROP_IN_RESOURCES_ROOT.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("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"); + } + + if (BIT_ARCH_64.fetchFrom(params)) { + data.put("PLATFORM", "x64"); + data.put("WIN64", "yes"); + } else { + data.put("PLATFORM", "x86"); + data.put("WIN64", "no"); + } + + data.put("UI_BLOCK", getUIBlock(params)); + + List> secondaryLaunchers = + SECONDARY_LAUNCHERS.fetchFrom(params); + + StringBuilder secondaryLauncherIcons = new StringBuilder(); + for (int i = 0; i < secondaryLaunchers.size(); i++) { + Map sl = secondaryLaunchers.get(i); + // + if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) { + File secondaryLauncher = new File(imageRootDir, + WinAppBundler.getLauncherName(sl)); + String secondaryLauncherPath = + relativePath(imageRootDir, secondaryLauncher); + String secondaryLauncherIconPath = + secondaryLauncherPath.replace(".exe", ".ico"); + + secondaryLauncherIcons.append(" \r\n"); + } + } + data.put("SECONDARY_LAUNCHER_ICONS", secondaryLauncherIcons.toString()); + + String wxs = Arguments.CREATE_JRE_INSTALLER.fetchFrom(params) ? + MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE; + + Writer w = new BufferedWriter( + new FileWriter(getConfig_ProjectFile(params))); + + String content = preprocessTextResource( + WinAppBundler.WIN_BUNDLER_PREFIX + + getConfig_ProjectFile(params).getName(), + I18N.getString("resource.wix-config-file"), + wxs, data, VERBOSE.fetchFrom(params), + DROP_IN_RESOURCES_ROOT.fetchFrom(params)); + w.write(content); + w.close(); + return true; + } + private int id; + private int compId; + private final static String LAUNCHER_ID = "LauncherId"; + private final static String LAUNCHER_SVC_ID = "LauncherSvcId"; + + /** + * 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"; + + /** + * 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) { + String uiBlock = " \n"; // UI-less element + + if (INSTALLDIR_CHOOSER.fetchFrom(params)) { + boolean enableTweakForExcludingLicense = + (getLicenseFile(params) == null); + uiBlock = " \n" + + " \n" + + " \n" + + (enableTweakForExcludingLicense ? + TWEAK_FOR_EXCLUDING_LICENSE : "") + +" \n"; + } else if (getLicenseFile(params) != null) { + uiBlock = " \n" + + " \n" + + " \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> secondaryLaunchers = + SECONDARY_LAUNCHERS.fetchFrom(params); + for (int i = 0; i < secondaryLaunchers.size(); i++) { + Map sl = secondaryLaunchers.get(i); + File secondaryLauncherFile = new File(imageRootDir, + WinAppBundler.getLauncherName(sl)); + if (f.equals(secondaryLauncherFile)) { + 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.info(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.info(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 + if (BIT_ARCH_64.fetchFrom(params)) { + out.println(" "); + } else { + out.println(" "); + } + } else { + // install to user folder + out.println( + " "); + } + out.println(" "); + + // dynamic part + id = 0; + compId = 0; // reset counters + walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, " "); + + // closing + 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 params) { + List licenseFiles = LICENSE_FILE.fetchFrom(params); + if (licenseFiles == null || licenseFiles.isEmpty()) { + return null; + } else { + return licenseFiles.get(0); + } + } + + 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(BUILD_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"); + } + 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); + } + + } +}