modules/fxpackager/src/main/java/com/sun/javafx/tools/packager/bundlers/WinMsiBundler.java

Print this page

        

@@ -36,89 +36,85 @@
 import java.text.MessageFormat;
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static com.oracle.bundlers.StandardBundlerParam.DESCRIPTION;
 import static com.oracle.bundlers.windows.WindowsBundlerParam.*;
 
 public class WinMsiBundler  extends AbstractBundler {
 
     private static final ResourceBundle I18N =
             ResourceBundle.getBundle("com.oracle.bundlers.windows.WinMsiBundler");
 
     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
             I18N.getString("param.app-bundler.name"),
             I18N.getString("param.app-bundler.description"),
-            "winAppBundler", //KEY
+            "win.app.bundler",
             WinAppBundler.class, null, params -> new WinAppBundler(), false, null);
 
     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 = new WindowsBundlerParam<>(
             I18N.getString("param.can-use-wix36.name"),
             I18N.getString("param.can-use-wix36.description"),
-            "canUseWix36", //KEY
-            Boolean.class, null, params -> false, false, Boolean::valueOf);
-
-    public static final BundlerParamInfo<File> OUT_DIR = new WindowsBundlerParam<>(
-            I18N.getString("param.out-dir.name"),
-            I18N.getString("param.out-dir.description"),
-            "outDir", //KEY
-            File.class, null, params -> null, false, s -> null);
+            "win.msi.canUseWix36",
+            Boolean.class, null, params -> false, false, (s, p) -> Boolean.valueOf(s));
 
     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
             I18N.getString("param.config-root.name"),
             I18N.getString("param.config-root.description"),
-            "configRoot", //KEY
+            "configRoot",
             File.class, null,params -> {
-                File imagesRoot = new File(StandardBundlerParam.BUILD_ROOT.fetchFrom(params), "windows");
+                File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
                 imagesRoot.mkdirs();
                 return imagesRoot;
-            }, false, s -> null);
+            }, false, (s, p) -> null);
 
-    public static final BundlerParamInfo<File> IMAGE_DIR = new WindowsBundlerParam<>(
+    public static final BundlerParamInfo<File> MSI_IMAGE_DIR = new WindowsBundlerParam<>(
             I18N.getString("param.image-dir.name"),
             I18N.getString("param.image-dir.description"),
-            "imageDir", //KEY
+            "win.msi.imageDir",
             File.class, null, params -> {
                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
-                return new File(imagesRoot, "win-msi");
-            }, false, s -> null);
+                if (!imagesRoot.exists()) imagesRoot.mkdirs();
+                return new File(imagesRoot, "win-msi.image");
+            }, false, (s, p) -> null);
 
