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(Map<String, ? super Object> p, 194 File outputParentDir) throws PackagerException { 195 return bundle(p, outputParentDir); 196 } 197 198 @Override 199 public boolean supported(boolean platformInstaller) { 200 return (Platform.getPlatform() == Platform.WINDOWS); 201 } 202 203 private static String findToolVersion(String toolName) { 204 try { 205 if (toolName == null || "".equals(toolName)) return null; 206 207 ProcessBuilder pb = new ProcessBuilder( 208 toolName, 209 "/?"); 210 VersionExtractor ve = 211 new VersionExtractor("Inno Setup (\\d+.?\\d*)"); 212 IOUtils.exec(pb, Log.isDebug(), true, ve); 213 // not interested in the output 214 String version = ve.getVersion(); 215 Log.verbose(MessageFormat.format( 216 getString("message.tool-version"), toolName, version)); 217 return version; 218 } catch (Exception e) { 219 if (Log.isDebug()) { 220 Log.verbose(e); 221 } 222 return null; 223 } 224 } 225 226 @Override 227 public boolean validate(Map<String, ? super Object> p) 228 throws UnsupportedPlatformException, ConfigException { 229 try { 230 if (p == null) throw new ConfigException( 231 getString("error.parameters-null"), 232 getString("error.parameters-null.advice")); 233 234 // run basic validation to ensure requirements are met 235 // we are not interested in return code, only possible exception 236 APP_BUNDLER.fetchFrom(p).validate(p); 237 238 // make sure some key values don't have newlines 239 for (BundlerParamInfo<String> pi : Arrays.asList( 240 APP_NAME, 241 COPYRIGHT, 242 DESCRIPTION, 243 MENU_GROUP, 244 TITLE, 245 VENDOR, 246 VERSION) 247 ) { 248 String v = pi.fetchFrom(p); 249 if (v.contains("\n") | v.contains("\r")) { 250 throw new ConfigException("Parmeter '" + pi.getID() + 251 "' cannot contain a newline.", 252 " Change the value of '" + pi.getID() + 253 " so that it does not contain any newlines"); 254 } 255 } 256 257 // exe bundlers trim the copyright to 100 characters, 258 // tell them this will happen 259 if (COPYRIGHT.fetchFrom(p).length() > 100) { 260 throw new ConfigException( 261 getString("error.copyright-is-too-long"), 262 getString("error.copyright-is-too-long.advice")); 263 } 264 265 String innoVersion = findToolVersion( 266 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p)); 267 268 //Inno Setup 5+ is required 269 String minVersion = "5.0"; 270 271 if (VersionExtractor.isLessThan(innoVersion, minVersion)) { 272 Log.error(MessageFormat.format( 273 getString("message.tool-wrong-version"), 274 TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion)); 275 throw new ConfigException( 276 getString("error.iscc-not-found"), 277 getString("error.iscc-not-found.advice")); 278 } 279 280 /********* validate bundle parameters *************/ 281 282 // only one mime type per association, at least one file extension 283 List<Map<String, ? super Object>> associations = 284 FILE_ASSOCIATIONS.fetchFrom(p); 285 if (associations != null) { 286 for (int i = 0; i < associations.size(); i++) { 287 Map<String, ? super Object> assoc = associations.get(i); 288 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 289 if (mimes.size() > 1) { 290 throw new ConfigException(MessageFormat.format( 291 getString("error.too-many-content-" 292 + "types-for-file-association"), i), 293 getString("error.too-many-content-" 294 + "types-for-file-association.advice")); 295 } 296 } 297 } 298 299 return true; 300 } catch (RuntimeException re) { 301 if (re.getCause() instanceof ConfigException) { 302 throw (ConfigException) re.getCause(); 303 } else { 304 throw new ConfigException(re); 305 } 306 } 307 } 308 309 private boolean prepareProto(Map<String, ? super Object> p) 310 throws PackagerException, IOException { 311 File appImage = StandardBundlerParam.getPredefinedAppImage(p); 312 File appDir = null; 313 314 // we either have an application image or need to build one 315 if (appImage != null) { 316 appDir = new File( 317 EXE_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p)); 318 // copy everything from appImage dir into appDir/name 319 IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); 320 } else { 321 appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, 322 EXE_IMAGE_DIR.fetchFrom(p), true); 323 } 324 325 if (appDir == null) { 326 return false; 327 } 328 329 p.put(WIN_APP_IMAGE.getID(), appDir); 330 331 String licenseFile = LICENSE_FILE.fetchFrom(p); 332 if (licenseFile != null) { 333 // need to copy license file to the working directory and convert to rtf if needed 334 File lfile = new File(licenseFile); 335 File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); 336 IOUtils.copyFile(lfile, destFile); 337 ensureByMutationFileIsRTF(destFile); 338 } 339 340 // copy file association icons 341 List<Map<String, ? super Object>> fileAssociations = 342 FILE_ASSOCIATIONS.fetchFrom(p); 343 344 for (Map<String, ? super Object> fa : fileAssociations) { 345 File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO 346 if (icon == null) { 347 continue; 348 } 349 350 File faIconFile = new File(appDir, icon.getName()); 351 352 if (icon.exists()) { 353 try { 354 IOUtils.copyFile(icon, faIconFile); 355 } catch (IOException e) { 356 Log.verbose(e); 357 } 358 } 359 } 360 361 return true; 362 } 363 364 public File bundle(Map<String, ? super Object> p, File outdir) 365 throws PackagerException { 366 if (!outdir.isDirectory() && !outdir.mkdirs()) { 367 throw new PackagerException("error.cannot-create-output-dir", 368 outdir.getAbsolutePath()); 369 } 370 if (!outdir.canWrite()) { 371 throw new PackagerException("error.cannot-write-to-output-dir", 372 outdir.getAbsolutePath()); 373 } 374 375 String tempDirectory = WindowsDefender.getUserTempDirectory(); 376 if (Arguments.CLIOptions.context().userProvidedBuildRoot) { 377 tempDirectory = BUILD_ROOT.fetchFrom(p).getAbsolutePath(); 378 } 379 if (WindowsDefender.isThereAPotentialWindowsDefenderIssue( 380 tempDirectory)) { 381 Log.error(MessageFormat.format( 382 getString("message.potential.windows.defender.issue"), 383 tempDirectory)); 384 } 385 386 // validate we have valid tools before continuing 387 String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p); 388 if (iscc == null || !new File(iscc).isFile()) { 389 Log.verbose(getString("error.iscc-not-found")); 390 Log.verbose(MessageFormat.format( 391 getString("message.iscc-file-string"), iscc)); 392 throw new PackagerException("error.iscc-not-found"); 393 } 394 395 File imageDir = EXE_IMAGE_DIR.fetchFrom(p); 396 try { 397 imageDir.mkdirs(); 398 399 boolean menuShortcut = MENU_HINT.fetchFrom(p); 400 boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p); 401 if (!menuShortcut && !desktopShortcut) { 402 // both can not be false - user will not find the app 403 Log.verbose(getString("message.one-shortcut-required")); 404 p.put(MENU_HINT.getID(), true); 405 } 406 407 if (prepareProto(p) && prepareProjectConfig(p)) { 408 File configScript = getConfig_Script(p); 409 if (configScript.exists()) { 410 Log.verbose(MessageFormat.format( 411 getString("message.running-wsh-script"), 412 configScript.getAbsolutePath())); 413 IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p)); 414 } 415 return buildEXE(p, outdir); 416 } 417 return null; 418 } catch (IOException ex) { 419 Log.verbose(ex); 420 throw new PackagerException(ex); 421 } 422 } 423 424 // name of post-image script 425 private File getConfig_Script(Map<String, ? super Object> p) { 426 return new File(EXE_IMAGE_DIR.fetchFrom(p), 427 APP_NAME.fetchFrom(p) + "-post-image.wsf"); 428 } 429 430 private String getAppIdentifier(Map<String, ? super Object> p) { 431 String nm = UPGRADE_UUID.fetchFrom(p).toString(); 432 433 // limitation of innosetup 434 if (nm.length() > 126) { 435 Log.error(getString("message-truncating-id")); 436 nm = nm.substring(0, 126); 437 } 438 439 return nm; 440 } 441 442 private String getLicenseFile(Map<String, ? super Object> p) { 443 String licenseFile = LICENSE_FILE.fetchFrom(p); 444 if (licenseFile != null) { 445 File lfile = new File(licenseFile); 446 File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName()); 447 String filePath = destFile.getAbsolutePath(); 448 if (filePath.contains(" ")) { 449 return "\"" + filePath + "\""; 450 } else { 451 return filePath; 452 } 453 } 454 455 return null; 456 } 457 458 void validateValueAndPut(Map<String, String> data, String key, 459 BundlerParamInfo<String> param, 460 Map<String, ? super Object> p) throws IOException { 461 String value = param.fetchFrom(p); 462 if (value.contains("\r") || value.contains("\n")) { 463 throw new IOException("Configuration Parameter " + 464 param.getID() + " cannot contain multiple lines of text"); 465 } 466 data.put(key, innosetupEscape(value)); 467 } 468 469 private String innosetupEscape(String value) { 470 if (value == null) { 471 return ""; 472 } 473 474 if (value.contains("\"") || !value.trim().equals(value)) { 475 value = "\"" + value.replace("\"", "\"\"") + "\""; 476 } 477 return value; 478 } 479 480 boolean prepareMainProjectFile(Map<String, ? super Object> p) 481 throws IOException { 482 Map<String, String> data = new HashMap<>(); 483 data.put("PRODUCT_APP_IDENTIFIER", 484 innosetupEscape(getAppIdentifier(p))); 485 486 487 validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p); 488 validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p); 489 validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p); 490 validateValueAndPut(data, "INSTALLER_FILE_NAME", 491 INSTALLER_FILE_NAME, p); 492 493 data.put("LAUNCHER_NAME", 494 innosetupEscape(WinAppBundler.getAppName(p))); 495 496 data.put("APPLICATION_LAUNCHER_FILENAME", 497 innosetupEscape(WinAppBundler.getLauncherName(p))); 498 499 data.put("APPLICATION_DESKTOP_SHORTCUT", 500 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 501 data.put("APPLICATION_MENU_SHORTCUT", 502 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 503 validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p); 504 validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p); 505 validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p); 506 507 data.put("APPLICATION_LICENSE_FILE", 508 innosetupEscape(getLicenseFile(p))); 509 data.put("DISABLE_DIR_PAGE", 510 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes"); 511 512 Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p); 513 514 if (isSystemWide) { 515 data.put("APPLICATION_INSTALL_ROOT", "{pf}"); 516 data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); 517 } else { 518 data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); 519 data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); 520 } 521 522 data.put("ARCHITECTURE_BIT_MODE", "x64"); 523 524 validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p); 525 526 validateValueAndPut(data, "APPLICATION_DESCRIPTION", 527 DESCRIPTION, p); 528 529 data.put("APPLICATION_SERVICE", "returnFalse"); 530 data.put("APPLICATION_NOT_SERVICE", "returnFalse"); 531 data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse"); 532 data.put("START_ON_INSTALL", ""); 533 data.put("STOP_ON_UNINSTALL", ""); 534 data.put("RUN_AT_STARTUP", ""); 535 536 String imagePathString = 537 WIN_APP_IMAGE.fetchFrom(p).toPath().toAbsolutePath().toString(); 538 data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString)); 539 Log.verbose("setting APPLICATION_IMAGE to " + 540 innosetupEscape(imagePathString) + " for InnoSetup"); 541 542 StringBuilder secondaryLaunchersCfg = new StringBuilder(); 543 for (Map<String, ? super Object> 544 launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) { 545 String application_name = APP_NAME.fetchFrom(launcher); 546 if (MENU_HINT.fetchFrom(launcher)) { 547 // Name: "{group}\APPLICATION_NAME"; 548 // Filename: "{app}\APPLICATION_NAME.exe"; 549 // IconFilename: "{app}\APPLICATION_NAME.ico" 550 secondaryLaunchersCfg.append("Name: \"{group}\\"); 551 secondaryLaunchersCfg.append(application_name); 552 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 553 secondaryLaunchersCfg.append(application_name); 554 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 555 secondaryLaunchersCfg.append(application_name); 556 secondaryLaunchersCfg.append(".ico\"\r\n"); 557 } 558 if (SHORTCUT_HINT.fetchFrom(launcher)) { 559 // Name: "{commondesktop}\APPLICATION_NAME"; 560 // Filename: "{app}\APPLICATION_NAME.exe"; 561 // IconFilename: "{app}\APPLICATION_NAME.ico" 562 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\"); 563 secondaryLaunchersCfg.append(application_name); 564 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 565 secondaryLaunchersCfg.append(application_name); 566 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 567 secondaryLaunchersCfg.append(application_name); 568 secondaryLaunchersCfg.append(".ico\"\r\n"); 569 } 570 } 571 data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString()); 572 573 StringBuilder registryEntries = new StringBuilder(); 574 String regName = APP_REGISTRY_NAME.fetchFrom(p); 575 List<Map<String, ? super Object>> fetchFrom = 576 FILE_ASSOCIATIONS.fetchFrom(p); 577 for (int i = 0; i < fetchFrom.size(); i++) { 578 Map<String, ? super Object> fileAssociation = fetchFrom.get(i); 579 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 580 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO 581 582 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 583 String entryName = regName + "File"; 584 if (i > 0) { 585 entryName += "." + i; 586 } 587 588 if (extensions == null) { 589 Log.verbose(getString( 590 "message.creating-association-with-null-extension")); 591 } else { 592 for (String ext : extensions) { 593 if (isSystemWide) { 594 // "Root: HKCR; Subkey: \".myp\"; 595 // ValueType: string; ValueName: \"\"; 596 // ValueData: \"MyProgramFile\"; 597 // Flags: uninsdeletevalue" 598 registryEntries.append("Root: HKCR; Subkey: \".") 599 .append(ext) 600 .append("\"; ValueType: string;" 601 + " ValueName: \"\"; ValueData: \"") 602 .append(entryName) 603 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 604 } else { 605 registryEntries.append( 606 "Root: HKCU; Subkey: \"Software\\Classes\\.") 607 .append(ext) 608 .append("\"; ValueType: string;" 609 + " ValueName: \"\"; ValueData: \"") 610 .append(entryName) 611 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 612 } 613 } 614 } 615 616 if (extensions != null && !extensions.isEmpty()) { 617 String ext = extensions.get(0); 618 List<String> mimeTypes = 619 FA_CONTENT_TYPE.fetchFrom(fileAssociation); 620 for (String mime : mimeTypes) { 621 if (isSystemWide) { 622 // "Root: HKCR; 623 // Subkey: HKCR\\Mime\\Database\\ 624 // Content Type\\application/chaos; 625 // ValueType: string; 626 // ValueName: Extension; 627 // ValueData: .chaos; 628 // Flags: uninsdeletevalue" 629 registryEntries.append("Root: HKCR; Subkey: " + 630 "\"Mime\\Database\\Content Type\\") 631 .append(mime) 632 .append("\"; ValueType: string; ValueName: " + 633 "\"Extension\"; ValueData: \".") 634 .append(ext) 635 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 636 } else { 637 registryEntries.append( 638 "Root: HKCU; Subkey: \"Software\\" + 639 "Classes\\Mime\\Database\\Content Type\\") 640 .append(mime) 641 .append("\"; ValueType: string; " + 642 "ValueName: \"Extension\"; ValueData: \".") 643 .append(ext) 644 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n"); 645 } 646 } 647 } 648 649 if (isSystemWide) { 650 // "Root: HKCR; 651 // Subkey: \"MyProgramFile\"; 652 // ValueType: string; 653 // ValueName: \"\"; 654 // ValueData: \"My Program File\"; 655 // Flags: uninsdeletekey" 656 registryEntries.append("Root: HKCR; Subkey: \"") 657 .append(entryName) 658 .append( 659 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 660 .append(removeQuotes(description)) 661 .append("\"; Flags: uninsdeletekey\r\n"); 662 } else { 663 registryEntries.append( 664 "Root: HKCU; Subkey: \"Software\\Classes\\") 665 .append(entryName) 666 .append( 667 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 668 .append(removeQuotes(description)) 669 .append("\"; Flags: uninsdeletekey\r\n"); 670 } 671 672 if (icon != null && icon.exists()) { 673 if (isSystemWide) { 674 // "Root: HKCR; 675 // Subkey: \"MyProgramFile\\DefaultIcon\"; 676 // ValueType: string; 677 // ValueName: \"\"; 678 // ValueData: \"{app}\\MYPROG.EXE,0\"\n" + 679 registryEntries.append("Root: HKCR; Subkey: \"") 680 .append(entryName) 681 .append("\\DefaultIcon\"; ValueType: string; " + 682 "ValueName: \"\"; ValueData: \"{app}\\") 683 .append(icon.getName()) 684 .append("\"\r\n"); 685 } else { 686 registryEntries.append( 687 "Root: HKCU; Subkey: \"Software\\Classes\\") 688 .append(entryName) 689 .append("\\DefaultIcon\"; ValueType: string; " + 690 "ValueName: \"\"; ValueData: \"{app}\\") 691 .append(icon.getName()) 692 .append("\"\r\n"); 693 } 694 } 695 696 if (isSystemWide) { 697 // "Root: HKCR; 698 // Subkey: \"MyProgramFile\\shell\\open\\command\"; 699 // ValueType: string; 700 // ValueName: \"\"; 701 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n" 702 registryEntries.append("Root: HKCR; Subkey: \"") 703 .append(entryName) 704 .append("\\shell\\open\\command\"; ValueType: " + 705 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 706 .append(APP_NAME.fetchFrom(p)) 707 .append("\"\" \"\"%1\"\"\"\r\n"); 708 } else { 709 registryEntries.append( 710 "Root: HKCU; Subkey: \"Software\\Classes\\") 711 .append(entryName) 712 .append("\\shell\\open\\command\"; ValueType: " + 713 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 714 .append(APP_NAME.fetchFrom(p)) 715 .append("\"\" \"\"%1\"\"\"\r\n"); 716 } 717 } 718 if (registryEntries.length() > 0) { 719 data.put("FILE_ASSOCIATIONS", 720 "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + 721 registryEntries.toString()); 722 } else { 723 data.put("FILE_ASSOCIATIONS", ""); 724 } 725 726 // TODO - alternate template for JRE installer 727 String iss = RUNTIME_INSTALLER.fetchFrom(p) ? 728 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE; 729 730 Writer w = new BufferedWriter(new FileWriter( 731 getConfig_ExeProjectFile(p))); 732 733 String content = preprocessTextResource( 734 getConfig_ExeProjectFile(p).getName(), 735 getString("resource.inno-setup-project-file"), 736 iss, data, VERBOSE.fetchFrom(p), 737 RESOURCE_DIR.fetchFrom(p)); 738 w.write(content); 739 w.close(); 740 return true; 741 } 742 743 private final static String removeQuotes(String s) { 744 if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) { 745 // special case for '"XXX"' return 'XXX' not '-XXX-' 746 // note '"' and '""' are excluded from this special case 747 s = s.substring(1, s.length() - 1); 748 } 749 // if there interior double quotes replace them with '-' 750 return s.replaceAll("\"", "-"); 751 } 752 753 private final static String DEFAULT_INNO_SETUP_ICON = 754 "icon_inno_setup.bmp"; 755 756 private boolean prepareProjectConfig(Map<String, ? super Object> p) 757 throws IOException { 758 prepareMainProjectFile(p); 759 760 // prepare installer icon 761 File iconTarget = getConfig_SmallInnoSetupIcon(p); 762 fetchResource(iconTarget.getName(), 763 getString("resource.setup-icon"), 764 DEFAULT_INNO_SETUP_ICON, 765 iconTarget, 766 VERBOSE.fetchFrom(p), 767 RESOURCE_DIR.fetchFrom(p)); 768 769 fetchResource(getConfig_Script(p).getName(), 770 getString("resource.post-install-script"), 771 (String) null, 772 getConfig_Script(p), 773 VERBOSE.fetchFrom(p), 774 RESOURCE_DIR.fetchFrom(p)); 775 return true; 776 } 777 778 private File getConfig_SmallInnoSetupIcon( 779 Map<String, ? super Object> p) { 780 return new File(EXE_IMAGE_DIR.fetchFrom(p), 781 APP_NAME.fetchFrom(p) + "-setup-icon.bmp"); 782 } 783 784 private File getConfig_ExeProjectFile(Map<String, ? super Object> p) { 785 return new File(EXE_IMAGE_DIR.fetchFrom(p), 786 APP_NAME.fetchFrom(p) + ".iss"); 787 } 788 789 790 private File buildEXE(Map<String, ? super Object> p, File outdir) 791 throws IOException { 792 Log.verbose(MessageFormat.format( 793 getString("message.outputting-to-location"), 794 outdir.getAbsolutePath())); 795 796 outdir.mkdirs(); 797 798 // run Inno Setup 799 ProcessBuilder pb = new ProcessBuilder( 800 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p), 801 "/q", // turn off inno setup output 802 "/o"+outdir.getAbsolutePath(), 803 getConfig_ExeProjectFile(p).getAbsolutePath()); 804 pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p)); 805 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 806 807 Log.verbose(MessageFormat.format( 808 getString("message.output-location"), 809 outdir.getAbsolutePath())); 810 811 // presume the result is the ".exe" file with the newest modified time 812 // not the best solution, but it is the most reliable 813 File result = null; 814 long lastModified = 0; 815 File[] list = outdir.listFiles(); 816 if (list != null) { 817 for (File f : list) { 818 if (f.getName().endsWith(".exe") && 819 f.lastModified() > lastModified) { 820 result = f; 821 lastModified = f.lastModified(); 822 } 823 } 824 } 825 826 return result; 827 } 828 829 public static void ensureByMutationFileIsRTF(File f) { 830 if (f == null || !f.isFile()) return; 831 832 try { 833 boolean existingLicenseIsRTF = false; 834 835 try (FileInputStream fin = new FileInputStream(f)) { 836 byte[] firstBits = new byte[7]; 837 838 if (fin.read(firstBits) == firstBits.length) { 839 String header = new String(firstBits); 840 existingLicenseIsRTF = "{\\rtf1\\".equals(header); 841 } 842 } 843 844 if (!existingLicenseIsRTF) { 845 List<String> oldLicense = Files.readAllLines(f.toPath()); 846 try (Writer w = Files.newBufferedWriter( 847 f.toPath(), Charset.forName("Windows-1252"))) { 848 w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" 849 + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" 850 + "\\viewkind4\\uc1\\pard\\sa200\\sl276" 851 + "\\slmult1\\lang9\\fs20 "); 852 oldLicense.forEach(l -> { 853 try { 854 for (char c : l.toCharArray()) { 855 if (c < 0x10) { 856 w.write("\\'0"); 857 w.write(Integer.toHexString(c)); 858 } else if (c > 0xff) { 859 w.write("\\ud"); 860 w.write(Integer.toString(c)); 861 w.write("?"); 862 } else if ((c < 0x20) || (c >= 0x80) || 863 (c == 0x5C) || (c == 0x7B) || 864 (c == 0x7D)) { 865 w.write("\\'"); 866 w.write(Integer.toHexString(c)); 867 } else { 868 w.write(c); 869 } 870 } 871 if (l.length() < 1) { 872 w.write("\\par"); 873 } else { 874 w.write(" "); 875 } 876 w.write("\r\n"); 877 } catch (IOException e) { 878 Log.verbose(e); 879 } 880 }); 881 w.write("}\r\n"); 882 } 883 } 884 } catch (IOException e) { 885 Log.verbose(e); 886 } 887 } 888 889 private static String getString(String key) 890 throws MissingResourceException { 891 return I18N.getString(key); 892 } 893 }