1 /*
   2  * Copyright (c) 2012, 2014, 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 
  26 package com.sun.javafx.tools.packager.bundlers;
  27 
  28 import com.oracle.bundlers.AbstractBundler;
  29 import com.oracle.bundlers.BundlerParamInfo;
  30 import com.oracle.bundlers.StandardBundlerParam;
  31 import com.oracle.bundlers.windows.WindowsBundlerParam;
  32 import com.sun.javafx.tools.packager.Log;
  33 import com.sun.javafx.tools.resource.windows.WinResources;
  34 
  35 import java.io.File;
  36 import java.io.FileNotFoundException;
  37 import java.io.IOException;
  38 import java.io.PrintStream;
  39 import java.net.MalformedURLException;
  40 import java.net.URL;
  41 import java.text.MessageFormat;
  42 import java.util.*;
  43 
  44 import static com.oracle.bundlers.JreUtils.*;
  45 import static com.oracle.bundlers.StandardBundlerParam.*;
  46 import static com.oracle.bundlers.windows.WindowsBundlerParam.BIT_ARCH_64;
  47 import static com.oracle.bundlers.windows.WindowsBundlerParam.BIT_ARCH_64_RUNTIME;
  48 
  49 public class WinAppBundler extends AbstractBundler {
  50 
  51     private static final ResourceBundle I18N = 
  52             ResourceBundle.getBundle("com.oracle.bundlers.windows.WinAppBundler");
  53 
  54     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  55             I18N.getString("param.config-root.name"),
  56             I18N.getString("param.config-root.description"), 
  57             "configRoot",
  58             File.class, null, params -> {
  59                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  60                 imagesRoot.mkdirs();
  61                 return imagesRoot;
  62             }, false, (s, p) -> null);
  63 
  64     //Subsetting of JRE is restricted.
  65     //JRE README defines what is allowed to strip:
  66     //   http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA
  67     public static final BundlerParamInfo<Rule[]> WIN_JRE_RULES = new StandardBundlerParam<>(
  68             "",
  69             "",
  70             ".win.runtime.rules",
  71             Rule[].class,
  72             null,
  73             params -> new Rule[]{
  74                 Rule.prefixNeg("\\bin\\new_plugin"),
  75                 Rule.prefixNeg("\\lib\\deploy"),
  76                 Rule.suffixNeg(".pdb"),
  77                 Rule.suffixNeg(".map"),
  78                 Rule.suffixNeg("axbridge.dll"),
  79                 Rule.suffixNeg("eula.dll"),
  80                 Rule.substrNeg("javacpl"),
  81                 Rule.suffixNeg("wsdetect.dll"),
  82                 Rule.substrNeg("eployjava1.dll"), //NP and IE versions
  83                 Rule.substrNeg("bin\\jp2"),
  84                 Rule.substrNeg("bin\\jpi"),
  85                 //Rule.suffixNeg("lib\\ext"), //need some of jars there for https to work
  86                 Rule.suffixNeg("ssv.dll"),
  87                 Rule.substrNeg("npjpi"),
  88                 Rule.substrNeg("npoji"),
  89                 Rule.suffixNeg(".exe"),
  90                 //keep core deploy files as JavaFX APIs use them
  91                 //Rule.suffixNeg("deploy.dll"),
  92                 Rule.suffixNeg("deploy.jar"),
  93                 //Rule.suffixNeg("javaws.jar"),
  94                 //Rule.suffixNeg("plugin.jar"),
  95                 Rule.suffix(".jar")
  96             },
  97             false,
  98             (s, p) -> null
  99     );
 100 
 101     public static final BundlerParamInfo<RelativeFileSet> WIN_RUNTIME = new StandardBundlerParam<>(
 102             RUNTIME.getName(),
 103             RUNTIME.getDescription(),
 104             RUNTIME.getID(),
 105             RelativeFileSet.class,
 106             null,
 107             params -> extractJreAsRelativeFileSet(System.getProperty("java.home"),
 108                     WIN_JRE_RULES.fetchFrom(params)),
 109             false,
 110             (s, p) -> extractJreAsRelativeFileSet(s,
 111                     WIN_JRE_RULES.fetchFrom(p))
 112     );
 113 
 114     private final static String EXECUTABLE_NAME = "WinLauncher.exe";
 115     private final static String EXECUTABLE_SVC_NAME = "WinLauncherSvc.exe";
 116 
 117     private static final String TOOL_ICON_SWAP="IconSwap.exe";
 118 
 119     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new WindowsBundlerParam<>(
 120             I18N.getString("param.raw-executable-url.name"),
 121             I18N.getString("param.raw-executable-url.description"),
 122             "win.launcher.url",
 123             URL.class, null, params -> WinResources.class.getResource(EXECUTABLE_NAME), 
 124             false, (s, p) -> {
 125                 try {
 126                     return new URL(s);
 127                 } catch (MalformedURLException e) {
 128                     Log.info(e.toString());
 129                     return null;
 130                 }
 131             });
 132 
 133     public static final BundlerParamInfo<Boolean> REBRAND_EXECUTABLE = new WindowsBundlerParam<>(
 134             I18N.getString("param.rebrand-executable.name"),
 135             I18N.getString("param.rebrand-executable.description"),
 136             "win.launcher.rebrand",
 137             Boolean.class, null, params -> Boolean.TRUE, 
 138             false, (s, p) -> Boolean.valueOf(s));
 139 
 140     public WinAppBundler() {
 141         super();
 142         baseResourceLoader = WinResources.class;
 143     }
 144 
 145     public final static String WIN_BUNDLER_PREFIX =
 146             BUNDLER_PREFIX + "windows/";
 147 
 148     File getConfigRoot(Map<String, ? super Object> params) {
 149         return CONFIG_ROOT.fetchFrom(params);
 150     }
 151 
 152     @Override
 153     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 154         try {
 155             if (params == null) throw new ConfigException(
 156                     I18N.getString("error.parameters-null"),
 157                     I18N.getString("error.parameters-null.advice"));
 158 
 159             return doValidate(params);
 160         } catch (RuntimeException re) {
 161             throw new ConfigException(re);
 162         }
 163     }
 164 
 165     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 166     // skipping validation if p.type does not include "image"
 167     boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 168         if (!System.getProperty("os.name").toLowerCase().startsWith("win")) {
 169             throw new UnsupportedPlatformException();
 170         }
 171 
 172         if (WinResources.class.getResource(TOOL_ICON_SWAP) == null) {
 173             throw new ConfigException(
 174                     I18N.getString("error.no-windows-resources"),
 175                     I18N.getString("error.no-windows-resources.advice"));
 176         }
 177 
 178         if (SERVICE_HINT.fetchFrom(p) && WinResources.class.getResource(EXECUTABLE_SVC_NAME) == null) {
 179             throw new ConfigException(
 180                     I18N.getString("error.no-windows-resources"),
 181                     I18N.getString("error.no-windows-resources.advice"));
 182         }
 183 
 184         if (MAIN_JAR.fetchFrom(p) == null) {
 185             throw new ConfigException(
 186                     I18N.getString("error.no-application-jar"),
 187                     I18N.getString("error.no-application-jar.advice"));
 188         }
 189 
 190         //validate required inputs
 191         if (USE_FX_PACKAGING.fetchFrom(p)) {
 192             testRuntime(p, new String[] {"lib/ext/jfxrt.jar", "lib/jfxrt.jar"});
 193         }
 194 
 195         //validate runtime bit-architectire
 196         testRuntimeBitArchitecture(p);
 197 
 198         return true;
 199     }
 200 
 201     private static void testRuntimeBitArchitecture(Map<String, ? super Object> params) throws ConfigException {
 202         if ("true".equalsIgnoreCase(System.getProperty("fxpackager.disableBitArchitectureMismatchCheck"))) {
 203             Log.debug(I18N.getString("message.disable-bit-architecture-check"));
 204             return;
 205         }
 206 
 207         if (BIT_ARCH_64.fetchFrom(params) != BIT_ARCH_64_RUNTIME.fetchFrom(params)) {
 208             throw new ConfigException(
 209                     I18N.getString("error.bit-architecture-mismatch"),
 210                     I18N.getString("error.bit-architecture-mismatch.advice"));
 211         }
 212     }
 213 
 214     static String getAppName(Map<String, ? super Object>  p) {
 215         return APP_NAME.fetchFrom(p);
 216     }
 217 
 218     static String getAppSvcName(Map<String, ? super Object>  p) {
 219         return APP_NAME.fetchFrom(p) + "Svc";
 220     }
 221 
 222     //it is static for the sake of sharing with "Exe" bundles
 223     // that may skip calls to validate/bundle in this class!
 224     private static File getRootDir(File outDir, Map<String, ? super Object> p) {
 225         return new File(outDir, getAppName(p));
 226     }
 227 
 228     public static File getLauncher(File outDir, Map<String, ? super Object> p) {
 229         return new File(getRootDir(outDir, p), getAppName(p)+".exe");
 230     }
 231 
 232     public static File getLauncherSvc(File outDir, Map<String, ? super Object> p) {
 233         return new File(getRootDir(outDir, p), getAppName(p)+"Svc.exe");
 234     }
 235 
 236     private File getConfig_AppIcon(Map<String, ? super Object> params) {
 237         return new File(getConfigRoot(params), getAppName(params) + ".ico");
 238     }
 239 
 240     private final static String TEMPLATE_APP_ICON ="javalogo_white_48.ico";
 241 
 242     //remove
 243     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 244         if (getConfig_AppIcon(params) != null) {
 245             getConfig_AppIcon(params).delete();
 246         }
 247     }
 248 
 249     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 250         File iconTarget = getConfig_AppIcon(params);
 251 
 252         File icon = ICON.fetchFrom(params);
 253         if (icon != null && icon.exists()) {
 254             fetchResource(WIN_BUNDLER_PREFIX + iconTarget.getName(),
 255                     I18N.getString("resource.application-icon"),
 256                     icon,
 257                     iconTarget,
 258                     VERBOSE.fetchFrom(params));
 259         } else {
 260             fetchResource(WIN_BUNDLER_PREFIX + iconTarget.getName(),
 261                     I18N.getString("resource.application-icon"),
 262                     WinAppBundler.TEMPLATE_APP_ICON,
 263                     iconTarget,
 264                     VERBOSE.fetchFrom(params));
 265         }
 266     }
 267 
 268     public boolean bundle(Map<String, ? super Object> p, File outputDirectory) {
 269         return doBundle(p, outputDirectory, false) != null;
 270     }
 271 
 272     File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 273         try {
 274             outputDirectory.mkdirs();
 275 
 276             if (!dependentTask) {
 277                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), getAppName(p), outputDirectory.getAbsolutePath()));
 278             }
 279 
 280             prepareConfigFiles(p);
 281 
 282             // Create directory structure
 283             File rootDirectory = getRootDir(outputDirectory, p);
 284             IOUtils.deleteRecursive(rootDirectory);
 285             rootDirectory.mkdirs();
 286 
 287             File appDirectory = new File(rootDirectory, "app");
 288             appDirectory.mkdirs();
 289             copyApplication(p, appDirectory);
 290 
 291             // Generate PkgInfo
 292             File pkgInfoFile = new File(appDirectory, "package.cfg");
 293             pkgInfoFile.createNewFile();
 294             writePkgInfo(p, pkgInfoFile);
 295 
 296             // Copy executable root folder
 297             File executableFile = getLauncher(outputDirectory, p);
 298             IOUtils.copyFromURL(
 299                     RAW_EXECUTABLE_URL.fetchFrom(p),
 300                     executableFile);
 301             executableFile.setExecutable(true, false);
 302 
 303             // Copy executable to install application as service
 304             if (SERVICE_HINT.fetchFrom(p)) {
 305                 File executableSvcFile = getLauncherSvc(outputDirectory, p);
 306                 IOUtils.copyFromURL(
 307                         WinResources.class.getResource(EXECUTABLE_SVC_NAME),
 308                         executableSvcFile);
 309                 executableSvcFile.setExecutable(true, false);
 310             }
 311 
 312             //Update branding of exe file
 313             if (REBRAND_EXECUTABLE.fetchFrom(p) && getConfig_AppIcon(p).exists()) {
 314                 //extract helper tool
 315                 File iconSwapTool = File.createTempFile("iconswap", ".exe");
 316                 iconSwapTool.delete();
 317                 IOUtils.copyFromURL(
 318                         WinResources.class.getResource(TOOL_ICON_SWAP),
 319                         iconSwapTool);
 320                 iconSwapTool.setExecutable(true, false);
 321                 iconSwapTool.deleteOnExit();
 322 
 323                 //run it on launcher file
 324                 executableFile.setWritable(true);
 325                 ProcessBuilder pb = new ProcessBuilder(
 326                         iconSwapTool.getAbsolutePath(),
 327                         getConfig_AppIcon(p).getAbsolutePath(),
 328                         executableFile.getAbsolutePath());
 329                 IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 330                 executableFile.setReadOnly();
 331                 iconSwapTool.delete();
 332             }
 333 
 334             // Copy runtime to PlugIns folder
 335             File runtimeDirectory = new File(rootDirectory, "runtime");
 336             copyRuntime(p, runtimeDirectory);
 337 
 338             IOUtils.copyFile(getConfig_AppIcon(p),
 339                     new File(getRootDir(outputDirectory, p), getAppName(p) + ".ico"));
 340 
 341             if (!dependentTask) {
 342                 Log.info(MessageFormat.format(I18N.getString("message.result-dir"), outputDirectory.getAbsolutePath()));
 343             }
 344 
 345             return rootDirectory;
 346         } catch (IOException ex) {
 347             System.out.println("Exception: "+ex);
 348             ex.printStackTrace();
 349             return null;
 350         } finally {
 351             if (VERBOSE.fetchFrom(p)) {
 352                 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), getConfigRoot(p).getAbsolutePath()));
 353             } else {
 354                 cleanupConfigFiles(p);
 355             }
 356         }
 357 
 358     }
 359 
 360     @Override
 361     public String toString() {
 362         return "Windows Application Bundler";
 363     }
 364 
 365     private void copyApplication(Map<String, ? super Object> params, File appDirectory) throws IOException {
 366         RelativeFileSet appResource = APP_RESOURCES.fetchFrom(params);
 367         if (appResource == null) {
 368             throw new RuntimeException("Null app resources?");
 369         }
 370         File srcdir = appResource.getBaseDirectory();
 371         for (String fname : appResource.getIncludedFiles()) {
 372             IOUtils.copyFile(
 373                     new File(srcdir, fname), new File(appDirectory, fname));
 374         }
 375     }
 376 
 377     private void writePkgInfo(Map<String, ? super Object> params, File pkgInfoFile) throws FileNotFoundException {
 378         pkgInfoFile.delete();
 379 
 380         PrintStream out = new PrintStream(pkgInfoFile);
 381         out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 382         out.println("app.version=" + VERSION.fetchFrom(params));
 383         //for future AU support (to be able to find app in the registry)
 384         out.println("app.id=" + IDENTIFIER.fetchFrom(params));
 385         out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
 386 
 387         if (USE_FX_PACKAGING.fetchFrom(params)) {
 388             out.println("app.mainclass=" +
 389                     JAVAFX_LAUNCHER_CLASS.replaceAll("\\.", "/"));
 390         } else {
 391             out.println("app.mainclass=" +
 392                     MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
 393         }
 394         //This will be emtry string for correctly packaged JavaFX apps
 395         out.println("app.classpath=" + MAIN_JAR_CLASSPATH.fetchFrom(params));
 396 
 397         List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
 398         int idx = 1;
 399         for (String a : jvmargs) {
 400             out.println("jvmarg."+idx+"="+a);
 401             idx++;
 402         }
 403         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 404         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 405             out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
 406             idx++;
 407         }
 408 
 409 
 410         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 411         idx = 1;
 412         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 413             if (arg.getKey() == null || arg.getValue() == null) {
 414                 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
 415             }
 416             else {
 417                 out.println("jvmuserarg."+idx+".name="+arg.getKey());
 418                 out.println("jvmuserarg."+idx+".value="+arg.getValue());
 419             }
 420             idx++;
 421         }
 422         out.close();
 423     }
 424 
 425     private void copyRuntime(Map<String, ? super Object> params, File runtimeDirectory) throws IOException {
 426         RelativeFileSet runtime = WIN_RUNTIME.fetchFrom(params);
 427         if (runtime == null) {
 428             //its ok, request to use system JRE
 429             return;
 430         }
 431         runtimeDirectory.mkdirs();
 432 
 433         File srcdir = runtime.getBaseDirectory();
 434         File destDir = new File(runtimeDirectory, srcdir.getName());
 435         Set<String> filesToCopy = runtime.getIncludedFiles();
 436         for (String fname : filesToCopy) {
 437             IOUtils.copyFile(
 438                     new File(srcdir, fname), new File(destDir, fname));
 439         }
 440     }
 441 
 442     @Override
 443     public String getName() {
 444         return I18N.getString("bundler.name");
 445     }
 446 
 447     @Override
 448     public String getDescription() {
 449         return I18N.getString("bundler.description");
 450     }
 451 
 452     @Override
 453     public String getID() {
 454         return "windows.app";
 455     }
 456 
 457     @Override
 458     public BundleType getBundleType() {
 459         return BundleType.IMAGE;
 460     }
 461 
 462     @Override
 463     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 464         return getAppBundleParameters();
 465     }
 466 
 467     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 468         return Arrays.asList(
 469                 APP_NAME,
 470                 APP_RESOURCES,
 471                 BUILD_ROOT,
 472                 CONFIG_ROOT,
 473                 ICON,
 474                 IDENTIFIER,
 475                 JVM_OPTIONS,
 476                 JVM_PROPERTIES,
 477                 MAIN_CLASS,
 478                 MAIN_JAR,
 479                 MAIN_JAR_CLASSPATH,
 480                 PREFERENCES_ID,
 481                 RAW_EXECUTABLE_URL,
 482                 WIN_RUNTIME,
 483                 USE_FX_PACKAGING,
 484                 USER_JVM_OPTIONS,
 485                 VERSION
 486         );
 487     }
 488 
 489     @Override
 490     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 491         return doBundle(params, outputParentDir, false);
 492     }
 493 }