-    public static final BundlerParamInfo<File> APP_DIR = new WindowsBundlerParam<>(
+    public static final BundlerParamInfo<File> WIN_APP_IMAGE = new WindowsBundlerParam<>(
             I18N.getString("param.app-dir.name"),
             I18N.getString("param.app-dir.description"),
-            "appDir",
-            File.class, null, null, false, s -> null);
+            "win.app.image",
+            File.class, null, null, false, (s, p) -> null);
 
     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
             new StandardBundlerParam<>(
                     I18N.getString("param.system-wide.name"),
                     I18N.getString("param.system-wide.description"),
-                    "winmsi" + BundleParams.PARAM_SYSTEM_WIDE, //KEY
+                    "win.msi." + BundleParams.PARAM_SYSTEM_WIDE,
                     Boolean.class,
                     new String[] {BundleParams.PARAM_SYSTEM_WIDE},
                     params -> true, // MSIs default to system wide
                     false,
-                    s -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
+                    (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
             );
 
 
     public static final BundlerParamInfo<UUID> UPGRADE_UUID = new WindowsBundlerParam<>(
             I18N.getString("param.upgrade-uuid.name"),
             I18N.getString("param.upgrade-uuid.description"),
-            "upgradeUUID", //KEY
+            "win.msi.upgradeUUID",
             UUID.class, null, params -> UUID.randomUUID(), // TODO check to see if identifier is a valid UUID during default 
-            false, UUID::fromString);
+            false, (s, p) -> UUID.fromString(s));
 
     private static final String TOOL_CANDLE = "candle.exe";
     private static final String TOOL_LIGHT = "light.exe";
     // autodetect just v3.7 and v3.8
     private static final String AUTODETECT_DIRS = ";C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;C:\\Program Files\\WiX Toolset v3.8\\bin;C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;C:\\Program Files\\WiX Toolset v3.7\\bin";
 
     public static final BundlerParamInfo<String> TOOL_CANDLE_EXECUTABLE = new WindowsBundlerParam<>(
             I18N.getString("param.candle-path.name"),
             I18N.getString("param.candle-path.description"),
-            "win.candle.exe", //KEY
+            "win.msi.candle.exe",
             String.class, null, params -> {
                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
                     File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
                     if (f.isFile()) {
                         return f.toString();

@@ -128,11 +124,11 @@
             }, false, null);
 
     public static final BundlerParamInfo<String> TOOL_LIGHT_EXECUTABLE = new WindowsBundlerParam<>(
             I18N.getString("param.light-path.name"),
             I18N.getString("param.light-path.descrption"),
-            "win.light.exe", //KEY
+            "win.msi.light.exe",
             String.class, null, params -> {
                 for (String dirString : (System.getenv("PATH") + AUTODETECT_DIRS).split(";")) {
                     File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
                     if (f.isFile()) {
                         return f.toString();

@@ -157,11 +153,11 @@
         return I18N.getString("bundler.description");
     }
 
     @Override
     public String getID() {
-        return "msi"; //KEY
+        return "msi";
     }
 
     @Override
     public BundleType getBundleType() {
         return BundleType.INSTALLER;

@@ -176,16 +172,16 @@
     }
 
     public static Collection<BundlerParamInfo<?>> getMsiBundleParameters() {
         return Arrays.asList(
                 APP_BUNDLER,
-                APP_DIR,
+                WIN_APP_IMAGE,
                 BUILD_ROOT,
                 CAN_USE_WIX36,
                 //CONFIG_ROOT, // duplicate from getAppBundleParameters
                 DESCRIPTION,
-                IMAGE_DIR,
+                MSI_IMAGE_DIR,
                 IMAGES_ROOT,
                 MENU_GROUP,
                 MENU_HINT,
                 MSI_SYSTEM_WIDE,
                 SHORTCUT_HINT,

@@ -241,10 +237,11 @@
         }
     }
 
     @Override
     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
+        try {
         if (p == null) throw new ConfigException(
                 I18N.getString("error.parameters-null"), 
                 I18N.getString("error.parameters-null.advice"));
 
         //run basic validation to ensure requirements are met

@@ -286,10 +283,13 @@
                     MessageFormat.format(I18N.getString("error.version-string-wrong-format"), version),
                     I18N.getString("error.version-string-wrong-format.advice"));
         }
 
         return true;
+        } catch (RuntimeException re) {
+            throw new ConfigException(re);
+        }
     }
 
     //http://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
     //The format of the string is as follows:
     //    major.minor.build

@@ -336,19 +336,29 @@
 
         return true;
     }
 
     private boolean prepareProto(Map<String, ? super Object> p) {
-        File bundleRoot = IMAGE_DIR.fetchFrom(p);
+        File bundleRoot = MSI_IMAGE_DIR.fetchFrom(p);
         File appDir = APP_BUNDLER.fetchFrom(p).doBundle(p, bundleRoot, true);
-        p.put(APP_DIR.getID(), appDir);
+        p.put(WIN_APP_IMAGE.getID(), appDir);
         return appDir != null;
     }
 
     public File bundle(Map<String, ? super Object> p, File outdir) {
-        File appDir = APP_DIR.fetchFrom(p);
-        File imageDir = IMAGE_DIR.fetchFrom(p);
+        // validate we have valid tools before continuing
+        String light = TOOL_LIGHT_EXECUTABLE.fetchFrom(p);
+        String candle = TOOL_CANDLE_EXECUTABLE.fetchFrom(p);
+        if (light == null || !new File(light).isFile() ||
+            candle == null || !new File(candle).isFile()) {
+            Log.info(I18N.getString("error.no-wix-tools"));
+            Log.info(MessageFormat.format(I18N.getString("message.light-file-string"), light));
+            Log.info(MessageFormat.format(I18N.getString("message.candle-file-string"), candle));
+            return null;
+        }
+
+        File imageDir = MSI_IMAGE_DIR.fetchFrom(p);
         try {
             imageDir.mkdirs();
 
             boolean menuShortcut = MENU_HINT.fetchFrom(p);
             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);

@@ -369,11 +379,11 @@
 
                     // for now we replicate it
                     File configScript = new File(imageDir, configScriptSrc.getName());
                     IOUtils.copyFile(configScriptSrc, configScript);
                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
-                    IOUtils.run("wscript", configScript, verbose);
+                    IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
                 }
                 return buildMSI(p, outdir);
             }
             return null;
         } catch (IOException ex) {

@@ -384,11 +394,11 @@
                 if (imageDir != null && !Log.isDebug()) {
                     IOUtils.deleteRecursive(imageDir);
                 } else if (imageDir != null) {
                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
                 }
-                if (verbose) {
+                if (VERBOSE.fetchFrom(p)) {
                     Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
                 } else {
                     cleanupConfigFiles(p);
                 }
             } catch (FileNotFoundException ex) {

@@ -420,11 +430,12 @@
 
     private boolean prepareBasicProjectConfig(Map<String, ? super Object> params) throws IOException {
         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
                 I18N.getString("resource.post-install-script"),
                 (String) null,
-                getConfig_Script(params));
+                getConfig_Script(params),
+                VERBOSE.fetchFrom(params));
         return true;
     }
 
     private String relativePath(File basedir, File file) {
         return file.getAbsolutePath().substring(

@@ -449,11 +460,11 @@
         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
 
         //WinAppBundler will add application folder again => step out
-        File imageRootDir = APP_DIR.fetchFrom(params);
+        File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
         File launcher = WinAppBundler.getLauncher(
                 imageRootDir.getParentFile(), params);
 
         String launcherPath = relativePath(imageRootDir, launcher);
         data.put("APPLICATION_LAUNCHER", launcherPath);

@@ -486,17 +497,18 @@
 
         Writer w = new BufferedWriter(new FileWriter(getConfig_ProjectFile(params)));
         w.write(preprocessTextResource(
                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ProjectFile(params).getName(),
                 I18N.getString("resource.wix-config-file"), 
-                MSI_PROJECT_TEMPLATE, data));
+                MSI_PROJECT_TEMPLATE, data, VERBOSE.fetchFrom(params)));
         w.close();
         return true;
     }
     private int id;
     private int compId;
     private final static String LAUNCHER_ID = "LauncherId";
