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.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 }