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, 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); 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\"; 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 try (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 } 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); 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 } | 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\"; 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 } |