+    private final static String LAUNCHER_SVC_ID = "LauncherSvcId";
 
     private void walkFileTree(Map<String, ? super Object> params, File root, PrintStream out, String prefix) {
         List<File> dirs = new ArrayList<>();
         List<File> files = new ArrayList<>();
 

@@ -523,17 +535,21 @@
                 + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
         out.println("  <CreateFolder/>");
         out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
 
         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
-        File imageRootDir = APP_DIR.fetchFrom(params);
+        File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
         File launcherFile = WinAppBundler.getLauncher(
                 /* Step up as WinAppBundler will add app folder */
                 imageRootDir.getParentFile(), params);
+        File launcherSvcFile = WinAppBundler.getLauncherSvc(
+                                imageRootDir.getParentFile(), params);
+
         //Find out if we need to use registry. We need it if
         //  - we doing user level install as file can not serve as KeyPath
         //  - if we adding shortcut in this component
+
         for (File f: files) {
             boolean isLauncher = f.equals(launcherFile);
             if (isLauncher) {
                 needRegistryKey = true;
             }

@@ -553,11 +569,19 @@
 
         boolean menuShortcut = MENU_HINT.fetchFrom(params);
         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
         for (File f : files) {
             boolean isLauncher = f.equals(launcherFile);
+            boolean isLauncherSvc = f.equals(launcherSvcFile);
+
+            // skip executable for service, will be covered by new component entry
+            if (isLauncherSvc) {
+                continue;
+            }
+
             boolean doShortcuts = isLauncher && (menuShortcut || desktopShortcut);
+
             out.println(prefix + "   <File Id=\"" +
                     (isLauncher ? LAUNCHER_ID : ("FileId" + (id++))) + "\""
                     + " Name=\"" + f.getName() + "\" "
                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
                     + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "") + ">");

@@ -573,10 +597,56 @@
             }
             out.println(prefix + "   </File>");
         }
         out.println(prefix + " </Component>");
 
