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