--- /dev/null 2019-01-11 12:14:14.000000000 -0500 +++ new/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java 2019-01-11 12:14:13.191276000 -0500 @@ -0,0 +1,913 @@ +/* + * Copyright (c) 2017, 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.Matcher; +import java.util.regex.Pattern; + +import static jdk.jpackage.internal.WindowsBundlerParam.*; + +public class WinExeBundler extends AbstractBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.jpackage.internal.resources.WinResources"); + + public static final BundlerParamInfo APP_BUNDLER = + new WindowsBundlerParam<>( + getString("param.exe-bundler.name"), + getString("param.exe-bundler.description"), + "win.app.bundler", + WinAppBundler.class, + params -> new WinAppBundler(), + null); + + public static final BundlerParamInfo EXE_IMAGE_DIR = + new WindowsBundlerParam<>( + getString("param.image-dir.name"), + getString("param.image-dir.description"), + "win.exe.imageDir", + File.class, + params -> { + File imagesRoot = IMAGES_ROOT.fetchFrom(params); + if (!imagesRoot.exists()) imagesRoot.mkdirs(); + return new File(imagesRoot, "win-exe.image"); + }, + (s, p) -> null); + + public static final BundlerParamInfo WIN_APP_IMAGE = + new WindowsBundlerParam<>( + getString("param.app-dir.name"), + getString("param.app-dir.description"), + "win.app.image", + File.class, + null, + (s, p) -> null); + + public static final BundlerParamInfo UPGRADE_UUID = + new WindowsBundlerParam<>( + I18N.getString("param.upgrade-uuid.name"), + I18N.getString("param.upgrade-uuid.description"), + Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(), + UUID.class, + params -> UUID.randomUUID(), + (s, p) -> UUID.fromString(s)); + + public static final StandardBundlerParam EXE_SYSTEM_WIDE = + new StandardBundlerParam<>( + getString("param.system-wide.name"), + getString("param.system-wide.description"), + Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(), + Boolean.class, + params -> true, // default to system wide + (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null + : Boolean.valueOf(s) + ); + public static final StandardBundlerParam PRODUCT_VERSION = + new StandardBundlerParam<>( + getString("param.product-version.name"), + getString("param.product-version.description"), + "win.msi.productVersion", + String.class, + VERSION::fetchFrom, + (s, p) -> s + ); + + public static final StandardBundlerParam MENU_HINT = + new WindowsBundlerParam<>( + getString("param.menu-shortcut-hint.name"), + getString("param.menu-shortcut-hint.description"), + Arguments.CLIOptions.WIN_MENU_HINT.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) + ); + + public static final StandardBundlerParam SHORTCUT_HINT = + new WindowsBundlerParam<>( + getString("param.desktop-shortcut-hint.name"), + getString("param.desktop-shortcut-hint.description"), + Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) + ); + + private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss"; + private final static String DEFAULT_JRE_EXE_TEMPLATE = "template.jre.iss"; + private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe"; + + public static final BundlerParamInfo + TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>( + getString("param.iscc-path.name"), + getString("param.iscc-path.description"), + "win.exe.iscc.exe", + String.class, + params -> { + for (String dirString : (System.getenv("PATH") + + ";C:\\Program Files (x86)\\Inno Setup 5;" + + "C:\\Program Files\\Inno Setup 5").split(";")) { + File f = new File(dirString.replace("\"", ""), + TOOL_INNO_SETUP_COMPILER); + if (f.isFile()) { + return f.toString(); + } + } + return null; + }, + null); + + @Override + public String getName() { + return getString("exe.bundler.name"); + } + + @Override + public String getDescription() { + return getString("exe.bundler.description"); + } + + @Override + public String getID() { + return "exe"; + } + + @Override + public String getBundleType() { + return "INSTALLER"; + } + + @Override + public Collection> getBundleParameters() { + Collection> results = new LinkedHashSet<>(); + results.addAll(WinAppBundler.getAppBundleParameters()); + results.addAll(getExeBundleParameters()); + return results; + } + + public static Collection> getExeBundleParameters() { + return Arrays.asList( + DESCRIPTION, + COPYRIGHT, + LICENSE_FILE, + MENU_GROUP, + MENU_HINT, + SHORTCUT_HINT, + EXE_SYSTEM_WIDE, + TITLE, + VENDOR, + INSTALLDIR_CHOOSER + ); + } + + @Override + public File execute( + Map p, File outputParentDir) { + return bundle(p, 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("Inno Setup (\\d+.?\\d*)"); + Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + String v = matcher.group(1); + version = Double.parseDouble(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(); + IOUtils.exec(pb, Log.isDebug(), true, ve); + // not interested in the output + double version = ve.getVersion(); + Log.verbose(MessageFormat.format( + 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( + getString("error.parameters-null"), + 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); + + // make sure some key values don't have newlines + for (BundlerParamInfo pi : Arrays.asList( + APP_NAME, + COPYRIGHT, + DESCRIPTION, + MENU_GROUP, + TITLE, + VENDOR, + VERSION) + ) { + String v = pi.fetchFrom(p); + if (v.contains("\n") | v.contains("\r")) { + throw new ConfigException("Parmeter '" + pi.getID() + + "' cannot contain a newline.", + " Change the value of '" + pi.getID() + + " so that it does not contain any newlines"); + } + } + + // exe bundlers trim the copyright to 100 characters, + // tell them this will happen + if (COPYRIGHT.fetchFrom(p).length() > 100) { + throw new ConfigException( + getString("error.copyright-is-too-long"), + getString("error.copyright-is-too-long.advice")); + } + + double innoVersion = findToolVersion( + TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p)); + + //Inno Setup 5+ is required + double minVersion = 5.0f; + + if (innoVersion < minVersion) { + Log.error(MessageFormat.format( + getString("message.tool-wrong-version"), + TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion)); + throw new ConfigException( + getString("error.iscc-not-found"), + getString("error.iscc-not-found.advice")); + } + + /********* validate bundle parameters *************/ + + // 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( + getString("error.too-many-content-" + + "types-for-file-association"), i), + 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); + } + } + } + + 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( + EXE_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, + EXE_IMAGE_DIR.fetchFrom(p), true); + } + + if (appDir == null) { + return false; + } + + 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) { + e.printStackTrace(); + } + } + } + + return true; + } + + public File bundle(Map p, File outdir) { + if (!outdir.isDirectory() && !outdir.mkdirs()) { + throw new RuntimeException(MessageFormat.format( + getString("error.cannot-create-output-dir"), + outdir.getAbsolutePath())); + } + if (!outdir.canWrite()) { + throw new RuntimeException(MessageFormat.format( + getString("error.cannot-write-to-output-dir"), + outdir.getAbsolutePath())); + } + + if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) { + Log.error(MessageFormat.format( + getString("message.potential.windows.defender.issue"), + WindowsDefender.getUserTempDirectory())); + } + + // validate we have valid tools before continuing + String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p); + if (iscc == null || !new File(iscc).isFile()) { + Log.error(getString("error.iscc-not-found")); + Log.error(MessageFormat.format( + getString("message.iscc-file-string"), iscc)); + return null; + } + + File imageDir = EXE_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(getString("message.one-shortcut-required")); + p.put(MENU_HINT.getID(), true); + } + + if (prepareProto(p) && prepareProjectConfig(p)) { + File configScript = getConfig_Script(p); + if (configScript.exists()) { + Log.verbose(MessageFormat.format( + getString("message.running-wsh-script"), + configScript.getAbsolutePath())); + IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p)); + } + return buildEXE(p, outdir); + } + return null; + } catch (IOException ex) { + ex.printStackTrace(); + return null; + } + } + + // name of post-image script + private File getConfig_Script(Map p) { + return new File(EXE_IMAGE_DIR.fetchFrom(p), + APP_NAME.fetchFrom(p) + "-post-image.wsf"); + } + + private String getAppIdentifier(Map p) { + String nm = UPGRADE_UUID.fetchFrom(p).toString(); + + // limitation of innosetup + if (nm.length() > 126) { + Log.error(getString("message-truncating-id")); + nm = nm.substring(0, 126); + } + + return nm; + } + + 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; + } + + void validateValueAndPut(Map data, String key, + BundlerParamInfo param, + Map p) throws IOException { + String value = param.fetchFrom(p); + if (value.contains("\r") || value.contains("\n")) { + throw new IOException("Configuration Parameter " + + param.getID() + " cannot contain multiple lines of text"); + } + data.put(key, innosetupEscape(value)); + } + + private String innosetupEscape(String value) { + if (value == null) { + return ""; + } + + if (value.contains("\"") || !value.trim().equals(value)) { + value = "\"" + value.replace("\"", "\"\"") + "\""; + } + return value; + } + + boolean prepareMainProjectFile(Map p) + throws IOException { + Map data = new HashMap<>(); + data.put("PRODUCT_APP_IDENTIFIER", + innosetupEscape(getAppIdentifier(p))); + + + validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p); + validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p); + validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p); + validateValueAndPut(data, "INSTALLER_FILE_NAME", + INSTALLER_FILE_NAME, p); + + data.put("LAUNCHER_NAME", + innosetupEscape(WinAppBundler.getAppName(p))); + + data.put("APPLICATION_LAUNCHER_FILENAME", + innosetupEscape(WinAppBundler.getLauncherName(p))); + + data.put("APPLICATION_DESKTOP_SHORTCUT", + SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); + data.put("APPLICATION_MENU_SHORTCUT", + MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); + validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p); + validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p); + validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p); + + data.put("APPLICATION_LICENSE_FILE", + innosetupEscape(getLicenseFile(p))); + data.put("DISABLE_DIR_PAGE", + INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes"); + + Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p); + + if (isSystemWide) { + data.put("APPLICATION_INSTALL_ROOT", "{pf}"); + data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); + } else { + data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); + data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); + } + + if (BIT_ARCH_64.fetchFrom(p)) { + data.put("ARCHITECTURE_BIT_MODE", "x64"); + } else { + data.put("ARCHITECTURE_BIT_MODE", ""); + } + validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p); + + validateValueAndPut(data, "APPLICATION_DESCRIPTION", + DESCRIPTION, p); + + data.put("APPLICATION_SERVICE", "returnFalse"); + data.put("APPLICATION_NOT_SERVICE", "returnFalse"); + data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse"); + data.put("START_ON_INSTALL", ""); + data.put("STOP_ON_UNINSTALL", ""); + data.put("RUN_AT_STARTUP", ""); + + String imagePathString = + WIN_APP_IMAGE.fetchFrom(p).toPath().toAbsolutePath().toString(); + data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString)); + Log.verbose("setting APPLICATION_IMAGE to " + + innosetupEscape(imagePathString) + " for InnoSetup"); + + StringBuilder secondaryLaunchersCfg = new StringBuilder(); + for (Map + launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) { + String application_name = APP_NAME.fetchFrom(launcher); + if (MENU_HINT.fetchFrom(launcher)) { + // Name: "{group}\APPLICATION_NAME"; + // Filename: "{app}\APPLICATION_NAME.exe"; + // IconFilename: "{app}\APPLICATION_NAME.ico" + secondaryLaunchersCfg.append("Name: \"{group}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append(".ico\"\r\n"); + } + if (SHORTCUT_HINT.fetchFrom(launcher)) { + // Name: "{commondesktop}\APPLICATION_NAME"; + // Filename: "{app}\APPLICATION_NAME.exe"; + // IconFilename: "{app}\APPLICATION_NAME.ico" + secondaryLaunchersCfg.append("Name: \"{commondesktop}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); + secondaryLaunchersCfg.append(application_name); + secondaryLaunchersCfg.append(".ico\"\r\n"); + } + } + data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString()); + + StringBuilder registryEntries = new StringBuilder(); + String regName = APP_REGISTRY_NAME.fetchFrom(p); + List> fetchFrom = + FILE_ASSOCIATIONS.fetchFrom(p); + for (int i = 0; i < fetchFrom.size(); i++) { + Map fileAssociation = fetchFrom.get(i); + String description = FA_DESCRIPTION.fetchFrom(fileAssociation); + File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO + + List extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); + String entryName = regName + "File"; + if (i > 0) { + entryName += "." + i; + } + + if (extensions == null) { + Log.verbose(getString( + "message.creating-association-with-null-extension")); + } else { + for (String ext : extensions) { + if (isSystemWide) { + // "Root: HKCR; Subkey: \".myp\"; + // ValueType: string; ValueName: \"\"; + // ValueData: \"MyProgramFile\"; + // Flags: uninsdeletevalue" + registryEntries.append("Root: HKCR; Subkey: \".") + .append(ext) + .append("\"; ValueType: string;" + + " ValueName: \"\"; ValueData: \"") + .append(entryName) + .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); + } else { + registryEntries.append( + "Root: HKCU; Subkey: \"Software\\Classes\\.") + .append(ext) + .append("\"; ValueType: string;" + + " ValueName: \"\"; ValueData: \"") + .append(entryName) + .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); + } + } + } + + if (extensions != null && !extensions.isEmpty()) { + String ext = extensions.get(0); + List mimeTypes = + FA_CONTENT_TYPE.fetchFrom(fileAssociation); + for (String mime : mimeTypes) { + if (isSystemWide) { + // "Root: HKCR; + // Subkey: HKCR\\Mime\\Database\\ + // Content Type\\application/chaos; + // ValueType: string; + // ValueName: Extension; + // ValueData: .chaos; + // Flags: uninsdeletevalue" + registryEntries.append("Root: HKCR; Subkey: " + + "\"Mime\\Database\\Content Type\\") + .append(mime) + .append("\"; ValueType: string; ValueName: " + + "\"Extension\"; ValueData: \".") + .append(ext) + .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); + } else { + registryEntries.append( + "Root: HKCU; Subkey: \"Software\\" + + "Classes\\Mime\\Database\\Content Type\\") + .append(mime) + .append("\"; ValueType: string; " + + "ValueName: \"Extension\"; ValueData: \".") + .append(ext) + .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); + } + } + } + + if (isSystemWide) { + // "Root: HKCR; + // Subkey: \"MyProgramFile\"; + // ValueType: string; + // ValueName: \"\"; + // ValueData: \"My Program File\"; + // Flags: uninsdeletekey" + registryEntries.append("Root: HKCR; Subkey: \"") + .append(entryName) + .append( + "\"; ValueType: string; ValueName: \"\"; ValueData: \"") + .append(removeQuotes(description)) + .append("\"; Flags: uninsdeletekey\r\n"); + } else { + registryEntries.append( + "Root: HKCU; Subkey: \"Software\\Classes\\") + .append(entryName) + .append( + "\"; ValueType: string; ValueName: \"\"; ValueData: \"") + .append(removeQuotes(description)) + .append("\"; Flags: uninsdeletekey\r\n"); + } + + if (icon != null && icon.exists()) { + if (isSystemWide) { + // "Root: HKCR; + // Subkey: \"MyProgramFile\\DefaultIcon\"; + // ValueType: string; + // ValueName: \"\"; + // ValueData: \"{app}\\MYPROG.EXE,0\"\n" + + registryEntries.append("Root: HKCR; Subkey: \"") + .append(entryName) + .append("\\DefaultIcon\"; ValueType: string; " + + "ValueName: \"\"; ValueData: \"{app}\\") + .append(icon.getName()) + .append("\"\r\n"); + } else { + registryEntries.append( + "Root: HKCU; Subkey: \"Software\\Classes\\") + .append(entryName) + .append("\\DefaultIcon\"; ValueType: string; " + + "ValueName: \"\"; ValueData: \"{app}\\") + .append(icon.getName()) + .append("\"\r\n"); + } + } + + if (isSystemWide) { + // "Root: HKCR; + // Subkey: \"MyProgramFile\\shell\\open\\command\"; + // ValueType: string; + // ValueName: \"\"; + // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n" + registryEntries.append("Root: HKCR; Subkey: \"") + .append(entryName) + .append("\\shell\\open\\command\"; ValueType: " + + "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") + .append(APP_NAME.fetchFrom(p)) + .append("\"\" \"\"%1\"\"\"\r\n"); + } else { + registryEntries.append( + "Root: HKCU; Subkey: \"Software\\Classes\\") + .append(entryName) + .append("\\shell\\open\\command\"; ValueType: " + + "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") + .append(APP_NAME.fetchFrom(p)) + .append("\"\" \"\"%1\"\"\"\r\n"); + } + } + if (registryEntries.length() > 0) { + data.put("FILE_ASSOCIATIONS", + "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + + registryEntries.toString()); + } else { + data.put("FILE_ASSOCIATIONS", ""); + } + + // TODO - alternate template for JRE installer + String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ? + DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE; + + Writer w = new BufferedWriter(new FileWriter( + getConfig_ExeProjectFile(p))); + + String content = preprocessTextResource( + getConfig_ExeProjectFile(p).getName(), + getString("resource.inno-setup-project-file"), + iss, data, VERBOSE.fetchFrom(p), + RESOURCE_DIR.fetchFrom(p)); + w.write(content); + w.close(); + return true; + } + + private final static String removeQuotes(String s) { + if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) { + // special case for '"XXX"' return 'XXX' not '-XXX-' + // note '"' and '""' are excluded from this special case + s = s.substring(1, s.length() - 1); + } + // if there interior double quotes replace them with '-' + return s.replaceAll("\"", "-"); + } + + private final static String DEFAULT_INNO_SETUP_ICON = + "icon_inno_setup.bmp"; + + private boolean prepareProjectConfig(Map p) + throws IOException { + prepareMainProjectFile(p); + + // prepare installer icon + File iconTarget = getConfig_SmallInnoSetupIcon(p); + fetchResource(iconTarget.getName(), + getString("resource.setup-icon"), + DEFAULT_INNO_SETUP_ICON, + iconTarget, + VERBOSE.fetchFrom(p), + RESOURCE_DIR.fetchFrom(p)); + + fetchResource(getConfig_Script(p).getName(), + getString("resource.post-install-script"), + (String) null, + getConfig_Script(p), + VERBOSE.fetchFrom(p), + RESOURCE_DIR.fetchFrom(p)); + return true; + } + + private File getConfig_SmallInnoSetupIcon( + Map p) { + return new File(EXE_IMAGE_DIR.fetchFrom(p), + APP_NAME.fetchFrom(p) + "-setup-icon.bmp"); + } + + private File getConfig_ExeProjectFile(Map p) { + return new File(EXE_IMAGE_DIR.fetchFrom(p), + APP_NAME.fetchFrom(p) + ".iss"); + } + + + private File buildEXE(Map p, File outdir) + throws IOException { + Log.verbose(MessageFormat.format( + getString("message.outputting-to-location"), + outdir.getAbsolutePath())); + + outdir.mkdirs(); + + // run Inno Setup + ProcessBuilder pb = new ProcessBuilder( + TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p), + "/q", // turn off inno setup output + "/o"+outdir.getAbsolutePath(), + getConfig_ExeProjectFile(p).getAbsolutePath()); + pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p)); + IOUtils.exec(pb, VERBOSE.fetchFrom(p)); + + Log.verbose(MessageFormat.format( + getString("message.output-location"), + outdir.getAbsolutePath())); + + // presume the result is the ".exe" file with the newest modified time + // not the best solution, but it is the most reliable + File result = null; + long lastModified = 0; + File[] list = outdir.listFiles(); + if (list != null) { + for (File f : list) { + if (f.getName().endsWith(".exe") && + f.lastModified() > lastModified) { + result = f; + lastModified = f.lastModified(); + } + } + } + + return result; + } + + 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()) { + if (c < 0x10) { + w.write("\\'0"); + w.write(Integer.toHexString(c)); + } else if (c > 0xff) { + w.write("\\ud"); + w.write(Integer.toString(c)); + 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); + } + } + 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); + } + } + + private static String getString(String key) + throws MissingResourceException { + return I18N.getString(key); + } +}