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 String tempDirectory = WindowsDefender.getUserTempDirectory(); 398 if (Arguments.CLIOptions.context().userProvidedBuildRoot) { 399 tempDirectory = BUILD_ROOT.fetchFrom(p).getAbsolutePath(); 400 } 401 if (WindowsDefender.isThereAPotentialWindowsDefenderIssue( 402 tempDirectory)) { 403 Log.error(MessageFormat.format( 404 getString("message.potential.windows.defender.issue"), 405 tempDirectory)); 406 } 407 408 // validate we have valid tools before continuing 409 String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p); 410 if (iscc == null || !new File(iscc).isFile()) { 411 Log.error(getString("error.iscc-not-found")); 412 Log.error(MessageFormat.format( 413 getString("message.iscc-file-string"), iscc)); 414 return null; 415 } 416 417 File imageDir = EXE_IMAGE_DIR.fetchFrom(p); 418 try { 419 imageDir.mkdirs(); 420 421 boolean menuShortcut = MENU_HINT.fetchFrom(p); 422 boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p); 423 if (!menuShortcut && !desktopShortcut) { 424 // both can not be false - user will not find the app 425 Log.verbose(getString("message.one-shortcut-required")); 426 p.put(MENU_HINT.getID(), true); 427 } 428 429 if (prepareProto(p) && prepareProjectConfig(p)) { 430 File configScript = getConfig_Script(p); 431 if (configScript.exists()) { 432 Log.verbose(MessageFormat.format( 433 getString("message.running-wsh-script"), 434 configScript.getAbsolutePath())); 435 IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p)); 436 } 437 return buildEXE(p, outdir); 438 } 439 return null; 440 } catch (IOException ex) { 441 ex.printStackTrace(); 442 return null; 443 } 444 } 445 446 // name of post-image script 447 private File getConfig_Script(Map<String, ? super Object> p) { 448 return new File(EXE_IMAGE_DIR.fetchFrom(p), 449 APP_NAME.fetchFrom(p) + "-post-image.wsf"); 450 } 451 452 private String getAppIdentifier(Map<String, ? super Object> p) { 453 String nm = UPGRADE_UUID.fetchFrom(p).toString(); 454 455 // limitation of innosetup 456 if (nm.length() > 126) { 457 Log.error(getString("message-truncating-id")); 458 nm = nm.substring(0, 126); 459 } 460 461 return nm; 462 } 463 464 private String getLicenseFile(Map<String, ? super Object> p) { 465 String licenseFile = LICENSE_FILE.fetchFrom(p); 466 if (licenseFile != null) { 467 File lfile = new File(licenseFile); 468 File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); 469 String filePath = destFile.getAbsolutePath(); 470 if (filePath.contains(" ")) { 471 return "\"" + filePath + "\""; 472 } else { 473 return filePath; 474 } 475 } 476 477 return null; 478 } 479 480 void validateValueAndPut(Map<String, String> data, String key, 481 BundlerParamInfo<String> param, 482 Map<String, ? super Object> p) throws IOException { 483 String value = param.fetchFrom(p); 484 if (value.contains("\r") || value.contains("\n")) { 485 throw new IOException("Configuration Parameter " + 486 param.getID() + " cannot contain multiple lines of text"); 487 } 488 data.put(key, innosetupEscape(value)); 489 } 490 491 private String innosetupEscape(String value) { 492 if (value == null) { 493 return ""; 494 } 495 496 if (value.contains("\"") || !value.trim().equals(value)) { 497 value = "\"" + value.replace("\"", "\"\"") + "\""; 498 } 499 return value; 500 } 501 502 boolean prepareMainProjectFile(Map<String, ? super Object> p) 503 throws IOException { 504 Map<String, String> data = new HashMap<>(); 505 data.put("PRODUCT_APP_IDENTIFIER", 506 innosetupEscape(getAppIdentifier(p))); 507 508 509 validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p); 510 validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p); 511 validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p); 512 validateValueAndPut(data, "INSTALLER_FILE_NAME", 513 INSTALLER_FILE_NAME, p); 514 515 data.put("LAUNCHER_NAME", 516 innosetupEscape(WinAppBundler.getAppName(p))); 517 518 data.put("APPLICATION_LAUNCHER_FILENAME", 519 innosetupEscape(WinAppBundler.getLauncherName(p))); 520 521 data.put("APPLICATION_DESKTOP_SHORTCUT", 522 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 523 data.put("APPLICATION_MENU_SHORTCUT", 524 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 525 validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p); 526 validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p); 527 validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p); 528 529 data.put("APPLICATION_LICENSE_FILE", 530 innosetupEscape(getLicenseFile(p))); 531 data.put("DISABLE_DIR_PAGE", 532 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes"); 533 534 Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p); 535 536 if (isSystemWide) { 537 data.put("APPLICATION_INSTALL_ROOT", "{pf}"); 538 data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); 539 } else { 540 data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); 541 data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); 542 } 543 544 if (BIT_ARCH_64.fetchFrom(p)) { 545 data.put("ARCHITECTURE_BIT_MODE", "x64"); 546 } else { 547 data.put("ARCHITECTURE_BIT_MODE", ""); 548 } 549 validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p); 550 551 validateValueAndPut(data, "APPLICATION_DESCRIPTION", 552 DESCRIPTION, p); 553 554 data.put("APPLICATION_SERVICE", "returnFalse"); 555 data.put("APPLICATION_NOT_SERVICE", "returnFalse"); 556 data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse"); 557 data.put("START_ON_INSTALL", ""); 558 data.put("STOP_ON_UNINSTALL", ""); 559 data.put("RUN_AT_STARTUP", ""); 560 561 String imagePathString = 562 WIN_APP_IMAGE.fetchFrom(p).toPath().toAbsolutePath().toString(); 563 data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString)); 564 Log.verbose("setting APPLICATION_IMAGE to " + 565 innosetupEscape(imagePathString) + " for InnoSetup"); 566 567 StringBuilder secondaryLaunchersCfg = new StringBuilder(); 568 for (Map<String, ? super Object> 569 launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) { 570 String application_name = APP_NAME.fetchFrom(launcher); 571 if (MENU_HINT.fetchFrom(launcher)) { 572 // Name: "{group}\APPLICATION_NAME"; 573 // Filename: "{app}\APPLICATION_NAME.exe"; 574 // IconFilename: "{app}\APPLICATION_NAME.ico" 575 secondaryLaunchersCfg.append("Name: \"{group}\\"); 576 secondaryLaunchersCfg.append(application_name); 577 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 578 secondaryLaunchersCfg.append(application_name); 579 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 580 secondaryLaunchersCfg.append(application_name); 581 secondaryLaunchersCfg.append(".ico\"\r\n"); 582 } 583 if (SHORTCUT_HINT.fetchFrom(launcher)) { 584 // Name: "{commondesktop}\APPLICATION_NAME"; 585 // Filename: "{app}\APPLICATION_NAME.exe"; 586 // IconFilename: "{app}\APPLICATION_NAME.ico" 587 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\"); 588 secondaryLaunchersCfg.append(application_name); 589 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 590 secondaryLaunchersCfg.append(application_name); 591 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 592 secondaryLaunchersCfg.append(application_name); 593 secondaryLaunchersCfg.append(".ico\"\r\n"); 594 } 595 } 596 data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString()); 597 598 StringBuilder registryEntries = new StringBuilder(); 599 String regName = APP_REGISTRY_NAME.fetchFrom(p); 600 List<Map<String, ? super Object>> fetchFrom = 601 FILE_ASSOCIATIONS.fetchFrom(p); 602 for (int i = 0; i < fetchFrom.size(); i++) { 603 Map<String, ? super Object> fileAssociation = fetchFrom.get(i); 604 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 605 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO 606 607 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 608 String entryName = regName + "File"; 609 if (i > 0) { 610 entryName += "." + i; 611 } 612 613 if (extensions == null) { 614 Log.verbose(getString( 615 "message.creating-association-with-null-extension")); 616 } else { 617 for (String ext : extensions) { 618 if (isSystemWide) { 619 // "Root: HKCR; Subkey: \".myp\"; 620 // ValueType: string; ValueName: \"\"; 621 // ValueData: \"MyProgramFile\"; 622 // Flags: uninsdeletevalue" 623 registryEntries.append("Root: HKCR; Subkey: \".") 624 .append(ext) 625 .append("\"; ValueType: string;" 626 + " ValueName: \"\"; ValueData: \"") 627 .append(entryName) 628 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 629 } else { 630 registryEntries.append( 631 "Root: HKCU; Subkey: \"Software\\Classes\\.") 632 .append(ext) 633 .append("\"; ValueType: string;" 634 + " ValueName: \"\"; ValueData: \"") 635 .append(entryName) 636 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 637 } 638 } 639 } 640 641 if (extensions != null && !extensions.isEmpty()) { 642 String ext = extensions.get(0); 643 List<String> mimeTypes = 644 FA_CONTENT_TYPE.fetchFrom(fileAssociation); 645 for (String mime : mimeTypes) { 646 if (isSystemWide) { 647 // "Root: HKCR; 648 // Subkey: HKCR\\Mime\\Database\\ 649 // Content Type\\application/chaos; 650 // ValueType: string; 651 // ValueName: Extension; 652 // ValueData: .chaos; 653 // Flags: uninsdeletevalue" 654 registryEntries.append("Root: HKCR; Subkey: " + 655 "\"Mime\\Database\\Content Type\\") 656 .append(mime) 657 .append("\"; ValueType: string; ValueName: " + 658 "\"Extension\"; ValueData: \".") 659 .append(ext) 660 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 661 } else { 662 registryEntries.append( 663 "Root: HKCU; Subkey: \"Software\\" + 664 "Classes\\Mime\\Database\\Content Type\\") 665 .append(mime) 666 .append("\"; ValueType: string; " + 667 "ValueName: \"Extension\"; ValueData: \".") 668 .append(ext) 669 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 670 } 671 } 672 } 673 674 if (isSystemWide) { 675 // "Root: HKCR; 676 // Subkey: \"MyProgramFile\"; 677 // ValueType: string; 678 // ValueName: \"\"; 679 // ValueData: \"My Program File\"; 680 // Flags: uninsdeletekey" 681 registryEntries.append("Root: HKCR; Subkey: \"") 682 .append(entryName) 683 .append( 684 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 685 .append(removeQuotes(description)) 686 .append("\"; Flags: uninsdeletekey\r\n"); 687 } else { 688 registryEntries.append( 689 "Root: HKCU; Subkey: \"Software\\Classes\\") 690 .append(entryName) 691 .append( 692 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 693 .append(removeQuotes(description)) 694 .append("\"; Flags: uninsdeletekey\r\n"); 695 } 696 697 if (icon != null && icon.exists()) { 698 if (isSystemWide) { 699 // "Root: HKCR; 700 // Subkey: \"MyProgramFile\\DefaultIcon\"; 701 // ValueType: string; 702 // ValueName: \"\"; 703 // ValueData: \"{app}\\MYPROG.EXE,0\"\n" + 704 registryEntries.append("Root: HKCR; Subkey: \"") 705 .append(entryName) 706 .append("\\DefaultIcon\"; ValueType: string; " + 707 "ValueName: \"\"; ValueData: \"{app}\\") 708 .append(icon.getName()) 709 .append("\"\r\n"); 710 } else { 711 registryEntries.append( 712 "Root: HKCU; Subkey: \"Software\\Classes\\") 713 .append(entryName) 714 .append("\\DefaultIcon\"; ValueType: string; " + 715 "ValueName: \"\"; ValueData: \"{app}\\") 716 .append(icon.getName()) 717 .append("\"\r\n"); 718 } 719 } 720 721 if (isSystemWide) { 722 // "Root: HKCR; 723 // Subkey: \"MyProgramFile\\shell\\open\\command\"; 724 // ValueType: string; 725 // ValueName: \"\"; 726 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n" 727 registryEntries.append("Root: HKCR; Subkey: \"") 728 .append(entryName) 729 .append("\\shell\\open\\command\"; ValueType: " + 730 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 731 .append(APP_NAME.fetchFrom(p)) 732 .append("\"\" \"\"%1\"\"\"\r\n"); 733 } else { 734 registryEntries.append( 735 "Root: HKCU; Subkey: \"Software\\Classes\\") 736 .append(entryName) 737 .append("\\shell\\open\\command\"; ValueType: " + 738 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 739 .append(APP_NAME.fetchFrom(p)) 740 .append("\"\" \"\"%1\"\"\"\r\n"); 741 } 742 } 743 if (registryEntries.length() > 0) { 744 data.put("FILE_ASSOCIATIONS", 745 "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + 746 registryEntries.toString()); 747 } else { 748 data.put("FILE_ASSOCIATIONS", ""); 749 } 750 751 // TODO - alternate template for JRE installer 752 String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ? 753 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE; 754 755 Writer w = new BufferedWriter(new FileWriter( 756 getConfig_ExeProjectFile(p))); 757 758 String content = preprocessTextResource( 759 getConfig_ExeProjectFile(p).getName(), 760 getString("resource.inno-setup-project-file"), 761 iss, data, VERBOSE.fetchFrom(p), 762 RESOURCE_DIR.fetchFrom(p)); 763 w.write(content); 764 w.close(); 765 return true; 766 } 767 768 private final static String removeQuotes(String s) { 769 if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) { 770 // special case for '"XXX"' return 'XXX' not '-XXX-' 771 // note '"' and '""' are excluded from this special case 772 s = s.substring(1, s.length() - 1); 773 } 774 // if there interior double quotes replace them with '-' 775 return s.replaceAll("\"", "-"); 776 } 777 778 private final static String DEFAULT_INNO_SETUP_ICON = 779 "icon_inno_setup.bmp"; 780 781 private boolean prepareProjectConfig(Map<String, ? super Object> p) 782 throws IOException { 783 prepareMainProjectFile(p); 784 785 // prepare installer icon 786 File iconTarget = getConfig_SmallInnoSetupIcon(p); 787 fetchResource(iconTarget.getName(), 788 getString("resource.setup-icon"), 789 DEFAULT_INNO_SETUP_ICON, 790 iconTarget, 791 VERBOSE.fetchFrom(p), 792 RESOURCE_DIR.fetchFrom(p)); 793 794 fetchResource(getConfig_Script(p).getName(), 795 getString("resource.post-install-script"), 796 (String) null, 797 getConfig_Script(p), 798 VERBOSE.fetchFrom(p), 799 RESOURCE_DIR.fetchFrom(p)); 800 return true; 801 } 802 803 private File getConfig_SmallInnoSetupIcon( 804 Map<String, ? super Object> p) { 805 return new File(EXE_IMAGE_DIR.fetchFrom(p), 806 APP_NAME.fetchFrom(p) + "-setup-icon.bmp"); 807 } 808 809 private File getConfig_ExeProjectFile(Map<String, ? super Object> p) { 810 return new File(EXE_IMAGE_DIR.fetchFrom(p), 811 APP_NAME.fetchFrom(p) + ".iss"); 812 } 813 814 815 private File buildEXE(Map<String, ? super Object> p, File outdir) 816 throws IOException { 817 Log.verbose(MessageFormat.format( 818 getString("message.outputting-to-location"), 819 outdir.getAbsolutePath())); 820 821 outdir.mkdirs(); 822 823 // run Inno Setup 824 ProcessBuilder pb = new ProcessBuilder( 825 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p), 826 "/q", // turn off inno setup output 827 "/o"+outdir.getAbsolutePath(), 828 getConfig_ExeProjectFile(p).getAbsolutePath()); 829 pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p)); 830 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 831 832 Log.verbose(MessageFormat.format( 833 getString("message.output-location"), 834 outdir.getAbsolutePath())); 835 836 // presume the result is the ".exe" file with the newest modified time 837 // not the best solution, but it is the most reliable 838 File result = null; 839 long lastModified = 0; 840 File[] list = outdir.listFiles(); 841 if (list != null) { 842 for (File f : list) { 843 if (f.getName().endsWith(".exe") && 844 f.lastModified() > lastModified) { 845 result = f; 846 lastModified = f.lastModified(); 847 } 848 } 849 } 850 851 return result; 852 } 853 854 public static void ensureByMutationFileIsRTF(File f) { 855 if (f == null || !f.isFile()) return; 856 857 try { 858 boolean existingLicenseIsRTF = false; 859 860 try (FileInputStream fin = new FileInputStream(f)) { 861 byte[] firstBits = new byte[7]; 862 863 if (fin.read(firstBits) == firstBits.length) { 864 String header = new String(firstBits); 865 existingLicenseIsRTF = "{\\rtf1\\".equals(header); 866 } 867 } 868 869 if (!existingLicenseIsRTF) { 870 List<String> oldLicense = Files.readAllLines(f.toPath()); 871 try (Writer w = Files.newBufferedWriter( 872 f.toPath(), Charset.forName("Windows-1252"))) { 873 w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" 874 + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" 875 + "\\viewkind4\\uc1\\pard\\sa200\\sl276" 876 + "\\slmult1\\lang9\\fs20 "); 877 oldLicense.forEach(l -> { 878 try { 879 for (char c : l.toCharArray()) { 880 if (c < 0x10) { 881 w.write("\\'0"); 882 w.write(Integer.toHexString(c)); 883 } else if (c > 0xff) { 884 w.write("\\ud"); 885 w.write(Integer.toString(c)); 886 w.write("?"); 887 } else if ((c < 0x20) || (c >= 0x80) || 888 (c == 0x5C) || (c == 0x7B) || 889 (c == 0x7D)) { 890 w.write("\\'"); 891 w.write(Integer.toHexString(c)); 892 } else { 893 w.write(c); 894 } 895 } 896 if (l.length() < 1) { 897 w.write("\\par"); 898 } else { 899 w.write(" "); 900 } 901 w.write("\r\n"); 902 } catch (IOException e) { 903 Log.verbose(e); 904 } 905 }); 906 w.write("}\r\n"); 907 } 908 } 909 } catch (IOException e) { 910 Log.verbose(e); 911 } 912 } 913 914 private static String getString(String key) 915 throws MissingResourceException { 916 return I18N.getString(key); 917 } 918 }