1 /* 2 * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jpackage.internal; 27 28 import java.io.*; 29 import java.nio.charset.Charset; 30 import java.nio.file.Files; 31 import java.text.MessageFormat; 32 import java.util.*; 33 import java.util.regex.Matcher; 34 import java.util.regex.Pattern; 35 36 import static jdk.jpackage.internal.WindowsBundlerParam.*; 37 38 public class WinExeBundler extends AbstractBundler { 39 40 private static final ResourceBundle I18N = ResourceBundle.getBundle( 41 "jdk.jpackage.internal.resources.WinResources"); 42 43 public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = 44 new WindowsBundlerParam<>( 45 getString("param.exe-bundler.name"), 46 getString("param.exe-bundler.description"), 47 "win.app.bundler", 48 WinAppBundler.class, 49 params -> new WinAppBundler(), 50 null); 51 52 public static final BundlerParamInfo<File> EXE_IMAGE_DIR = 53 new WindowsBundlerParam<>( 54 getString("param.image-dir.name"), 55 getString("param.image-dir.description"), 56 "win.exe.imageDir", 57 File.class, 58 params -> { 59 File imagesRoot = IMAGES_ROOT.fetchFrom(params); 60 if (!imagesRoot.exists()) imagesRoot.mkdirs(); 61 return new File(imagesRoot, "win-exe.image"); 62 }, 63 (s, p) -> null); 64 65 public static final BundlerParamInfo<File> WIN_APP_IMAGE = 66 new WindowsBundlerParam<>( 67 getString("param.app-dir.name"), 68 getString("param.app-dir.description"), 69 "win.app.image", 70 File.class, 71 null, 72 (s, p) -> null); 73 74 public static final BundlerParamInfo<UUID> UPGRADE_UUID = 75 new WindowsBundlerParam<>( 76 I18N.getString("param.upgrade-uuid.name"), 77 I18N.getString("param.upgrade-uuid.description"), 78 Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(), 79 UUID.class, 80 params -> UUID.randomUUID(), 81 (s, p) -> UUID.fromString(s)); 82 83 public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE = 84 new StandardBundlerParam<>( 85 getString("param.system-wide.name"), 86 getString("param.system-wide.description"), 87 Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(), 88 Boolean.class, 89 params -> true, // default to system wide 90 (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null 91 : Boolean.valueOf(s) 92 ); 93 public static final StandardBundlerParam<String> PRODUCT_VERSION = 94 new StandardBundlerParam<>( 95 getString("param.product-version.name"), 96 getString("param.product-version.description"), 97 "win.msi.productVersion", 98 String.class, 99 VERSION::fetchFrom, 100 (s, p) -> s 101 ); 102 103 public static final StandardBundlerParam<Boolean> MENU_HINT = 104 new WindowsBundlerParam<>( 105 getString("param.menu-shortcut-hint.name"), 106 getString("param.menu-shortcut-hint.description"), 107 Arguments.CLIOptions.WIN_MENU_HINT.getId(), 108 Boolean.class, 109 params -> false, 110 (s, p) -> (s == null || 111 "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) 112 ); 113 114 public static final StandardBundlerParam<Boolean> SHORTCUT_HINT = 115 new WindowsBundlerParam<>( 116 getString("param.desktop-shortcut-hint.name"), 117 getString("param.desktop-shortcut-hint.description"), 118 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), 119 Boolean.class, 120 params -> false, 121 (s, p) -> (s == null || 122 "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) 123 ); 124 125 private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss"; 126 private final static String DEFAULT_JRE_EXE_TEMPLATE = "template.jre.iss"; 127 private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe"; 128 129 public static final BundlerParamInfo<String> 130 TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>( 131 getString("param.iscc-path.name"), 132 getString("param.iscc-path.description"), 133 "win.exe.iscc.exe", 134 String.class, 135 params -> { 136 for (String dirString : (System.getenv("PATH") 137 + ";C:\\Program Files (x86)\\Inno Setup 5;" 138 + "C:\\Program Files\\Inno Setup 5").split(";")) { 139 File f = new File(dirString.replace("\"", ""), 140 TOOL_INNO_SETUP_COMPILER); 141 if (f.isFile()) { 142 return f.toString(); 143 } 144 } 145 return null; 146 }, 147 null); 148 149 @Override 150 public String getName() { 151 return getString("exe.bundler.name"); 152 } 153 154 @Override 155 public String getDescription() { 156 return getString("exe.bundler.description"); 157 } 158 159 @Override 160 public String getID() { 161 return "exe"; 162 } 163 164 @Override 165 public String getBundleType() { 166 return "INSTALLER"; 167 } 168 169 @Override 170 public Collection<BundlerParamInfo<?>> getBundleParameters() { 171 Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>(); 172 results.addAll(WinAppBundler.getAppBundleParameters()); 173 results.addAll(getExeBundleParameters()); 174 return results; 175 } 176 177 public static Collection<BundlerParamInfo<?>> getExeBundleParameters() { 178 return Arrays.asList( 179 DESCRIPTION, 180 COPYRIGHT, 181 LICENSE_FILE, 182 MENU_GROUP, 183 MENU_HINT, 184 SHORTCUT_HINT, 185 EXE_SYSTEM_WIDE, 186 TITLE, 187 VENDOR, 188 INSTALLDIR_CHOOSER 189 ); 190 } 191 192 @Override 193 public File execute( 194 Map<String, ? super Object> p, File outputParentDir) { 195 return bundle(p, outputParentDir); 196 } 197 198 @Override 199 public boolean supported() { 200 return (Platform.getPlatform() == Platform.WINDOWS); 201 } 202 203 static class VersionExtractor extends PrintStream { 204 double version = 0f; 205 206 public VersionExtractor() { 207 super(new ByteArrayOutputStream()); 208 } 209 210 double getVersion() { 211 if (version == 0f) { 212 String content = 213 new String(((ByteArrayOutputStream) out).toByteArray()); 214 Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)"); 215 Matcher matcher = pattern.matcher(content); 216 if (matcher.find()) { 217 String v = matcher.group(1); 218 version = Double.parseDouble(v); 219 } 220 } 221 return version; 222 } 223 } 224 225 private static double findToolVersion(String toolName) { 226 try { 227 if (toolName == null || "".equals(toolName)) return 0f; 228 229 ProcessBuilder pb = new ProcessBuilder( 230 toolName, 231 "/?"); 232 VersionExtractor ve = new VersionExtractor(); 233 IOUtils.exec(pb, Log.isDebug(), true, ve); 234 // not interested in the output 235 double version = ve.getVersion(); 236 Log.verbose(MessageFormat.format( 237 getString("message.tool-version"), toolName, version)); 238 return version; 239 } catch (Exception e) { 240 if (Log.isDebug()) { 241 Log.verbose(e); 242 } 243 return 0f; 244 } 245 } 246 247 @Override 248 public boolean validate(Map<String, ? super Object> p) 249 throws UnsupportedPlatformException, ConfigException { 250 try { 251 if (p == null) throw new ConfigException( 252 getString("error.parameters-null"), 253 getString("error.parameters-null.advice")); 254 255 // run basic validation to ensure requirements are met 256 // we are not interested in return code, only possible exception 257 APP_BUNDLER.fetchFrom(p).validate(p); 258 259 // make sure some key values don't have newlines 260 for (BundlerParamInfo<String> pi : Arrays.asList( 261 APP_NAME, 262 COPYRIGHT, 263 DESCRIPTION, 264 MENU_GROUP, 265 TITLE, 266 VENDOR, 267 VERSION) 268 ) { 269 String v = pi.fetchFrom(p); 270 if (v.contains("\n") | v.contains("\r")) { 271 throw new ConfigException("Parmeter '" + pi.getID() + 272 "' cannot contain a newline.", 273 " Change the value of '" + pi.getID() + 274 " so that it does not contain any newlines"); 275 } 276 } 277 278 // exe bundlers trim the copyright to 100 characters, 279 // tell them this will happen 280 if (COPYRIGHT.fetchFrom(p).length() > 100) { 281 throw new ConfigException( 282 getString("error.copyright-is-too-long"), 283 getString("error.copyright-is-too-long.advice")); 284 } 285 286 double innoVersion = findToolVersion( 287 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p)); 288 289 //Inno Setup 5+ is required 290 double minVersion = 5.0f; 291 292 if (innoVersion < minVersion) { 293 Log.error(MessageFormat.format( 294 getString("message.tool-wrong-version"), 295 TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion)); 296 throw new ConfigException( 297 getString("error.iscc-not-found"), 298 getString("error.iscc-not-found.advice")); 299 } 300 301 /********* validate bundle parameters *************/ 302 303 // only one mime type per association, at least one file extension 304 List<Map<String, ? super Object>> associations = 305 FILE_ASSOCIATIONS.fetchFrom(p); 306 if (associations != null) { 307 for (int i = 0; i < associations.size(); i++) { 308 Map<String, ? super Object> assoc = associations.get(i); 309 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 310 if (mimes.size() > 1) { 311 throw new ConfigException(MessageFormat.format( 312 getString("error.too-many-content-" 313 + "types-for-file-association"), i), 314 getString("error.too-many-content-" 315 + "types-for-file-association.advice")); 316 } 317 } 318 } 319 320 return true; 321 } catch (RuntimeException re) { 322 if (re.getCause() instanceof ConfigException) { 323 throw (ConfigException) re.getCause(); 324 } else { 325 throw new ConfigException(re); 326 } 327 } 328 } 329 330 private boolean prepareProto(Map<String, ? super Object> p) 331 throws IOException { 332 File appImage = StandardBundlerParam.getPredefinedAppImage(p); 333 File appDir = null; 334 335 // we either have an application image or need to build one 336 if (appImage != null) { 337 appDir = new File( 338 EXE_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p)); 339 // copy everything from appImage dir into appDir/name 340 IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); 341 } else { 342 appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, 343 EXE_IMAGE_DIR.fetchFrom(p), true); 344 } 345 346 if (appDir == null) { 347 return false; 348 } 349 350 p.put(WIN_APP_IMAGE.getID(), appDir); 351 352 String licenseFile = LICENSE_FILE.fetchFrom(p); 353 if (licenseFile != null) { 354 // need to copy license file to the working directory and convert to rtf if needed 355 File lfile = new File(licenseFile); 356 File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); 357 IOUtils.copyFile(lfile, destFile); 358 ensureByMutationFileIsRTF(destFile); 359 } 360 361 // copy file association icons 362 List<Map<String, ? super Object>> fileAssociations = 363 FILE_ASSOCIATIONS.fetchFrom(p); 364 365 for (Map<String, ? super Object> fa : fileAssociations) { 366 File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO 367 if (icon == null) { 368 continue; 369 } 370 371 File faIconFile = new File(appDir, icon.getName()); 372 373 if (icon.exists()) { 374 try { 375 IOUtils.copyFile(icon, faIconFile); 376 } catch (IOException e) { 377 e.printStackTrace(); 378 } 379 } 380 } 381 382 return true; 383 } 384 385 public File bundle(Map<String, ? super Object> p, File outdir) { 386 if (!outdir.isDirectory() && !outdir.mkdirs()) { 387 throw new RuntimeException(MessageFormat.format( 388 getString("error.cannot-create-output-dir"), 389 outdir.getAbsolutePath())); 390 } 391 if (!outdir.canWrite()) { 392 throw new RuntimeException(MessageFormat.format( 393 getString("error.cannot-write-to-output-dir"), 394 outdir.getAbsolutePath())); 395 } 396 397 if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) { 398 Log.error(MessageFormat.format( 399 getString("message.potential.windows.defender.issue"), 400 WindowsDefender.getUserTempDirectory())); 401 } 402 403 // validate we have valid tools before continuing 404 String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p); 405 if (iscc == null || !new File(iscc).isFile()) { 406 Log.error(getString("error.iscc-not-found")); 407 Log.error(MessageFormat.format( 408 getString("message.iscc-file-string"), iscc)); 409 return null; 410 } 411 412 File imageDir = EXE_IMAGE_DIR.fetchFrom(p); 413 try { 414 imageDir.mkdirs(); 415 416 boolean menuShortcut = MENU_HINT.fetchFrom(p); 417 boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p); 418 if (!menuShortcut && !desktopShortcut) { 419 // both can not be false - user will not find the app 420 Log.verbose(getString("message.one-shortcut-required")); 421 p.put(MENU_HINT.getID(), true); 422 } 423 424 if (prepareProto(p) && prepareProjectConfig(p)) { 425 File configScript = getConfig_Script(p); 426 if (configScript.exists()) { 427 Log.verbose(MessageFormat.format( 428 getString("message.running-wsh-script"), 429 configScript.getAbsolutePath())); 430 IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p)); 431 } 432 return buildEXE(p, outdir); 433 } 434 return null; 435 } catch (IOException ex) { 436 ex.printStackTrace(); 437 return null; 438 } 439 } 440 441 // name of post-image script 442 private File getConfig_Script(Map<String, ? super Object> p) { 443 return new File(EXE_IMAGE_DIR.fetchFrom(p), 444 APP_NAME.fetchFrom(p) + "-post-image.wsf"); 445 } 446 447 private String getAppIdentifier(Map<String, ? super Object> p) { 448 String nm = UPGRADE_UUID.fetchFrom(p).toString(); 449 450 // limitation of innosetup 451 if (nm.length() > 126) { 452 Log.error(getString("message-truncating-id")); 453 nm = nm.substring(0, 126); 454 } 455 456 return nm; 457 } 458 459 private String getLicenseFile(Map<String, ? super Object> p) { 460 String licenseFile = LICENSE_FILE.fetchFrom(p); 461 if (licenseFile != null) { 462 File lfile = new File(licenseFile); 463 File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); 464 String filePath = destFile.getAbsolutePath(); 465 if (filePath.contains(" ")) { 466 return "\"" + filePath + "\""; 467 } else { 468 return filePath; 469 } 470 } 471 472 return null; 473 } 474 475 void validateValueAndPut(Map<String, String> data, String key, 476 BundlerParamInfo<String> param, 477 Map<String, ? super Object> p) throws IOException { 478 String value = param.fetchFrom(p); 479 if (value.contains("\r") || value.contains("\n")) { 480 throw new IOException("Configuration Parameter " + 481 param.getID() + " cannot contain multiple lines of text"); 482 } 483 data.put(key, innosetupEscape(value)); 484 } 485 486 private String innosetupEscape(String value) { 487 if (value == null) { 488 return ""; 489 } 490 491 if (value.contains("\"") || !value.trim().equals(value)) { 492 value = "\"" + value.replace("\"", "\"\"") + "\""; 493 } 494 return value; 495 } 496 497 boolean prepareMainProjectFile(Map<String, ? super Object> p) 498 throws IOException { 499 Map<String, String> data = new HashMap<>(); 500 data.put("PRODUCT_APP_IDENTIFIER", 501 innosetupEscape(getAppIdentifier(p))); 502 503 504 validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p); 505 validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p); 506 validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p); 507 validateValueAndPut(data, "INSTALLER_FILE_NAME", 508 INSTALLER_FILE_NAME, p); 509 510 data.put("LAUNCHER_NAME", 511 innosetupEscape(WinAppBundler.getAppName(p))); 512 513 data.put("APPLICATION_LAUNCHER_FILENAME", 514 innosetupEscape(WinAppBundler.getLauncherName(p))); 515 516 data.put("APPLICATION_DESKTOP_SHORTCUT", 517 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 518 data.put("APPLICATION_MENU_SHORTCUT", 519 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 520 validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p); 521 validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p); 522 validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p); 523 524 data.put("APPLICATION_LICENSE_FILE", 525 innosetupEscape(getLicenseFile(p))); 526 data.put("DISABLE_DIR_PAGE", 527 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes"); 528 529 Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p); 530 531 if (isSystemWide) { 532 data.put("APPLICATION_INSTALL_ROOT", "{pf}"); 533 data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); 534 } else { 535 data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); 536 data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); 537 } 538 539 if (BIT_ARCH_64.fetchFrom(p)) { 540 data.put("ARCHITECTURE_BIT_MODE", "x64"); 541 } else { 542 data.put("ARCHITECTURE_BIT_MODE", ""); 543 } 544 validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p); 545 546 validateValueAndPut(data, "APPLICATION_DESCRIPTION", 547 DESCRIPTION, p); 548 549 data.put("APPLICATION_SERVICE", "returnFalse"); 550 data.put("APPLICATION_NOT_SERVICE", "returnFalse"); 551 data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse"); 552 data.put("START_ON_INSTALL", ""); 553 data.put("STOP_ON_UNINSTALL", ""); 554 data.put("RUN_AT_STARTUP", ""); 555 556 String imagePathString = 557 WIN_APP_IMAGE.fetchFrom(p).toPath().toAbsolutePath().toString(); 558 data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString)); 559 Log.verbose("setting APPLICATION_IMAGE to " + 560 innosetupEscape(imagePathString) + " for InnoSetup"); 561 562 StringBuilder secondaryLaunchersCfg = new StringBuilder(); 563 for (Map<String, ? super Object> 564 launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) { 565 String application_name = APP_NAME.fetchFrom(launcher); 566 if (MENU_HINT.fetchFrom(launcher)) { 567 // Name: "{group}\APPLICATION_NAME"; 568 // Filename: "{app}\APPLICATION_NAME.exe"; 569 // IconFilename: "{app}\APPLICATION_NAME.ico" 570 secondaryLaunchersCfg.append("Name: \"{group}\\"); 571 secondaryLaunchersCfg.append(application_name); 572 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 573 secondaryLaunchersCfg.append(application_name); 574 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 575 secondaryLaunchersCfg.append(application_name); 576 secondaryLaunchersCfg.append(".ico\"\r\n"); 577 } 578 if (SHORTCUT_HINT.fetchFrom(launcher)) { 579 // Name: "{commondesktop}\APPLICATION_NAME"; 580 // Filename: "{app}\APPLICATION_NAME.exe"; 581 // IconFilename: "{app}\APPLICATION_NAME.ico" 582 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\"); 583 secondaryLaunchersCfg.append(application_name); 584 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 585 secondaryLaunchersCfg.append(application_name); 586 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 587 secondaryLaunchersCfg.append(application_name); 588 secondaryLaunchersCfg.append(".ico\"\r\n"); 589 } 590 } 591 data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString()); 592 593 StringBuilder registryEntries = new StringBuilder(); 594 String regName = APP_REGISTRY_NAME.fetchFrom(p); 595 List<Map<String, ? super Object>> fetchFrom = 596 FILE_ASSOCIATIONS.fetchFrom(p); 597 for (int i = 0; i < fetchFrom.size(); i++) { 598 Map<String, ? super Object> fileAssociation = fetchFrom.get(i); 599 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 600 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO 601 602 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 603 String entryName = regName + "File"; 604 if (i > 0) { 605 entryName += "." + i; 606 } 607 608 if (extensions == null) { 609 Log.verbose(getString( 610 "message.creating-association-with-null-extension")); 611 } else { 612 for (String ext : extensions) { 613 if (isSystemWide) { 614 // "Root: HKCR; Subkey: \".myp\"; 615 // ValueType: string; ValueName: \"\"; 616 // ValueData: \"MyProgramFile\"; 617 // Flags: uninsdeletevalue" 618 registryEntries.append("Root: HKCR; Subkey: \".") 619 .append(ext) 620 .append("\"; ValueType: string;" 621 + " ValueName: \"\"; ValueData: \"") 622 .append(entryName) 623 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 624 } else { 625 registryEntries.append( 626 "Root: HKCU; Subkey: \"Software\\Classes\\.") 627 .append(ext) 628 .append("\"; ValueType: string;" 629 + " ValueName: \"\"; ValueData: \"") 630 .append(entryName) 631 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 632 } 633 } 634 } 635 636 if (extensions != null && !extensions.isEmpty()) { 637 String ext = extensions.get(0); 638 List<String> mimeTypes = 639 FA_CONTENT_TYPE.fetchFrom(fileAssociation); 640 for (String mime : mimeTypes) { 641 if (isSystemWide) { 642 // "Root: HKCR; 643 // Subkey: HKCR\\Mime\\Database\\ 644 // Content Type\\application/chaos; 645 // ValueType: string; 646 // ValueName: Extension; 647 // ValueData: .chaos; 648 // Flags: uninsdeletevalue" 649 registryEntries.append("Root: HKCR; Subkey: " + 650 "\"Mime\\Database\\Content Type\\") 651 .append(mime) 652 .append("\"; ValueType: string; ValueName: " + 653 "\"Extension\"; ValueData: \".") 654 .append(ext) 655 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 656 } else { 657 registryEntries.append( 658 "Root: HKCU; Subkey: \"Software\\" + 659 "Classes\\Mime\\Database\\Content Type\\") 660 .append(mime) 661 .append("\"; ValueType: string; " + 662 "ValueName: \"Extension\"; ValueData: \".") 663 .append(ext) 664 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 665 } 666 } 667 } 668 669 if (isSystemWide) { 670 // "Root: HKCR; 671 // Subkey: \"MyProgramFile\"; 672 // ValueType: string; 673 // ValueName: \"\"; 674 // ValueData: \"My Program File\"; 675 // Flags: uninsdeletekey" 676 registryEntries.append("Root: HKCR; Subkey: \"") 677 .append(entryName) 678 .append( 679 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 680 .append(removeQuotes(description)) 681 .append("\"; Flags: uninsdeletekey\r\n"); 682 } else { 683 registryEntries.append( 684 "Root: HKCU; Subkey: \"Software\\Classes\\") 685 .append(entryName) 686 .append( 687 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 688 .append(removeQuotes(description)) 689 .append("\"; Flags: uninsdeletekey\r\n"); 690 } 691 692 if (icon != null && icon.exists()) { 693 if (isSystemWide) { 694 // "Root: HKCR; 695 // Subkey: \"MyProgramFile\\DefaultIcon\"; 696 // ValueType: string; 697 // ValueName: \"\"; 698 // ValueData: \"{app}\\MYPROG.EXE,0\"\n" + 699 registryEntries.append("Root: HKCR; Subkey: \"") 700 .append(entryName) 701 .append("\\DefaultIcon\"; ValueType: string; " + 702 "ValueName: \"\"; ValueData: \"{app}\\") 703 .append(icon.getName()) 704 .append("\"\r\n"); 705 } else { 706 registryEntries.append( 707 "Root: HKCU; Subkey: \"Software\\Classes\\") 708 .append(entryName) 709 .append("\\DefaultIcon\"; ValueType: string; " + 710 "ValueName: \"\"; ValueData: \"{app}\\") 711 .append(icon.getName()) 712 .append("\"\r\n"); 713 } 714 } 715 716 if (isSystemWide) { 717 // "Root: HKCR; 718 // Subkey: \"MyProgramFile\\shell\\open\\command\"; 719 // ValueType: string; 720 // ValueName: \"\"; 721 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n" 722 registryEntries.append("Root: HKCR; Subkey: \"") 723 .append(entryName) 724 .append("\\shell\\open\\command\"; ValueType: " + 725 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 726 .append(APP_NAME.fetchFrom(p)) 727 .append("\"\" \"\"%1\"\"\"\r\n"); 728 } else { 729 registryEntries.append( 730 "Root: HKCU; Subkey: \"Software\\Classes\\") 731 .append(entryName) 732 .append("\\shell\\open\\command\"; ValueType: " + 733 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 734 .append(APP_NAME.fetchFrom(p)) 735 .append("\"\" \"\"%1\"\"\"\r\n"); 736 } 737 } 738 if (registryEntries.length() > 0) { 739 data.put("FILE_ASSOCIATIONS", 740 "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + 741 registryEntries.toString()); 742 } else { 743 data.put("FILE_ASSOCIATIONS", ""); 744 } 745 746 // TODO - alternate template for JRE installer 747 String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ? 748 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE; 749 750 Writer w = new BufferedWriter(new FileWriter( 751 getConfig_ExeProjectFile(p))); 752 753 String content = preprocessTextResource( 754 getConfig_ExeProjectFile(p).getName(), 755 getString("resource.inno-setup-project-file"), 756 iss, data, VERBOSE.fetchFrom(p), 757 RESOURCE_DIR.fetchFrom(p)); 758 w.write(content); 759 w.close(); 760 return true; 761 } 762 763 private final static String removeQuotes(String s) { 764 if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) { 765 // special case for '"XXX"' return 'XXX' not '-XXX-' 766 // note '"' and '""' are excluded from this special case 767 s = s.substring(1, s.length() - 1); 768 } 769 // if there interior double quotes replace them with '-' 770 return s.replaceAll("\"", "-"); 771 } 772 773 private final static String DEFAULT_INNO_SETUP_ICON = 774 "icon_inno_setup.bmp"; 775 776 private boolean prepareProjectConfig(Map<String, ? super Object> p) 777 throws IOException { 778 prepareMainProjectFile(p); 779 780 // prepare installer icon 781 File iconTarget = getConfig_SmallInnoSetupIcon(p); 782 fetchResource(iconTarget.getName(), 783 getString("resource.setup-icon"), 784 DEFAULT_INNO_SETUP_ICON, 785 iconTarget, 786 VERBOSE.fetchFrom(p), 787 RESOURCE_DIR.fetchFrom(p)); 788 789 fetchResource(getConfig_Script(p).getName(), 790 getString("resource.post-install-script"), 791 (String) null, 792 getConfig_Script(p), 793 VERBOSE.fetchFrom(p), 794 RESOURCE_DIR.fetchFrom(p)); 795 return true; 796 } 797 798 private File getConfig_SmallInnoSetupIcon( 799 Map<String, ? super Object> p) { 800 return new File(EXE_IMAGE_DIR.fetchFrom(p), 801 APP_NAME.fetchFrom(p) + "-setup-icon.bmp"); 802 } 803 804 private File getConfig_ExeProjectFile(Map<String, ? super Object> p) { 805 return new File(EXE_IMAGE_DIR.fetchFrom(p), 806 APP_NAME.fetchFrom(p) + ".iss"); 807 } 808 809 810 private File buildEXE(Map<String, ? super Object> p, File outdir) 811 throws IOException { 812 Log.verbose(MessageFormat.format( 813 getString("message.outputting-to-location"), 814 outdir.getAbsolutePath())); 815 816 outdir.mkdirs(); 817 818 // run Inno Setup 819 ProcessBuilder pb = new ProcessBuilder( 820 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p), 821 "/q", // turn off inno setup output 822 "/o"+outdir.getAbsolutePath(), 823 getConfig_ExeProjectFile(p).getAbsolutePath()); 824 pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p)); 825 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 826 827 Log.verbose(MessageFormat.format( 828 getString("message.output-location"), 829 outdir.getAbsolutePath())); 830 831 // presume the result is the ".exe" file with the newest modified time 832 // not the best solution, but it is the most reliable 833 File result = null; 834 long lastModified = 0; 835 File[] list = outdir.listFiles(); 836 if (list != null) { 837 for (File f : list) { 838 if (f.getName().endsWith(".exe") && 839 f.lastModified() > lastModified) { 840 result = f; 841 lastModified = f.lastModified(); 842 } 843 } 844 } 845 846 return result; 847 } 848 849 public static void ensureByMutationFileIsRTF(File f) { 850 if (f == null || !f.isFile()) return; 851 852 try { 853 boolean existingLicenseIsRTF = false; 854 855 try (FileInputStream fin = new FileInputStream(f)) { 856 byte[] firstBits = new byte[7]; 857 858 if (fin.read(firstBits) == firstBits.length) { 859 String header = new String(firstBits); 860 existingLicenseIsRTF = "{\\rtf1\\".equals(header); 861 } 862 } 863 864 if (!existingLicenseIsRTF) { 865 List<String> oldLicense = Files.readAllLines(f.toPath()); 866 try (Writer w = Files.newBufferedWriter( 867 f.toPath(), Charset.forName("Windows-1252"))) { 868 w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" 869 + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" 870 + "\\viewkind4\\uc1\\pard\\sa200\\sl276" 871 + "\\slmult1\\lang9\\fs20 "); 872 oldLicense.forEach(l -> { 873 try { 874 for (char c : l.toCharArray()) { 875 if (c < 0x10) { 876 w.write("\\'0"); 877 w.write(Integer.toHexString(c)); 878 } else if (c > 0xff) { 879 w.write("\\ud"); 880 w.write(Integer.toString(c)); 881 w.write("?"); 882 } else if ((c < 0x20) || (c >= 0x80) || 883 (c == 0x5C) || (c == 0x7B) || 884 (c == 0x7D)) { 885 w.write("\\'"); 886 w.write(Integer.toHexString(c)); 887 } else { 888 w.write(c); 889 } 890 } 891 if (l.length() < 1) { 892 w.write("\\par"); 893 } else { 894 w.write(" "); 895 } 896 w.write("\r\n"); 897 } catch (IOException e) { 898 Log.verbose(e); 899 } 900 }); 901 w.write("}\r\n"); 902 } 903 } 904 } catch (IOException e) { 905 Log.verbose(e); 906 } 907 } 908 909 private static String getString(String key) 910 throws MissingResourceException { 911 return I18N.getString(key); 912 } 913 }