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.packager.internal.windows; 27 28 import jdk.packager.internal.*; 29 import jdk.packager.internal.ConfigException; 30 import jdk.packager.internal.Arguments; 31 import jdk.packager.internal.UnsupportedPlatformException; 32 import jdk.packager.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.packager.internal.windows.WindowsBundlerParam.*; 43 44 public class WinExeBundler extends AbstractBundler { 45 46 private static final ResourceBundle I18N = ResourceBundle.getBundle( 47 "jdk.packager.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 = new Double(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.info(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.info(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.info(getString("error.iscc-not-found")); 454 Log.info(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.info(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.info(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.info(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 validateValueAndPut(data, "APPLICATION_NAME", APP_NAME, p); 563 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("APPLICATION_LAUNCHER_FILENAME", 570 innosetupEscape(WinAppBundler.getLauncherName(p))); 571 572 data.put("APPLICATION_DESKTOP_SHORTCUT", 573 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 574 data.put("APPLICATION_MENU_SHORTCUT", 575 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse"); 576 validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p); 577 validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, p); 578 validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p); 579 580 data.put("APPLICATION_LICENSE_FILE", 581 innosetupEscape(getLicenseFile(p))); 582 data.put("DISABLE_DIR_PAGE", 583 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes"); 584 585 Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p); 586 587 if (isSystemWide) { 588 data.put("APPLICATION_INSTALL_ROOT", "{pf}"); 589 data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); 590 } else { 591 data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); 592 data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); 593 } 594 595 if (BIT_ARCH_64.fetchFrom(p)) { 596 data.put("ARCHITECTURE_BIT_MODE", "x64"); 597 } else { 598 data.put("ARCHITECTURE_BIT_MODE", ""); 599 } 600 validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p); 601 602 validateValueAndPut(data, "APPLICATION_DESCRIPTION", 603 DESCRIPTION, p); 604 605 data.put("APPLICATION_SERVICE", "returnFalse"); 606 data.put("APPLICATION_NOT_SERVICE", "returnFalse"); 607 data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse"); 608 data.put("START_ON_INSTALL", ""); 609 data.put("STOP_ON_UNINSTALL", ""); 610 data.put("RUN_AT_STARTUP", ""); 611 612 StringBuilder secondaryLaunchersCfg = new StringBuilder(); 613 for (Map<String, ? super Object> 614 launcher : SECONDARY_LAUNCHERS.fetchFrom(p)) { 615 String application_name = APP_NAME.fetchFrom(launcher); 616 if (MENU_HINT.fetchFrom(launcher)) { 617 // Name: "{group}\APPLICATION_NAME"; 618 // Filename: "{app}\APPLICATION_NAME.exe"; 619 // IconFilename: "{app}\APPLICATION_NAME.ico" 620 secondaryLaunchersCfg.append("Name: \"{group}\\"); 621 secondaryLaunchersCfg.append(application_name); 622 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 623 secondaryLaunchersCfg.append(application_name); 624 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 625 secondaryLaunchersCfg.append(application_name); 626 secondaryLaunchersCfg.append(".ico\"\r\n"); 627 } 628 if (SHORTCUT_HINT.fetchFrom(launcher)) { 629 // Name: "{commondesktop}\APPLICATION_NAME"; 630 // Filename: "{app}\APPLICATION_NAME.exe"; 631 // IconFilename: "{app}\APPLICATION_NAME.ico" 632 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\"); 633 secondaryLaunchersCfg.append(application_name); 634 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\"); 635 secondaryLaunchersCfg.append(application_name); 636 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\"); 637 secondaryLaunchersCfg.append(application_name); 638 secondaryLaunchersCfg.append(".ico\"\r\n"); 639 } 640 } 641 data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString()); 642 643 StringBuilder registryEntries = new StringBuilder(); 644 String regName = APP_REGISTRY_NAME.fetchFrom(p); 645 List<Map<String, ? super Object>> fetchFrom = 646 FILE_ASSOCIATIONS.fetchFrom(p); 647 for (int i = 0; i < fetchFrom.size(); i++) { 648 Map<String, ? super Object> fileAssociation = fetchFrom.get(i); 649 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 650 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO 651 652 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 653 String entryName = regName + "File"; 654 if (i > 0) { 655 entryName += "." + i; 656 } 657 658 if (extensions == null) { 659 Log.info(getString( 660 "message.creating-association-with-null-extension")); 661 } else { 662 for (String ext : extensions) { 663 if (isSystemWide) { 664 // "Root: HKCR; Subkey: \".myp\"; 665 // ValueType: string; ValueName: \"\"; 666 // ValueData: \"MyProgramFile\"; 667 // Flags: uninsdeletevalue" 668 registryEntries.append("Root: HKCR; Subkey: \".") 669 .append(ext) 670 .append("\"; ValueType: string;" 671 + " ValueName: \"\"; ValueData: \"") 672 .append(entryName) 673 .append("\"; Flags: uninsdeletevalue\r\n"); 674 } else { 675 registryEntries.append( 676 "Root: HKCU; Subkey: \"Software\\Classes\\.") 677 .append(ext) 678 .append("\"; ValueType: string;" 679 + " ValueName: \"\"; ValueData: \"") 680 .append(entryName) 681 .append("\"; Flags: uninsdeletevalue\r\n"); 682 } 683 } 684 } 685 686 if (extensions != null && !extensions.isEmpty()) { 687 String ext = extensions.get(0); 688 List<String> mimeTypes = 689 FA_CONTENT_TYPE.fetchFrom(fileAssociation); 690 for (String mime : mimeTypes) { 691 if (isSystemWide) { 692 // "Root: HKCR; 693 // Subkey: HKCR\\Mime\\Database\\ 694 // Content Type\\application/chaos; 695 // ValueType: string; 696 // ValueName: Extension; 697 // ValueData: .chaos; 698 // Flags: uninsdeletevalue" 699 registryEntries.append("Root: HKCR; Subkey: " + 700 "\"Mime\\Database\\Content Type\\") 701 .append(mime) 702 .append("\"; ValueType: string; ValueName: " + 703 "\"Extension\"; ValueData: \".") 704 .append(ext) 705 .append("\"; Flags: uninsdeletevalue\r\n"); 706 } else { 707 registryEntries.append( 708 "Root: HKCU; Subkey: \"Software\\" + 709 "Classes\\Mime\\Database\\Content Type\\") 710 .append(mime) 711 .append("\"; ValueType: string; " + 712 "ValueName: \"Extension\"; ValueData: \".") 713 .append(ext) 714 .append("\"; Flags: uninsdeletevalue\r\n"); 715 } 716 } 717 } 718 719 if (isSystemWide) { 720 // "Root: HKCR; 721 // Subkey: \"MyProgramFile\"; 722 // ValueType: string; 723 // ValueName: \"\"; 724 // ValueData: \"My Program File\"; 725 // Flags: uninsdeletekey" 726 registryEntries.append("Root: HKCR; Subkey: \"") 727 .append(entryName) 728 .append( 729 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 730 .append(description) 731 .append("\"; Flags: uninsdeletekey\r\n"); 732 } else { 733 registryEntries.append( 734 "Root: HKCU; Subkey: \"Software\\Classes\\") 735 .append(entryName) 736 .append( 737 "\"; ValueType: string; ValueName: \"\"; ValueData: \"") 738 .append(description) 739 .append("\"; Flags: uninsdeletekey\r\n"); 740 } 741 742 if (icon != null && icon.exists()) { 743 if (isSystemWide) { 744 // "Root: HKCR; 745 // Subkey: \"MyProgramFile\\DefaultIcon\"; 746 // ValueType: string; 747 // ValueName: \"\"; 748 // ValueData: \"{app}\\MYPROG.EXE,0\"\n" + 749 registryEntries.append("Root: HKCR; Subkey: \"") 750 .append(entryName) 751 .append("\\DefaultIcon\"; ValueType: string; " + 752 "ValueName: \"\"; ValueData: \"{app}\\") 753 .append(icon.getName()) 754 .append("\"\r\n"); 755 } else { 756 registryEntries.append( 757 "Root: HKCU; Subkey: \"Software\\Classes\\") 758 .append(entryName) 759 .append("\\DefaultIcon\"; ValueType: string; " + 760 "ValueName: \"\"; ValueData: \"{app}\\") 761 .append(icon.getName()) 762 .append("\"\r\n"); 763 } 764 } 765 766 if (isSystemWide) { 767 // "Root: HKCR; 768 // Subkey: \"MyProgramFile\\shell\\open\\command\"; 769 // ValueType: string; 770 // ValueName: \"\"; 771 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n" 772 registryEntries.append("Root: HKCR; Subkey: \"") 773 .append(entryName) 774 .append("\\shell\\open\\command\"; ValueType: " + 775 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 776 .append(APP_NAME.fetchFrom(p)) 777 .append("\"\" \"\"%1\"\"\"\r\n"); 778 } else { 779 registryEntries.append( 780 "Root: HKCU; Subkey: \"Software\\Classes\\") 781 .append(entryName) 782 .append("\\shell\\open\\command\"; ValueType: " + 783 "string; ValueName: \"\"; ValueData: \"\"\"{app}\\") 784 .append(APP_NAME.fetchFrom(p)) 785 .append("\"\" \"\"%1\"\"\"\r\n"); 786 } 787 } 788 if (registryEntries.length() > 0) { 789 data.put("FILE_ASSOCIATIONS", 790 "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + 791 registryEntries.toString()); 792 } else { 793 data.put("FILE_ASSOCIATIONS", ""); 794 } 795 796 // TODO - alternate template for JRE installer 797 String iss = Arguments.CREATE_JRE_INSTALLER.fetchFrom(p) ? 798 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE; 799 800 Writer w = new BufferedWriter(new FileWriter( 801 getConfig_ExeProjectFile(p))); 802 803 String content = preprocessTextResource( 804 WinAppBundler.WIN_BUNDLER_PREFIX + 805 getConfig_ExeProjectFile(p).getName(), 806 getString("resource.inno-setup-project-file"), 807 iss, data, VERBOSE.fetchFrom(p), 808 DROP_IN_RESOURCES_ROOT.fetchFrom(p)); 809 w.write(content); 810 w.close(); 811 return true; 812 } 813 814 private final static String DEFAULT_INNO_SETUP_ICON = 815 "icon_inno_setup.bmp"; 816 817 private boolean prepareProjectConfig(Map<String, ? super Object> p) 818 throws IOException { 819 prepareMainProjectFile(p); 820 821 // prepare installer icon 822 File iconTarget = getConfig_SmallInnoSetupIcon(p); 823 fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(), 824 getString("resource.setup-icon"), 825 DEFAULT_INNO_SETUP_ICON, 826 iconTarget, 827 VERBOSE.fetchFrom(p), 828 DROP_IN_RESOURCES_ROOT.fetchFrom(p)); 829 830 fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + 831 getConfig_Script(p).getName(), 832 getString("resource.post-install-script"), 833 (String) null, 834 getConfig_Script(p), 835 VERBOSE.fetchFrom(p), 836 DROP_IN_RESOURCES_ROOT.fetchFrom(p)); 837 return true; 838 } 839 840 private File getConfig_SmallInnoSetupIcon( 841 Map<String, ? super Object> p) { 842 return new File(EXE_IMAGE_DIR.fetchFrom(p), 843 APP_NAME.fetchFrom(p) + "-setup-icon.bmp"); 844 } 845 846 private File getConfig_ExeProjectFile(Map<String, ? super Object> p) { 847 return new File(EXE_IMAGE_DIR.fetchFrom(p), 848 APP_NAME.fetchFrom(p) + ".iss"); 849 } 850 851 852 private File buildEXE(Map<String, ? super Object> p, File outdir) 853 throws IOException { 854 Log.verbose(MessageFormat.format( 855 getString("message.outputting-to-location"), 856 outdir.getAbsolutePath())); 857 858 outdir.mkdirs(); 859 860 // run Inno Setup 861 ProcessBuilder pb = new ProcessBuilder( 862 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p), 863 "/q", // turn off inno setup output 864 "/o"+outdir.getAbsolutePath(), 865 getConfig_ExeProjectFile(p).getAbsolutePath()); 866 pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p)); 867 IOUtils.exec(pb, VERBOSE.fetchFrom(p)); 868 869 Log.info(MessageFormat.format( 870 getString("message.output-location"), 871 outdir.getAbsolutePath())); 872 873 // presume the result is the ".exe" file with the newest modified time 874 // not the best solution, but it is the most reliable 875 File result = null; 876 long lastModified = 0; 877 File[] list = outdir.listFiles(); 878 if (list != null) { 879 for (File f : list) { 880 if (f.getName().endsWith(".exe") && 881 f.lastModified() > lastModified) { 882 result = f; 883 lastModified = f.lastModified(); 884 } 885 } 886 } 887 888 return result; 889 } 890 891 public static void ensureByMutationFileIsRTF(File f) { 892 if (f == null || !f.isFile()) return; 893 894 try { 895 boolean existingLicenseIsRTF = false; 896 897 try (FileInputStream fin = new FileInputStream(f)) { 898 byte[] firstBits = new byte[7]; 899 900 if (fin.read(firstBits) == firstBits.length) { 901 String header = new String(firstBits); 902 existingLicenseIsRTF = "{\\rtf1\\".equals(header); 903 } 904 } 905 906 if (!existingLicenseIsRTF) { 907 List<String> oldLicense = Files.readAllLines(f.toPath()); 908 try (Writer w = Files.newBufferedWriter( 909 f.toPath(), Charset.forName("Windows-1252"))) { 910 w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" 911 + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" 912 + "\\viewkind4\\uc1\\pard\\sa200\\sl276" 913 + "\\slmult1\\lang9\\fs20 "); 914 oldLicense.forEach(l -> { 915 try { 916 for (char c : l.toCharArray()) { 917 if (c < 0x10) { 918 w.write("\\'0"); 919 w.write(Integer.toHexString(c)); 920 } else if (c > 0xff) { 921 w.write("\\ud"); 922 w.write(Integer.toString(c)); 923 w.write("?"); 924 } else if ((c < 0x20) || (c >= 0x80) || 925 (c == 0x5C) || (c == 0x7B) || 926 (c == 0x7D)) { 927 w.write("\\'"); 928 w.write(Integer.toHexString(c)); 929 } else { 930 w.write(c); 931 } 932 } 933 if (l.length() < 1) { 934 w.write("\\par"); 935 } else { 936 w.write(" "); 937 } 938 w.write("\r\n"); 939 } catch (IOException e) { 940 Log.verbose(e); 941 } 942 }); 943 w.write("}\r\n"); 944 } 945 } 946 } catch (IOException e) { 947 Log.verbose(e); 948 } 949 } 950 951 private static String getString(String key) 952 throws MissingResourceException { 953 return I18N.getString(key); 954 } 955 }