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