+        // Two components cannot share the same key path value.
+        // We already have HKCU created with key path set and
+        // we need to create separate component for ServiceInstall element
+        // to ensure that key path is also set to the service executable.
+        //
+        // http://wixtoolset.org/documentation/manual/v3/xsd/wix/serviceinstall.html
+
+        boolean needServiceEntries = false;
+        for (File f: files) {
+            boolean isLauncherSvc = f.equals(launcherSvcFile);
+            if (isLauncherSvc && SERVICE_HINT.fetchFrom(params)) {
+                needServiceEntries = true;
+            }
+        }
+
+        if (needServiceEntries) {
+            out.println(prefix + " <Component Id=\"comp" + (compId++) + "\" DiskId=\"1\""
+                    + " Guid=\"" + UUID.randomUUID().toString() + "\""
+                    + (BIT_ARCH_64.fetchFrom(params) ? " Win64=\"yes\"" : "") + ">");
+            out.println("  <CreateFolder/>");
+            out.println("  <RemoveFolder Id=\"RemoveDir" + (id++) + "\" On=\"uninstall\" />");
+
+            out.println(prefix + "   <File Id=\"" + LAUNCHER_SVC_ID + "\""
+                    + " Name=\"" + launcherSvcFile.getName() + "\" "
+                    + " Source=\"" + relativePath(imageRootDir, launcherSvcFile) + "\""
+                    + (BIT_ARCH_64.fetchFrom(params) ? " ProcessorArchitecture=\"x64\"" : "")
+                    + " KeyPath=\"yes\">");
+            out.println(prefix + "   </File>");
+            out.println(prefix + "   <ServiceInstall Id=\"" + WinAppBundler.getAppName(params) + "\""
+                    + " Name=\"" + WinAppBundler.getAppName(params) + "\""
+                    + " Description=\"" + DESCRIPTION.fetchFrom(params) + "\""
+                    + " ErrorControl=\"normal\""
+                    + " Start=\"" + (RUN_AT_STARTUP.fetchFrom(params) ? "auto" : "demand") + "\""
+                    + " Type=\"ownProcess\" Vital=\"yes\" Account=\"LocalSystem\""
+                    + " Arguments=\"-mainExe " + launcherFile.getName() + "\"/>");
+
+            out.println(prefix + "   <ServiceControl Id=\""+ WinAppBundler.getAppName(params) + "\""
+                    + " Name=\"" + WinAppBundler.getAppName(params) + "\""
+                    + (START_ON_INSTALL.fetchFrom(params) ? " Start=\"install\"" : "")
+                    + (STOP_ON_UNINSTALL.fetchFrom(params) ? " Stop=\"uninstall\"" : "")
+                    + " Remove=\"uninstall\""
+                    + " Wait=\"yes\" />");
+
+            out.println(prefix + " </Component>");
+        }
+
         for (File d : dirs) {
             out.println(prefix + " <Directory Id=\"dirid" + (id++)
                     + "\" Name=\"" + d.getName() + "\">");
             walkFileTree(params, d, out, prefix + " ");
             out.println(prefix + " </Directory>");

@@ -615,11 +685,11 @@
                 + WinAppBundler.getAppName(params) + "\">");
 
         //dynamic part
         id = 0;
         compId = 0; //reset counters
-        walkFileTree(params, APP_DIR.fetchFrom(params), out, "    ");
+        walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
 
         //closing
         out.println("   </Directory>");
         out.println("  </Directory>");
 

@@ -688,12 +758,12 @@
                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
                 "-nologo",
                 getConfig_ProjectFile(params).getAbsolutePath(),
                 "-ext", "WixUtilExtension",
                 "-out", candleOut.getAbsolutePath());
-        pb = pb.directory(APP_DIR.fetchFrom(params));
-        IOUtils.exec(pb, verbose);
+        pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
+        IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 
         Log.verbose(MessageFormat.format(I18N.getString("message.generating-msi"), msiOut.getAbsolutePath()));
 
         //create .msi
         pb = new ProcessBuilder(

@@ -702,12 +772,12 @@
                 "-spdb",
                 "-sice:60", //ignore warnings due to "missing launcguage info" (ICE60)
                 candleOut.getAbsolutePath(),
                 "-ext", "WixUtilExtension",
                 "-out", msiOut.getAbsolutePath());
-        pb = pb.directory(APP_DIR.fetchFrom(params));
-        IOUtils.exec(pb, verbose);
+        pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
+        IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 
         candleOut.delete();
         IOUtils.deleteRecursive(tmpDir);
 
         return msiOut;