src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java
branchJDK-8200758-branch
changeset 58696 61c44899b4eb
parent 58647 2c43b89b1679
child 58762 0fe62353385b
equal deleted inserted replaced
58695:64adf683bc7b 58696:61c44899b4eb
    31 import java.nio.file.Path;
    31 import java.nio.file.Path;
    32 import java.nio.file.Paths;
    32 import java.nio.file.Paths;
    33 import java.text.MessageFormat;
    33 import java.text.MessageFormat;
    34 import java.util.*;
    34 import java.util.*;
    35 import java.util.regex.Pattern;
    35 import java.util.regex.Pattern;
       
    36 import java.util.stream.Collectors;
    36 import javax.xml.stream.XMLOutputFactory;
    37 import javax.xml.stream.XMLOutputFactory;
    37 import javax.xml.stream.XMLStreamException;
    38 import javax.xml.stream.XMLStreamException;
    38 import javax.xml.stream.XMLStreamWriter;
    39 import javax.xml.stream.XMLStreamWriter;
    39 import static jdk.jpackage.internal.OverridableResource.createResource;
    40 import static jdk.jpackage.internal.OverridableResource.createResource;
       
    41 import static jdk.jpackage.internal.StandardBundlerParam.*;
    40 
    42 
    41 import static jdk.jpackage.internal.WindowsBundlerParam.*;
    43 import static jdk.jpackage.internal.WindowsBundlerParam.*;
    42 
    44 
    43 /**
    45 /**
    44  * WinMsiBundler
    46  * WinMsiBundler
    50  * of installer to be processed by WiX tools. Generated source files are stored
    52  * of installer to be processed by WiX tools. Generated source files are stored
    51  * in "config" subdirectory next to "app" subdirectory in the root work
    53  * in "config" subdirectory next to "app" subdirectory in the root work
    52  * directory. The following WiX source files are generated:
    54  * directory. The following WiX source files are generated:
    53  * <ul>
    55  * <ul>
    54  * <li>main.wxs. Main source file with the installer description
    56  * <li>main.wxs. Main source file with the installer description
    55  * <li>bundle.wxi. Source file with application and Java run-time directory tree
    57  * <li>bundle.wxf. Source file with application and Java run-time directory tree
    56  * description. This source file is included from main.wxs
    58  * description.
    57  * <li>icons.wxi. Source file with the list of icons used by the application.
       
    58  * This source file is included from main.wxs
       
    59  * </ul>
    59  * </ul>
    60  * <p>
    60  * <p>
    61  * main.wxs file is a copy of main.wxs resource from
    61  * main.wxs file is a copy of main.wxs resource from
    62  * jdk.jpackage.internal.resources package. It is parametrized with the
    62  * jdk.jpackage.internal.resources package. It is parametrized with the
    63  * following WiX variables:
    63  * following WiX variables:
    84  * option was specified. Undefined otherwise
    84  * option was specified. Undefined otherwise
    85  * <li>JpConfigDir. Absolute path to the directory with generated WiX source
    85  * <li>JpConfigDir. Absolute path to the directory with generated WiX source
    86  * files.
    86  * files.
    87  * <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
    87  * <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
    88  * option was not specified. Undefined otherwise
    88  * option was not specified. Undefined otherwise
    89  * <li>JpWixVersion36OrNewer. Set to "yes" if WiX Toolkit v3.6 or newer is used.
       
    90  * Undefined otherwise
       
    91  * </ul>
    89  * </ul>
    92  */
    90  */
    93 public class WinMsiBundler  extends AbstractBundler {
    91 public class WinMsiBundler  extends AbstractBundler {
    94 
       
    95     private static final ResourceBundle I18N = ResourceBundle.getBundle(
       
    96             "jdk.jpackage.internal.resources.WinResources");
       
    97 
    92 
    98     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
    93     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
    99             new WindowsBundlerParam<>(
    94             new WindowsBundlerParam<>(
   100             "win.app.bundler",
    95             "win.app.bundler",
   101             WinAppBundler.class,
    96             WinAppBundler.class,
   102             params -> new WinAppBundler(),
    97             params -> new WinAppBundler(),
   103             null);
    98             null);
   104 
       
   105     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 =
       
   106             new WindowsBundlerParam<>(
       
   107             "win.msi.canUseWix36",
       
   108             Boolean.class,
       
   109             params -> false,
       
   110             (s, p) -> Boolean.valueOf(s));
       
   111 
    99 
   112     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
   100     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
   113             new WindowsBundlerParam<>(
   101             new WindowsBundlerParam<>(
   114             "win.msi.imageDir",
   102             "win.msi.imageDir",
   115             File.class,
   103             File.class,
   152             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
   140             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
   153             UUID.class,
   141             UUID.class,
   154             params -> UUID.randomUUID(),
   142             params -> UUID.randomUUID(),
   155             (s, p) -> UUID.fromString(s));
   143             (s, p) -> UUID.fromString(s));
   156 
   144 
   157     private static final String TOOL_CANDLE = "candle.exe";
       
   158     private static final String TOOL_LIGHT = "light.exe";
       
   159     // autodetect just v3.7, v3.8, 3.9, 3.10 and 3.11
       
   160     private static final String AUTODETECT_DIRS =
       
   161             ";C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;"
       
   162             + "C:\\Program Files\\WiX Toolset v3.11\\bin;"
       
   163             + "C:\\Program Files (x86)\\WiX Toolset v3.10\\bin;"
       
   164             + "C:\\Program Files\\WiX Toolset v3.10\\bin;"
       
   165             + "C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;"
       
   166             + "C:\\Program Files\\WiX Toolset v3.9\\bin;"
       
   167             + "C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;"
       
   168             + "C:\\Program Files\\WiX Toolset v3.8\\bin;"
       
   169             + "C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;"
       
   170             + "C:\\Program Files\\WiX Toolset v3.7\\bin";
       
   171 
       
   172     private static String getCandlePath() {
       
   173         for (String dirString : (System.getenv("PATH")
       
   174                 + AUTODETECT_DIRS).split(";")) {
       
   175             File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
       
   176             if (f.isFile()) {
       
   177                 return f.toString();
       
   178             }
       
   179         }
       
   180         return null;
       
   181     }
       
   182 
       
   183     private static String getLightPath() {
       
   184         for (String dirString : (System.getenv("PATH")
       
   185                 + AUTODETECT_DIRS).split(";")) {
       
   186             File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
       
   187             if (f.isFile()) {
       
   188                 return f.toString();
       
   189             }
       
   190         }
       
   191         return null;
       
   192     }
       
   193 
       
   194 
       
   195     public static final StandardBundlerParam<Boolean> MENU_HINT =
       
   196         new WindowsBundlerParam<>(
       
   197                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
       
   198                 Boolean.class,
       
   199                 params -> false,
       
   200                 // valueOf(null) is false,
       
   201                 // and we actually do want null in some cases
       
   202                 (s, p) -> (s == null ||
       
   203                         "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
       
   204         );
       
   205 
       
   206     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
       
   207         new WindowsBundlerParam<>(
       
   208                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
       
   209                 Boolean.class,
       
   210                 params -> false,
       
   211                 // valueOf(null) is false,
       
   212                 // and we actually do want null in some cases
       
   213                 (s, p) -> (s == null ||
       
   214                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
       
   215         );
       
   216 
       
   217     @Override
   145     @Override
   218     public String getName() {
   146     public String getName() {
   219         return I18N.getString("msi.bundler.name");
   147         return I18N.getString("msi.bundler.name");
   220     }
   148     }
   221 
   149 
   235         return bundle(params, outputParentDir);
   163         return bundle(params, outputParentDir);
   236     }
   164     }
   237 
   165 
   238     @Override
   166     @Override
   239     public boolean supported(boolean platformInstaller) {
   167     public boolean supported(boolean platformInstaller) {
   240         return isSupported();
       
   241     }
       
   242 
       
   243     @Override
       
   244     public boolean isDefault() {
       
   245         return false;
       
   246     }
       
   247 
       
   248     public static boolean isSupported() {
       
   249         try {
   168         try {
   250             validateWixTools();
   169             if (wixToolset == null) {
       
   170                 wixToolset = WixTool.toolset();
       
   171             }
   251             return true;
   172             return true;
   252         } catch (ConfigException ce) {
   173         } catch (ConfigException ce) {
   253             Log.error(ce.getMessage());
   174             Log.error(ce.getMessage());
   254             if (ce.getAdvice() != null) {
   175             if (ce.getAdvice() != null) {
   255                 Log.error(ce.getAdvice());
   176                 Log.error(ce.getAdvice());
   258             Log.error(e.getMessage());
   179             Log.error(e.getMessage());
   259         }
   180         }
   260         return false;
   181         return false;
   261     }
   182     }
   262 
   183 
   263     private static String findToolVersion(String toolName) {
   184     @Override
   264         try {
   185     public boolean isDefault() {
   265             if (toolName == null || "".equals(toolName)) return null;
   186         return false;
   266 
       
   267             ProcessBuilder pb = new ProcessBuilder(
       
   268                     toolName,
       
   269                     "/?");
       
   270             VersionExtractor ve = new VersionExtractor("version (\\d+.\\d+)");
       
   271             // not interested in the output
       
   272             IOUtils.exec(pb, true, ve);
       
   273             String version = ve.getVersion();
       
   274             Log.verbose(MessageFormat.format(
       
   275                     I18N.getString("message.tool-version"),
       
   276                     toolName, version));
       
   277             return version;
       
   278         } catch (Exception e) {
       
   279             Log.verbose(e);
       
   280             return null;
       
   281         }
       
   282     }
       
   283 
       
   284     public static void validateWixTools() throws ConfigException{
       
   285         String candleVersion = findToolVersion(getCandlePath());
       
   286         String lightVersion = findToolVersion(getLightPath());
       
   287 
       
   288         // WiX 3.0+ is required
       
   289         String minVersion = "3.0";
       
   290         if (candleVersion == null || lightVersion == null) {
       
   291             throw new ConfigException(
       
   292                     I18N.getString("error.no-wix-tools"),
       
   293                     I18N.getString("error.no-wix-tools.advice"));
       
   294         }
       
   295 
       
   296         if (VersionExtractor.isLessThan(candleVersion, minVersion)) {
       
   297             throw new ConfigException(
       
   298                     MessageFormat.format(
       
   299                     I18N.getString("message.wrong-tool-version"),
       
   300                     TOOL_CANDLE, candleVersion, minVersion),
       
   301                     I18N.getString("error.no-wix-tools.advice"));
       
   302         }
       
   303         if (VersionExtractor.isLessThan(lightVersion, minVersion)) {
       
   304             throw new ConfigException(
       
   305                     MessageFormat.format(
       
   306                     I18N.getString("message.wrong-tool-version"),
       
   307                     TOOL_LIGHT, lightVersion, minVersion),
       
   308                     I18N.getString("error.no-wix-tools.advice"));
       
   309         }
       
   310     }
   187     }
   311 
   188 
   312     @Override
   189     @Override
   313     public boolean validate(Map<String, ? super Object> params)
   190     public boolean validate(Map<String, ? super Object> params)
   314             throws ConfigException {
   191             throws ConfigException {
   315         try {
   192         try {
   316             if (params == null) throw new ConfigException(
   193             if (wixToolset == null) {
   317                     I18N.getString("error.parameters-null"),
   194                 wixToolset = WixTool.toolset();
   318                     I18N.getString("error.parameters-null.advice"));
   195             }
   319 
   196 
   320             // run basic validation to ensure requirements are met
   197             for (var toolInfo: wixToolset.values()) {
   321 
   198                 Log.verbose(MessageFormat.format(I18N.getString(
   322             String lightVersion = findToolVersion(getLightPath());
   199                         "message.tool-version"), toolInfo.path.getFileName(),
   323             if (!VersionExtractor.isLessThan(lightVersion, "3.6")) {
   200                         toolInfo.version));
   324                 Log.verbose(I18N.getString("message.use-wix36-features"));
   201             }
   325                 params.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
   202 
   326             }
   203             wixSourcesBuilder.setWixVersion(wixToolset.get(WixTool.Light).version);
       
   204 
       
   205             wixSourcesBuilder.logWixFeatures();
   327 
   206 
   328             /********* validate bundle parameters *************/
   207             /********* validate bundle parameters *************/
   329 
   208 
   330             String version = PRODUCT_VERSION.fetchFrom(params);
   209             String version = PRODUCT_VERSION.fetchFrom(params);
   331             if (!isVersionStringValid(version)) {
   210             if (!isVersionStringValid(version)) {
   413         }
   292         }
   414 
   293 
   415         return true;
   294         return true;
   416     }
   295     }
   417 
   296 
   418     private boolean prepareProto(Map<String, ? super Object> params)
   297     private void prepareProto(Map<String, ? super Object> params)
   419                 throws PackagerException, IOException {
   298                 throws PackagerException, IOException {
   420         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
   299         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
   421         File appDir = null;
   300         File appDir = null;
   422 
   301 
   423         // we either have an application image or need to build one
   302         // we either have an application image or need to build one
   443 
   322 
   444             IOUtils.copyFile(lfile, destFile);
   323             IOUtils.copyFile(lfile, destFile);
   445             destFile.setWritable(true);
   324             destFile.setWritable(true);
   446             ensureByMutationFileIsRTF(destFile);
   325             ensureByMutationFileIsRTF(destFile);
   447         }
   326         }
   448 
       
   449         // copy file association icons
       
   450         List<Map<String, ? super Object>> fileAssociations =
       
   451                 FILE_ASSOCIATIONS.fetchFrom(params);
       
   452         for (Map<String, ? super Object> fa : fileAssociations) {
       
   453             File icon = FA_ICON.fetchFrom(fa);
       
   454             if (icon == null) {
       
   455                 continue;
       
   456             }
       
   457 
       
   458             File faIconFile = new File(appDir, icon.getName());
       
   459 
       
   460             if (icon.exists()) {
       
   461                 try {
       
   462                     IOUtils.copyFile(icon, faIconFile);
       
   463                 } catch (IOException e) {
       
   464                     Log.verbose(e);
       
   465                 }
       
   466             }
       
   467         }
       
   468 
       
   469         return appDir != null;
       
   470     }
   327     }
   471 
   328 
   472     public File bundle(Map<String, ? super Object> params, File outdir)
   329     public File bundle(Map<String, ? super Object> params, File outdir)
   473             throws PackagerException {
   330             throws PackagerException {
   474 
   331 
   475         IOUtils.writableOutputDir(outdir.toPath());
   332         IOUtils.writableOutputDir(outdir.toPath());
   476 
   333 
   477         // validate we have valid tools before continuing
   334         Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath();
   478         String light = getLightPath();
       
   479         String candle = getCandlePath();
       
   480         if (light == null || !new File(light).isFile() ||
       
   481             candle == null || !new File(candle).isFile()) {
       
   482             Log.verbose(MessageFormat.format(
       
   483                    I18N.getString("message.light-file-string"), light));
       
   484             Log.verbose(MessageFormat.format(
       
   485                    I18N.getString("message.candle-file-string"), candle));
       
   486             throw new PackagerException("error.no-wix-tools");
       
   487         }
       
   488 
       
   489         Map<String, String> wixVars = null;
       
   490 
       
   491         File imageDir = MSI_IMAGE_DIR.fetchFrom(params);
       
   492         try {
   335         try {
   493             imageDir.mkdirs();
   336             Files.createDirectories(imageDir);
   494 
   337 
   495             prepareBasicProjectConfig(params);
   338             Path postImageScript = imageDir.resolve(APP_NAME.fetchFrom(params) + "-post-image.wsf");
   496             if (prepareProto(params)) {
   339             createResource(null, params)
   497                 wixVars = prepareWiXConfig(params);
   340                     .setCategory(I18N.getString("resource.post-install-script"))
   498 
   341                     .saveToFile(postImageScript);
   499                 File configScriptSrc = getConfig_Script(params);
   342 
   500                 if (configScriptSrc.exists()) {
   343             prepareProto(params);
   501                     // we need to be running post script in the image folder
   344 
   502 
   345             wixSourcesBuilder
   503                     // NOTE: Would it be better to generate it to the image
   346             .initFromParams(WIN_APP_IMAGE.fetchFrom(params).toPath(), params)
   504                     // folder and save only if "verbose" is requested?
   347             .createMainFragment(CONFIG_ROOT.fetchFrom(params).toPath().resolve(
   505 
   348                     "bundle.wxf"));
   506                     // for now we replicate it
   349 
   507                     File configScript =
   350             Map<String, String> wixVars = prepareMainProjectFile(params);
   508                         new File(imageDir, configScriptSrc.getName());
   351 
   509                     IOUtils.copyFile(configScriptSrc, configScript);
   352             if (Files.exists(postImageScript)) {
   510                     Log.verbose(MessageFormat.format(
   353                 Log.verbose(MessageFormat.format(I18N.getString(
   511                             I18N.getString("message.running-wsh-script"),
   354                         "message.running-wsh-script"),
   512                             configScript.getAbsolutePath()));
   355                         postImageScript.toAbsolutePath()));
   513                     IOUtils.run("wscript", configScript);
   356                 Executor.of("wscript", postImageScript.toString()).executeExpectSuccess();
   514                 }
   357             }
   515                 return buildMSI(params, wixVars, outdir);
   358             return buildMSI(params, wixVars, outdir);
   516             }
       
   517             return null;
       
   518         } catch (IOException ex) {
   359         } catch (IOException ex) {
   519             Log.verbose(ex);
   360             Log.verbose(ex);
   520             throw new PackagerException(ex);
   361             throw new PackagerException(ex);
   521         }
       
   522     }
       
   523 
       
   524     // name of post-image script
       
   525     private File getConfig_Script(Map<String, ? super Object> params) {
       
   526         return new File(CONFIG_ROOT.fetchFrom(params),
       
   527                 APP_NAME.fetchFrom(params) + "-post-image.wsf");
       
   528     }
       
   529 
       
   530     private void prepareBasicProjectConfig(
       
   531         Map<String, ? super Object> params) throws IOException {
       
   532 
       
   533         Path scriptPath = getConfig_Script(params).toPath();
       
   534 
       
   535         createResource(null, params)
       
   536                 .setCategory(I18N.getString("resource.post-install-script"))
       
   537                 .saveToFile(scriptPath);
       
   538     }
       
   539 
       
   540     private static String relativePath(File basedir, File file) {
       
   541         return file.getAbsolutePath().substring(
       
   542                 basedir.getAbsolutePath().length() + 1);
       
   543     }
       
   544 
       
   545     private AppImageFile appImageFile = null;
       
   546     private String[] getLaunchers( Map<String, ? super Object> params) {
       
   547         try {
       
   548             ArrayList<String> launchers = new ArrayList<String>();
       
   549             if (appImageFile == null) {
       
   550                 appImageFile = AppImageFile.load(
       
   551                         WIN_APP_IMAGE.fetchFrom(params).toPath());
       
   552             }
       
   553             launchers.add(appImageFile.getLauncherName());
       
   554             launchers.addAll(appImageFile.getAddLauncherNames());
       
   555             return launchers.toArray(new String[0]);
       
   556         } catch (IOException ioe) {
       
   557             Log.verbose(ioe.getMessage());
       
   558         }
       
   559         String [] launcherNames = new String [1];
       
   560         launcherNames[0] = APP_NAME.fetchFrom(params);
       
   561         return launcherNames;
       
   562     }
       
   563 
       
   564     private void prepareIconsFile(
       
   565             Map<String, ? super Object> params) throws IOException {
       
   566 
       
   567         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
       
   568 
       
   569         List<Map<String, ? super Object>> addLaunchers =
       
   570                 ADD_LAUNCHERS.fetchFrom(params);
       
   571 
       
   572         XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
       
   573         try (Writer w = new BufferedWriter(new FileWriter(new File(
       
   574                 CONFIG_ROOT.fetchFrom(params), "icons.wxi")))) {
       
   575             XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
       
   576 
       
   577             xml.writeStartDocument();
       
   578             xml.writeStartElement("Include");
       
   579 
       
   580             String[] launcherNames = getLaunchers(params);
       
   581 
       
   582             File[] icons = new File[launcherNames.length];
       
   583             for (int i=0; i<launcherNames.length; i++) {
       
   584                 icons[i] = new File(imageRootDir, launcherNames[i] + ".ico");
       
   585             }
       
   586 
       
   587             for (int i = 0; i < icons.length; i++) {
       
   588                 if (icons[i].exists()) {
       
   589                     String iconPath = icons[i].getAbsolutePath();
       
   590 
       
   591                     if (MENU_HINT.fetchFrom(params)) {
       
   592                         xml.writeStartElement("Icon");
       
   593                         xml.writeAttribute("Id", "StartMenuIcon.exe" + i);
       
   594                         xml.writeAttribute("SourceFile", iconPath);
       
   595                         xml.writeEndElement();
       
   596                     }
       
   597                     if (SHORTCUT_HINT.fetchFrom(params)) {
       
   598                         xml.writeStartElement("Icon");
       
   599                         xml.writeAttribute("Id", "DesktopIcon.exe" + i);
       
   600                         xml.writeAttribute("SourceFile", iconPath);
       
   601                         xml.writeEndElement();
       
   602                     }
       
   603                 }
       
   604             }
       
   605 
       
   606             xml.writeEndElement();
       
   607             xml.writeEndDocument();
       
   608             xml.flush();
       
   609             xml.close();
       
   610         } catch (XMLStreamException ex) {
       
   611             Log.verbose(ex);
       
   612             throw new IOException(ex);
       
   613         }
   362         }
   614     }
   363     }
   615 
   364 
   616     Map<String, String> prepareMainProjectFile(
   365     Map<String, String> prepareMainProjectFile(
   617             Map<String, ? super Object> params) throws IOException {
   366             Map<String, ? super Object> params) throws IOException {
   634 
   383 
   635         if (!UPGRADE_UUID.getIsDefaultValue()) {
   384         if (!UPGRADE_UUID.getIsDefaultValue()) {
   636             data.put("JpAllowDowngrades", "yes");
   385             data.put("JpAllowDowngrades", "yes");
   637         }
   386         }
   638 
   387 
   639         if (CAN_USE_WIX36.fetchFrom(params)) {
       
   640             data.put("JpWixVersion36OrNewer", "yes");
       
   641         }
       
   642 
       
   643         data.put("JpAppName", APP_NAME.fetchFrom(params));
   388         data.put("JpAppName", APP_NAME.fetchFrom(params));
   644         data.put("JpAppDescription", DESCRIPTION.fetchFrom(params));
   389         data.put("JpAppDescription", DESCRIPTION.fetchFrom(params));
   645         data.put("JpAppVendor", VENDOR.fetchFrom(params));
   390         data.put("JpAppVendor", VENDOR.fetchFrom(params));
   646         data.put("JpAppVersion", PRODUCT_VERSION.fetchFrom(params));
   391         data.put("JpAppVersion", PRODUCT_VERSION.fetchFrom(params));
   647 
   392 
   648         data.put("JpConfigDir",
   393         data.put("JpConfigDir",
   649                 CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
   394                 CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
   650 
       
   651         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
       
   652 
   395 
   653         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
   396         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
   654             data.put("JpIsSystemWide", "yes");
   397             data.put("JpIsSystemWide", "yes");
   655         }
   398         }
   656 
   399 
   687                     getConfig_ProjectFile(params).getAbsolutePath()));
   430                     getConfig_ProjectFile(params).getAbsolutePath()));
   688         }
   431         }
   689 
   432 
   690         return data;
   433         return data;
   691     }
   434     }
   692     private int id;
       
   693     private int compId;
       
   694     private final static String LAUNCHER_ID = "LauncherId";
       
   695 
       
   696     private void walkFileTree(Map<String, ? super Object> params,
       
   697             File root, PrintStream out, String prefix) {
       
   698         List<File> dirs = new ArrayList<>();
       
   699         List<File> files = new ArrayList<>();
       
   700 
       
   701         if (!root.isDirectory()) {
       
   702             throw new RuntimeException(
       
   703                     MessageFormat.format(
       
   704                             I18N.getString("error.cannot-walk-directory"),
       
   705                             root.getAbsolutePath()));
       
   706         }
       
   707 
       
   708         // sort to files and dirs
       
   709         File[] children = root.listFiles();
       
   710         if (children != null) {
       
   711             for (File f : children) {
       
   712                 if (f.isDirectory()) {
       
   713                     dirs.add(f);
       
   714                 } else {
       
   715                     files.add(f);
       
   716                 }
       
   717             }
       
   718         }
       
   719 
       
   720         // have files => need to output component
       
   721         out.println(prefix + " <Component Id=\"comp" + (compId++)
       
   722                 + "\" DiskId=\"1\""
       
   723                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
       
   724                 + " Win64=\"yes\""
       
   725                 + ">");
       
   726         out.println(prefix + "  <CreateFolder/>");
       
   727         out.println(prefix + "  <RemoveFolder Id=\"RemoveDir"
       
   728                 + (id++) + "\" On=\"uninstall\" />");
       
   729 
       
   730 
       
   731         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
       
   732 
       
   733         // Find out if we need to use registry. We need it if
       
   734         //  - we doing user level install as file can not serve as KeyPath
       
   735         //  - if we adding shortcut in this component
       
   736         boolean menuShortcut = MENU_HINT.fetchFrom(params);
       
   737         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
       
   738         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params) ||
       
   739                 menuShortcut || desktopShortcut;
       
   740 
       
   741         if (needRegistryKey) {
       
   742             // has to be under HKCU to make WiX happy
       
   743             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
       
   744                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
       
   745                     + APP_NAME.fetchFrom(params) + "\""
       
   746                     + (CAN_USE_WIX36.fetchFrom(params) ?
       
   747                     ">" : " Action=\"createAndRemoveOnUninstall\">"));
       
   748             out.println(prefix
       
   749                     + "     <RegistryValue Name=\"Version\" Value=\""
       
   750                     + VERSION.fetchFrom(params)
       
   751                     + "\" Type=\"string\" KeyPath=\"yes\"/>");
       
   752             out.println(prefix + "   </RegistryKey>");
       
   753         }
       
   754 
       
   755         String[] launcherNames = getLaunchers(params);
       
   756 
       
   757         File[] launcherFiles = new File[launcherNames.length];
       
   758         for (int i=0; i<launcherNames.length; i++) {
       
   759             launcherFiles[i] =
       
   760                     new File(imageRootDir, launcherNames[i] + ".exe");
       
   761         }
       
   762         Map<String, String> idToFileMap = new TreeMap<>();
       
   763         boolean launcherSet = false;
       
   764 
       
   765         for (File f : files) {
       
   766             boolean isMainLauncher =
       
   767                     launcherFiles.length > 0 && f.equals(launcherFiles[0]);
       
   768 
       
   769             launcherSet = launcherSet || isMainLauncher;
       
   770 
       
   771             String thisFileId = isMainLauncher ? LAUNCHER_ID : ("FileId" + (id++));
       
   772             idToFileMap.put(f.getName(), thisFileId);
       
   773 
       
   774             out.println(prefix + "   <File Id=\"" +
       
   775                     thisFileId + "\""
       
   776                     + " Name=\"" + f.getName() + "\" "
       
   777                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
       
   778                     + " ProcessorArchitecture=\"x64\"" + ">");
       
   779             if (isMainLauncher && desktopShortcut) {
       
   780                 out.println(prefix
       
   781                         + "  <Shortcut Id=\"desktopShortcut\" Directory="
       
   782                         + "\"DesktopFolder\""
       
   783                         + " Name=\"" + launcherNames[0]
       
   784                         + "\" WorkingDirectory=\"INSTALLDIR\""
       
   785                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe0\""
       
   786                         + " IconIndex=\"0\" />");
       
   787             }
       
   788             if (isMainLauncher && menuShortcut) {
       
   789                 out.println(prefix
       
   790                         + "     <Shortcut Id=\"ExeShortcut\" Directory="
       
   791                         + "\"ProgramMenuDir\""
       
   792                         + " Name=\"" + launcherNames[0]
       
   793                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe0\""
       
   794                         + " IconIndex=\"0\" />");
       
   795             }
       
   796 
       
   797             // any additional launchers
       
   798             for (int index = 1; index < launcherNames.length; index++ ) {
       
   799 
       
   800                 if (f.equals(launcherFiles[index])) {
       
   801                     if (desktopShortcut) {
       
   802                         out.println(prefix
       
   803                                 + "  <Shortcut Id=\"desktopShortcut"
       
   804                                 + index + "\" Directory=\"DesktopFolder\""
       
   805                                 + " Name=\"" + launcherNames[index]
       
   806                                 + "\" WorkingDirectory=\"INSTALLDIR\""
       
   807                                 + " Advertise=\"no\" Icon=\"DesktopIcon.exe"
       
   808                                 + index + "\""
       
   809                                 + " IconIndex=\"0\" />");
       
   810                     }
       
   811                     if (menuShortcut) {
       
   812                         out.println(prefix
       
   813                             + "     <Shortcut Id=\"ExeShortcut"
       
   814                             + index + "\" Directory=\"ProgramMenuDir\""
       
   815                             + " Name=\"" + launcherNames[index]
       
   816                             + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe"
       
   817                             + index + "\""
       
   818                             + " IconIndex=\"0\" />");
       
   819                     }
       
   820                 }
       
   821             }
       
   822             out.println(prefix + "   </File>");
       
   823         }
       
   824 
       
   825         if (launcherSet) {
       
   826             List<Map<String, ? super Object>> fileAssociations =
       
   827                 FILE_ASSOCIATIONS.fetchFrom(params);
       
   828             Set<String> defaultedMimes = new TreeSet<>();
       
   829             for (Map<String, ? super Object> fa : fileAssociations) {
       
   830                 String description = FA_DESCRIPTION.fetchFrom(fa);
       
   831                 List<String> extensions = FA_EXTENSIONS.fetchFrom(fa);
       
   832                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa);
       
   833                 File icon = FA_ICON.fetchFrom(fa);
       
   834 
       
   835                 String mime = (mimeTypes == null ||
       
   836                     mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
       
   837 
       
   838                 String entryName = APP_REGISTRY_NAME.fetchFrom(params) + "File";
       
   839 
       
   840                 if (extensions == null) {
       
   841                     Log.verbose(I18N.getString(
       
   842                           "message.creating-association-with-null-extension"));
       
   843 
       
   844                     out.print(prefix + "   <ProgId Id='" + entryName
       
   845                             + "' Description='" + description + "'");
       
   846                     if (icon != null && icon.exists()) {
       
   847                         out.print(" Icon='" + idToFileMap.get(icon.getName())
       
   848                                 + "' IconIndex='0'");
       
   849                     }
       
   850                     out.println(" />");
       
   851                 } else {
       
   852                     for (String ext : extensions) {
       
   853 
       
   854                         entryName = ext.toUpperCase() + "File";
       
   855 
       
   856                         out.print(prefix + "   <ProgId Id='" + entryName
       
   857                                 + "' Description='" + description + "'");
       
   858                         if (icon != null && icon.exists()) {
       
   859                             out.print(" Icon='"
       
   860                                     + idToFileMap.get(icon.getName())
       
   861                                     + "' IconIndex='0'");
       
   862                         }
       
   863                         out.println(">");
       
   864 
       
   865                         out.print(prefix + "    <Extension Id='"
       
   866                                 + ext + "' Advertise='no'");
       
   867                         if (mime == null) {
       
   868                             out.println(">");
       
   869                         } else {
       
   870                             out.println(" ContentType='" + mime + "'>");
       
   871                             if (!defaultedMimes.contains(mime)) {
       
   872                                 out.println(prefix
       
   873                                         + "      <MIME ContentType='"
       
   874                                         + mime + "' Default='yes' />");
       
   875                                 defaultedMimes.add(mime);
       
   876                             }
       
   877                         }
       
   878                         out.println(prefix
       
   879                                 + "      <Verb Id='open' Command='Open' "
       
   880                                 + "TargetFile='" + LAUNCHER_ID
       
   881                                 + "' Argument='\"%1\"' />");
       
   882                         out.println(prefix + "    </Extension>");
       
   883                         out.println(prefix + "   </ProgId>");
       
   884                     }
       
   885                 }
       
   886             }
       
   887         }
       
   888 
       
   889         out.println(prefix + " </Component>");
       
   890 
       
   891         for (File d : dirs) {
       
   892             out.println(prefix + " <Directory Id=\"dirid" + (id++)
       
   893                     + "\" Name=\"" + d.getName() + "\">");
       
   894             walkFileTree(params, d, out, prefix + " ");
       
   895             out.println(prefix + " </Directory>");
       
   896         }
       
   897     }
       
   898 
       
   899     void prepareContentList(Map<String, ? super Object> params)
       
   900             throws FileNotFoundException {
       
   901         File f = new File(
       
   902                 CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
       
   903 
       
   904         try (PrintStream out = new PrintStream(f)) {
       
   905 
       
   906             // opening
       
   907             out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
       
   908             out.println("<Include>");
       
   909 
       
   910             out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
       
   911             if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
       
   912                 // install to programfiles
       
   913                 out.println("  <Directory Id=\"ProgramFiles64Folder\" "
       
   914                             + "Name=\"PFiles\">");
       
   915             } else {
       
   916                 // install to user folder
       
   917                 out.println(
       
   918                     "  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
       
   919             }
       
   920 
       
   921             // reset counters
       
   922             compId = 0;
       
   923             id = 0;
       
   924 
       
   925             // We should get valid folder or subfolders
       
   926             String installDir = WINDOWS_INSTALL_DIR.fetchFrom(params);
       
   927             String [] installDirs = installDir.split(Pattern.quote("\\"));
       
   928             for (int i = 0; i < (installDirs.length - 1); i++)  {
       
   929                 out.println("   <Directory Id=\"SUBDIR" + i + "\" Name=\""
       
   930                     + installDirs[i] + "\">");
       
   931                 if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
       
   932                     out.println("   <Component Id=\"comp" + (compId++)
       
   933                         + "\" DiskId=\"1\""
       
   934                         + " Guid=\"" + UUID.randomUUID().toString() + "\""
       
   935                         + " Win64=\"yes\""
       
   936                         + ">");
       
   937                     out.println("<CreateFolder/>");
       
   938                     // has to be under HKCU to make WiX happy
       
   939                     out.println("    <RegistryKey Root=\"HKCU\" "
       
   940                         + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
       
   941                         + APP_NAME.fetchFrom(params) + "\""
       
   942                         + (CAN_USE_WIX36.fetchFrom(params) ?
       
   943                         ">" : " Action=\"createAndRemoveOnUninstall\">"));
       
   944                     out.println("     <RegistryValue Name=\"Version\" Value=\""
       
   945                         + VERSION.fetchFrom(params)
       
   946                         + "\" Type=\"string\" KeyPath=\"yes\"/>");
       
   947                     out.println("   </RegistryKey>");
       
   948                     out.println("   <RemoveFolder Id=\"RemoveDir"
       
   949                         + (id++) + "\" Directory=\"SUBDIR" + i
       
   950                         + "\" On=\"uninstall\" />");
       
   951                     out.println("</Component>");
       
   952                 }
       
   953             }
       
   954 
       
   955             out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
       
   956                     + installDirs[installDirs.length - 1] + "\">");
       
   957 
       
   958             // dynamic part
       
   959             walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
       
   960 
       
   961             // closing
       
   962             for (int i = 0; i < installDirs.length; i++)  {
       
   963                 out.println("   </Directory>");
       
   964             }
       
   965             out.println("  </Directory>");
       
   966 
       
   967             // for shortcuts
       
   968             if (SHORTCUT_HINT.fetchFrom(params)) {
       
   969                 out.println("  <Directory Id=\"DesktopFolder\" />");
       
   970             }
       
   971             if (MENU_HINT.fetchFrom(params)) {
       
   972                 out.println("  <Directory Id=\"ProgramMenuFolder\">");
       
   973                 out.println("    <Directory Id=\"ProgramMenuDir\" Name=\""
       
   974                         + MENU_GROUP.fetchFrom(params) + "\">");
       
   975                 out.println("      <Component Id=\"comp" + (compId++) + "\""
       
   976                         + " Guid=\"" + UUID.randomUUID().toString() + "\""
       
   977                         + " Win64=\"yes\""
       
   978                         + ">");
       
   979                 out.println("        <RemoveFolder Id=\"ProgramMenuDir\" "
       
   980                         + "On=\"uninstall\" />");
       
   981                 // This has to be under HKCU to make WiX happy.
       
   982                 // There are numberous discussions on this amoung WiX users
       
   983                 // (if user A installs and user B uninstalls key is left behind)
       
   984                 // there are suggested workarounds but none are appealing.
       
   985                 // Leave it for now
       
   986                 out.println(
       
   987                         "         <RegistryValue Root=\"HKCU\" Key=\"Software\\"
       
   988                         + VENDOR.fetchFrom(params) + "\\"
       
   989                         + APP_NAME.fetchFrom(params)
       
   990                         + "\" Type=\"string\" Value=\"\" />");
       
   991                 out.println("      </Component>");
       
   992                 out.println("    </Directory>");
       
   993                 out.println(" </Directory>");
       
   994             }
       
   995 
       
   996             out.println(" </Directory>");
       
   997 
       
   998             out.println(" <Feature Id=\"DefaultFeature\" "
       
   999                     + "Title=\"Main Feature\" Level=\"1\">");
       
  1000             for (int j = 0; j < compId; j++) {
       
  1001                 out.println("    <ComponentRef Id=\"comp" + j + "\" />");
       
  1002             }
       
  1003             // component is defined in the main.wsx
       
  1004             out.println(
       
  1005                     "    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
       
  1006             out.println(" </Feature>");
       
  1007             out.println("</Include>");
       
  1008 
       
  1009         }
       
  1010     }
       
  1011 
   435 
  1012     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
   436     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
  1013         return new File(CONFIG_ROOT.fetchFrom(params),
   437         return new File(CONFIG_ROOT.fetchFrom(params), "main.wxs");
  1014                 APP_NAME.fetchFrom(params) + ".wxs");
   438     }
  1015     }
       
  1016 
       
  1017     private Map<String, String> prepareWiXConfig(
       
  1018             Map<String, ? super Object> params) throws IOException {
       
  1019         prepareContentList(params);
       
  1020         prepareIconsFile(params);
       
  1021         return prepareMainProjectFile(params);
       
  1022     }
       
  1023 
       
  1024     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
       
  1025 
   439 
  1026     private File buildMSI(Map<String, ? super Object> params,
   440     private File buildMSI(Map<String, ? super Object> params,
  1027             Map<String, String> wixVars, File outdir)
   441             Map<String, String> wixVars, File outdir)
  1028             throws IOException {
   442             throws IOException {
  1029         File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp");
   443 
  1030         File candleOut = new File(
       
  1031                 tmpDir, APP_NAME.fetchFrom(params) + ".wixobj");
       
  1032         File msiOut = new File(
   444         File msiOut = new File(
  1033                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
   445                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
  1034 
   446 
  1035         Log.verbose(MessageFormat.format(I18N.getString(
   447         Log.verbose(MessageFormat.format(I18N.getString(
  1036                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
   448                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
  1037 
   449 
  1038         msiOut.getParentFile().mkdirs();
   450         WixPipeline wixPipeline = new WixPipeline()
  1039 
   451         .setToolset(wixToolset.entrySet().stream().collect(
  1040         List<String> commandLine = new ArrayList<>(Arrays.asList(
   452                 Collectors.toMap(
  1041                 getCandlePath(),
   453                         entry -> entry.getKey(),
  1042                 "-nologo",
   454                         entry -> entry.getValue().path)))
  1043                 getConfig_ProjectFile(params).getAbsolutePath(),
   455         .setWixObjDir(TEMP_ROOT.fetchFrom(params).toPath().resolve("wixobj"))
  1044                 "-ext", "WixUtilExtension",
   456         .setWorkDir(WIN_APP_IMAGE.fetchFrom(params).toPath())
  1045                 "-out", candleOut.getAbsolutePath()));
   457         .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("main.wxs"), wixVars)
  1046         for(Map.Entry<String, String> wixVar: wixVars.entrySet()) {
   458         .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("bundle.wxf"), null);
  1047             String v = "-d" + wixVar.getKey() + "=" + wixVar.getValue();
       
  1048             commandLine.add(v);
       
  1049         }
       
  1050         ProcessBuilder pb = new ProcessBuilder(commandLine);
       
  1051         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
       
  1052         IOUtils.exec(pb);
       
  1053 
   459 
  1054         Log.verbose(MessageFormat.format(I18N.getString(
   460         Log.verbose(MessageFormat.format(I18N.getString(
  1055                 "message.generating-msi"), msiOut.getAbsolutePath()));
   461                 "message.generating-msi"), msiOut.getAbsolutePath()));
  1056 
   462 
  1057         boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
   463         boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
  1058         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
   464         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
  1059 
   465 
  1060         commandLine = new ArrayList<>();
   466         List<String> lightArgs = new ArrayList<>();
  1061 
   467 
  1062         commandLine.add(getLightPath());
       
  1063 
       
  1064         commandLine.add("-nologo");
       
  1065         commandLine.add("-spdb");
       
  1066         if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
   468         if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
  1067             commandLine.add("-sice:ICE91");
   469             wixPipeline.addLightOptions("-sice:ICE91");
  1068         }
   470         }
  1069         commandLine.add(candleOut.getAbsolutePath());
       
  1070         commandLine.add("-ext");
       
  1071         commandLine.add("WixUtilExtension");
       
  1072         if (enableLicenseUI || enableInstalldirUI) {
   471         if (enableLicenseUI || enableInstalldirUI) {
  1073             commandLine.add("-ext");
   472             wixPipeline.addLightOptions("-ext", "WixUIExtension");
  1074             commandLine.add("WixUIExtension");
   473         }
  1075         }
   474 
  1076 
   475         wixPipeline.addLightOptions("-loc",
  1077         commandLine.add("-loc");
   476                 CONFIG_ROOT.fetchFrom(params).toPath().resolve(I18N.getString(
  1078         commandLine.add(new File(CONFIG_ROOT.fetchFrom(params), I18N.getString(
   477                         "resource.wxl-file-name")).toAbsolutePath().toString());
  1079                 "resource.wxl-file-name")).getAbsolutePath());
       
  1080 
   478 
  1081         // Only needed if we using CA dll, so Wix can find it
   479         // Only needed if we using CA dll, so Wix can find it
  1082         if (enableInstalldirUI) {
   480         if (enableInstalldirUI) {
  1083             commandLine.add("-b");
   481             wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
  1084             commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
   482         }
  1085         }
   483 
  1086 
   484         wixPipeline.buildMsi(msiOut.toPath().toAbsolutePath());
  1087         commandLine.add("-out");
       
  1088         commandLine.add(msiOut.getAbsolutePath());
       
  1089 
       
  1090         // create .msi
       
  1091         pb = new ProcessBuilder(commandLine);
       
  1092 
       
  1093         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
       
  1094         IOUtils.exec(pb);
       
  1095 
       
  1096         candleOut.delete();
       
  1097         IOUtils.deleteRecursive(tmpDir);
       
  1098 
   485 
  1099         return msiOut;
   486         return msiOut;
  1100     }
   487     }
  1101 
   488 
  1102     public static void ensureByMutationFileIsRTF(File f) {
   489     public static void ensureByMutationFileIsRTF(File f) {
  1168             Log.verbose(e);
   555             Log.verbose(e);
  1169         }
   556         }
  1170 
   557 
  1171     }
   558     }
  1172 
   559 
       
   560     private Map<WixTool, WixTool.ToolInfo> wixToolset;
       
   561     private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder();
       
   562 
  1173 }
   563 }