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