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