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 }