1 /*
   2  * Copyright (c) 2012, 2015, 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.oracle.tools.packager.windows;
  27 
  28 import com.oracle.tools.packager.AbstractBundler;
  29 import com.oracle.tools.packager.BundlerParamInfo;
  30 import com.oracle.tools.packager.StandardBundlerParam;
  31 import com.oracle.tools.packager.Log;
  32 import com.oracle.tools.packager.ConfigException;
  33 import com.oracle.tools.packager.IOUtils;
  34 import com.oracle.tools.packager.RelativeFileSet;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 
  37 import java.io.File;
  38 import java.io.FileNotFoundException;
  39 import java.io.IOException;
  40 import java.io.PrintStream;
  41 import java.net.MalformedURLException;
  42 import java.net.URL;
  43 import java.text.MessageFormat;
  44 import java.util.*;
  45 
  46 import static com.oracle.tools.packager.StandardBundlerParam.*;
  47 import static com.oracle.tools.packager.windows.WindowsBundlerParam.BIT_ARCH_64;
  48 import static com.oracle.tools.packager.windows.WindowsBundlerParam.BIT_ARCH_64_RUNTIME;
  49 import static com.oracle.tools.packager.windows.WindowsBundlerParam.WIN_RUNTIME;
  50 
  51 public class WinAppBundler extends AbstractBundler {
  52 
  53     private static final ResourceBundle I18N = 
  54             ResourceBundle.getBundle(WinAppBundler.class.getName());
  55 
  56     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  57             I18N.getString("param.config-root.name"),
  58             I18N.getString("param.config-root.description"), 
  59             "configRoot",
  60             File.class,
  61             params -> {
  62                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  63                 imagesRoot.mkdirs();
  64                 return imagesRoot;
  65             },
  66             (s, p) -> null);
  67 
  68     private final static String EXECUTABLE_NAME = "WinLauncher.exe";
  69     private final static String LIBRARY_NAME = "packager.dll";
  70 
  71     private final static String[] VS_VERS = {"100", "110", "120"};
  72     private final static String REDIST_MSVCR = "msvcrVS_VER.dll";
  73     private final static String REDIST_MSVCP = "msvcpVS_VER.dll";
  74 
  75     private static final String TOOL_ICON_SWAP="IconSwap.exe";
  76 
  77     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new WindowsBundlerParam<>(
  78             I18N.getString("param.raw-executable-url.name"),
  79             I18N.getString("param.raw-executable-url.description"),
  80             "win.launcher.url",
  81             URL.class,
  82             params -> WinResources.class.getResource(EXECUTABLE_NAME),
  83             (s, p) -> {
  84                 try {
  85                     return new URL(s);
  86                 } catch (MalformedURLException e) {
  87                     Log.info(e.toString());
  88                     return null;
  89                 }
  90             });
  91 
  92     public static final BundlerParamInfo<Boolean> REBRAND_EXECUTABLE = new WindowsBundlerParam<>(
  93             I18N.getString("param.rebrand-executable.name"),
  94             I18N.getString("param.rebrand-executable.description"),
  95             "win.launcher.rebrand",
  96             Boolean.class,
  97             params -> Boolean.TRUE,
  98             (s, p) -> Boolean.valueOf(s));
  99 
 100     public static final BundlerParamInfo<File> ICON_ICO = new StandardBundlerParam<>(
 101             I18N.getString("param.icon-ico.name"),
 102             I18N.getString("param.icon-ico.description"),
 103             "icon.ico",
 104             File.class,
 105             params -> {
 106                 File f = ICON.fetchFrom(params);
 107                 if (f != null && !f.getName().toLowerCase().endsWith(".ico")) {
 108                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-ico"), f));
 109                     return null;
 110                 }
 111                 return f;
 112             },
 113             (s, p) -> new File(s));
 114 
 115     public WinAppBundler() {
 116         super();
 117         baseResourceLoader = WinResources.class;
 118     }
 119 
 120     public final static String WIN_BUNDLER_PREFIX =
 121             BUNDLER_PREFIX + "windows/";
 122 
 123     File getConfigRoot(Map<String, ? super Object> params) {
 124         return CONFIG_ROOT.fetchFrom(params);
 125     }
 126 
 127     @Override
 128     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 129         try {
 130             if (params == null) throw new ConfigException(
 131                     I18N.getString("error.parameters-null"),
 132                     I18N.getString("error.parameters-null.advice"));
 133 
 134             return doValidate(params);
 135         } catch (RuntimeException re) {
 136             if (re.getCause() instanceof ConfigException) {
 137                 throw (ConfigException) re.getCause();
 138             } else {
 139                 throw new ConfigException(re);
 140             }
 141         }
 142     }
 143 
 144     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 145     // skipping validation if p.type does not include "image"
 146     boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 147         if (!System.getProperty("os.name").toLowerCase().startsWith("win")) {
 148             throw new UnsupportedPlatformException();
 149         }
 150 
 151         StandardBundlerParam.validateMainClassInfoFromAppResources(p);
 152 
 153         Map<String, String> userJvmOptions = USER_JVM_OPTIONS.fetchFrom(p);
 154         if (userJvmOptions != null) {
 155             for (Map.Entry<String, String> entry : userJvmOptions.entrySet()) {
 156                 if (entry.getValue() == null || entry.getValue().isEmpty()) {
 157                     throw new ConfigException(
 158                             MessageFormat.format(I18N.getString("error.empty-user-jvm-option-value"), entry.getKey()),
 159                             I18N.getString("error.empty-user-jvm-option-value.advice"));
 160                 }
 161             }
 162         }
 163 
 164         if (WinResources.class.getResource(TOOL_ICON_SWAP) == null) {
 165             throw new ConfigException(
 166                     I18N.getString("error.no-windows-resources"),
 167                     I18N.getString("error.no-windows-resources.advice"));
 168         }
 169 
 170         if (MAIN_JAR.fetchFrom(p) == null) {
 171             throw new ConfigException(
 172                     I18N.getString("error.no-application-jar"),
 173                     I18N.getString("error.no-application-jar.advice"));
 174         }
 175 
 176         //validate required inputs
 177         testRuntime(WIN_RUNTIME.fetchFrom(p), new String[] {
 178                 "bin\\\\[^\\\\]+\\\\jvm.dll", // most reliable
 179                 "lib\\\\rt.jar", // fallback canary for JDK 8
 180         });
 181         if (USE_FX_PACKAGING.fetchFrom(p)) {
 182             testRuntime(WIN_RUNTIME.fetchFrom(p), new String[] {"lib\\\\ext\\\\jfxrt.jar", "lib\\\\jfxrt.jar"});
 183         }
 184 
 185         //validate runtime bit-architectire
 186         testRuntimeBitArchitecture(p);
 187 
 188         return true;
 189     }
 190 
 191     private static void testRuntimeBitArchitecture(Map<String, ? super Object> params) throws ConfigException {
 192         if ("true".equalsIgnoreCase(System.getProperty("fxpackager.disableBitArchitectureMismatchCheck"))) {
 193             Log.debug(I18N.getString("message.disable-bit-architecture-check"));
 194             return;
 195         }
 196 
 197         if (BIT_ARCH_64.fetchFrom(params) != BIT_ARCH_64_RUNTIME.fetchFrom(params)) {
 198             throw new ConfigException(
 199                     I18N.getString("error.bit-architecture-mismatch"),
 200                     I18N.getString("error.bit-architecture-mismatch.advice"));
 201         }
 202     }
 203 
 204     //it is static for the sake of sharing with "Exe" bundles
 205     // that may skip calls to validate/bundle in this class!
 206     private static File getRootDir(File outDir, Map<String, ? super Object> p) {
 207         return new File(outDir, APP_NAME.fetchFrom(p));
 208     }
 209 
 210     public static String getLauncherName(Map<String, ? super Object> p) {
 211         return APP_NAME.fetchFrom(p) +".exe";
 212     }
 213 
 214     public static String getLauncherCfgName(Map<String, ? super Object> p) {
 215         return "app\\" + APP_NAME.fetchFrom(p) +".cfg";
 216     }
 217 
 218     private File getConfig_AppIcon(Map<String, ? super Object> params) {
 219         return new File(getConfigRoot(params), APP_NAME.fetchFrom(params) + ".ico");
 220     }
 221 
 222     private final static String TEMPLATE_APP_ICON ="javalogo_white_48.ico";
 223 
 224     //remove
 225     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 226         if (getConfig_AppIcon(params) != null) {
 227             getConfig_AppIcon(params).delete();
 228         }
 229     }
 230 
 231     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 232         File iconTarget = getConfig_AppIcon(params);
 233 
 234         File icon = ICON_ICO.fetchFrom(params);
 235         if (icon != null && icon.exists()) {
 236             fetchResource(WIN_BUNDLER_PREFIX + iconTarget.getName(),
 237                     I18N.getString("resource.application-icon"),
 238                     icon,
 239                     iconTarget,
 240                     VERBOSE.fetchFrom(params),
 241                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 242         } else {
 243             fetchResource(WIN_BUNDLER_PREFIX + iconTarget.getName(),
 244                     I18N.getString("resource.application-icon"),
 245                     WinAppBundler.TEMPLATE_APP_ICON,
 246                     iconTarget,
 247                     VERBOSE.fetchFrom(params),
 248                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 249         }
 250     }
 251 
 252     public boolean bundle(Map<String, ? super Object> p, File outputDirectory) {
 253         return doBundle(p, outputDirectory, false) != null;
 254     }
 255 
 256     File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 257         Map<String, ? super Object> originalParams = new HashMap<>(p);
 258         if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 259             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 260         }
 261         if (!outputDirectory.canWrite()) {
 262             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 263         }
 264         try {
 265             if (!dependentTask) {
 266                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), APP_NAME.fetchFrom(p), outputDirectory.getAbsolutePath()));
 267             }
 268 
 269             // Create directory structure
 270             File rootDirectory = getRootDir(outputDirectory, p);
 271             IOUtils.deleteRecursive(rootDirectory);
 272             rootDirectory.mkdirs();
 273 
 274             File appDirectory = new File(rootDirectory, "app");
 275             appDirectory.mkdirs();
 276             copyApplication(p, appDirectory);
 277 
 278             // create the .exe launchers
 279             createLauncherForEntryPoint(p, rootDirectory);
 280 
 281             // copy in the needed libraries
 282             IOUtils.copyFromURL(
 283                     WinResources.class.getResource(LIBRARY_NAME),
 284                     new File(rootDirectory, LIBRARY_NAME));
 285 
 286             copyMSVCDLLs(rootDirectory);
 287 
 288             // create the secondary launchers, if any
 289             List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p);
 290             for (Map<String, ? super Object> entryPoint : entryPoints) {
 291                 Map<String, ? super Object> tmp = new HashMap<>(originalParams);
 292                 tmp.putAll(entryPoint);
 293                 createLauncherForEntryPoint(tmp, rootDirectory);
 294             }
 295 
 296             // Copy runtime to PlugIns folder
 297             File runtimeDirectory = new File(rootDirectory, "runtime");
 298             copyRuntime(p, runtimeDirectory);
 299 
 300             if (!dependentTask) {
 301                 Log.info(MessageFormat.format(I18N.getString("message.result-dir"), outputDirectory.getAbsolutePath()));
 302             }
 303 
 304             return rootDirectory;
 305         } catch (IOException ex) {
 306             Log.info("Exception: "+ex);
 307             Log.debug(ex);
 308             return null;
 309         } finally {
 310             if (VERBOSE.fetchFrom(p)) {
 311                 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), getConfigRoot(p).getAbsolutePath()));
 312             } else {
 313                 cleanupConfigFiles(p);
 314             }
 315         }
 316 
 317     }
 318 
 319     private void copyMSVCDLLs(File rootDirectory) throws IOException {
 320         for (String VS_VER : VS_VERS) {
 321              if (copyMSVCDLLs(rootDirectory, VS_VER))
 322                  return; // found and copied
 323         }
 324 
 325         throw new RuntimeException("Not found MSVC dlls");
 326     }
 327 
 328     private boolean copyMSVCDLLs(File rootDirectory, String VS_VER) throws IOException {
 329         final URL REDIST_MSVCR_URL = WinResources.class.getResource(
 330                                               REDIST_MSVCR.replaceAll("VS_VER", VS_VER));
 331         final URL REDIST_MSVCP_URL = WinResources.class.getResource(
 332                                               REDIST_MSVCP.replaceAll("VS_VER", VS_VER));
 333 
 334         if (REDIST_MSVCR_URL != null && REDIST_MSVCP_URL != null) {
 335             IOUtils.copyFromURL(
 336                     REDIST_MSVCR_URL,
 337                     new File(rootDirectory, REDIST_MSVCR.replaceAll("VS_VER", VS_VER)));
 338             IOUtils.copyFromURL(
 339                     REDIST_MSVCP_URL,
 340                     new File(rootDirectory, REDIST_MSVCP.replaceAll("VS_VER", VS_VER)));
 341             return true;
 342         }
 343 
 344         return false; // not found
 345     }
 346 
 347     private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
 348         prepareConfigFiles(p);
 349 
 350         writePkgInfo(p, rootDirectory);
 351 
 352         // Copy executable root folder
 353         File executableFile = new File(rootDirectory, getLauncherName(p));
 354         IOUtils.copyFromURL(
 355                 RAW_EXECUTABLE_URL.fetchFrom(p),
 356                 executableFile);
 357         executableFile.setExecutable(true, false);
 358 
 359         //Update branding of exe file
 360         if (REBRAND_EXECUTABLE.fetchFrom(p) && getConfig_AppIcon(p).exists()) {
 361             //extract helper tool
 362             File iconSwapTool = File.createTempFile("iconswap", ".exe");
 363             iconSwapTool.delete();
 364             IOUtils.copyFromURL(
 365                     WinResources.class.getResource(TOOL_ICON_SWAP),
 366                     iconSwapTool);
 367             iconSwapTool.setExecutable(true, false);
 368             iconSwapTool.deleteOnExit();
 369 
 370             //run it on launcher file
 371             executableFile.setWritable(true);
 372             ProcessBuilder pb = new ProcessBuilder(
 373                     iconSwapTool.getAbsolutePath(),
 374                     getConfig_AppIcon(p).getAbsolutePath(),
 375                     executableFile.getAbsolutePath());
 376             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 377             executableFile.setReadOnly();
 378             iconSwapTool.delete();
 379         }
 380 
 381         IOUtils.copyFile(getConfig_AppIcon(p),
 382                 new File(rootDirectory, APP_NAME.fetchFrom(p) + ".ico"));
 383     }
 384 
 385     private void copyApplication(Map<String, ? super Object> params, File appDirectory) throws IOException {
 386         List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 387         if (appResourcesList == null) {
 388             throw new RuntimeException("Null app resources?");
 389         }
 390         for (RelativeFileSet appResources : appResourcesList) {
 391             if (appResources == null) {
 392                 throw new RuntimeException("Null app resources?");
 393             }
 394             File srcdir = appResources.getBaseDirectory();
 395             for (String fname : appResources.getIncludedFiles()) {
 396                 IOUtils.copyFile(
 397                         new File(srcdir, fname), new File(appDirectory, fname));
 398             }
 399         }
 400     }
 401 
 402     private void writePkgInfo(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException {
 403         File pkgInfoFile = new File(rootDir, getLauncherCfgName(params));
 404 
 405         pkgInfoFile.delete();
 406 
 407         PrintStream out = new PrintStream(pkgInfoFile);
 408         if (WIN_RUNTIME.fetchFrom(params) == null) {
 409             out.println("app.runtime=");
 410         } else {
 411             out.println("app.runtime=$APPDIR\\runtime");
 412         }
 413         out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 414         out.println("app.version=" + VERSION.fetchFrom(params));
 415         //for future AU support (to be able to find app in the registry)
 416         out.println("app.id=" + IDENTIFIER.fetchFrom(params));
 417         out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
 418 
 419         out.println("app.mainclass=" +
 420                 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
 421         out.println("app.classpath=" + CLASSPATH.fetchFrom(params));
 422 
 423         List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
 424         int idx = 1;
 425         for (String a : jvmargs) {
 426             out.println("jvmarg."+idx+"="+a);
 427             idx++;
 428         }
 429         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 430         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 431             out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
 432             idx++;
 433         }
 434 
 435         String preloader = PRELOADER_CLASS.fetchFrom(params);
 436         if (preloader != null) {
 437             out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader);
 438         }
 439 
 440         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 441         idx = 1;
 442         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 443             if (arg.getKey() == null || arg.getValue() == null) {
 444                 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
 445             }
 446             else {
 447                 out.println("jvmuserarg."+idx+".name="+arg.getKey());
 448                 out.println("jvmuserarg."+idx+".value="+arg.getValue());
 449             }
 450             idx++;
 451         }
 452 
 453         // add command line args
 454         List<String> args = ARGUMENTS.fetchFrom(params);
 455         idx = 1;
 456         for (String a : args) {
 457             out.println("arg."+idx+"="+a);
 458             idx++;
 459         }
 460 
 461         out.close();
 462     }
 463 
 464     private void copyRuntime(Map<String, ? super Object> params, File runtimeDirectory) throws IOException {
 465         RelativeFileSet runtime = WIN_RUNTIME.fetchFrom(params);
 466         if (runtime == null) {
 467             //its ok, request to use system JRE
 468             return;
 469         }
 470         runtimeDirectory.mkdirs();
 471 
 472         File srcdir = runtime.getBaseDirectory();
 473         Set<String> filesToCopy = runtime.getIncludedFiles();
 474         for (String fname : filesToCopy) {
 475             IOUtils.copyFile(
 476                     new File(srcdir, fname), new File(runtimeDirectory, fname));
 477         }
 478     }
 479 
 480     @Override
 481     public String getName() {
 482         return I18N.getString("bundler.name");
 483     }
 484 
 485     @Override
 486     public String getDescription() {
 487         return I18N.getString("bundler.description");
 488     }
 489 
 490     @Override
 491     public String getID() {
 492         return "windows.app";
 493     }
 494 
 495     @Override
 496     public String getBundleType() {
 497         return "IMAGE";
 498     }
 499 
 500     @Override
 501     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 502         return getAppBundleParameters();
 503     }
 504 
 505     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 506         return Arrays.asList(
 507                 APP_NAME,
 508                 APP_RESOURCES,
 509                 // APP_RESOURCES_LIST, // ??
 510                 ARGUMENTS,
 511                 CLASSPATH,
 512                 ICON_ICO,
 513                 JVM_OPTIONS,
 514                 JVM_PROPERTIES,
 515                 MAIN_CLASS,
 516                 MAIN_JAR,
 517                 PREFERENCES_ID,
 518                 PRELOADER_CLASS,
 519                 USER_JVM_OPTIONS,
 520                 VERSION,
 521                 WIN_RUNTIME
 522             );
 523     }
 524 
 525     @Override
 526     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 527         return doBundle(params, outputParentDir, false);
 528     }
 529 }