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