< prev index next >

src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java

Print this page




 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         }


< prev index next >