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