1 /* 2 * Copyright (c) 2015, 2018, 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 jdk.packager.internal.builders.windows; 27 28 import jdk.packager.internal.BundlerParamInfo; 29 import jdk.packager.internal.Log; 30 import jdk.packager.internal.RelativeFileSet; 31 import jdk.packager.internal.IOUtils; 32 import jdk.packager.internal.StandardBundlerParam; 33 import jdk.packager.internal.resources.windows.WinResources; 34 import jdk.packager.internal.windows.WindowsBundlerParam; 35 import jdk.packager.internal.builders.AbstractAppImageBuilder; 36 import jdk.packager.internal.windows.WindowsDefender; 37 38 import java.io.File; 39 import java.io.FileOutputStream; 40 import java.io.FileInputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.OutputStream; 44 import java.io.OutputStreamWriter; 45 import java.io.UncheckedIOException; 46 import java.io.Writer; 47 import java.io.BufferedWriter; 48 import java.io.FileWriter; 49 import java.nio.file.Files; 50 import java.nio.file.Path; 51 import java.nio.file.attribute.PosixFilePermission; 52 import java.text.MessageFormat; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Objects; 57 import java.util.ResourceBundle; 58 import java.util.Set; 59 import java.util.concurrent.atomic.AtomicReference; 60 import java.util.regex.Pattern; 61 import java.util.stream.Stream; 62 import jdk.packager.internal.Arguments; 63 64 import static jdk.packager.internal.StandardBundlerParam.*; 65 66 public class WindowsAppImageBuilder extends AbstractAppImageBuilder { 67 68 private static final ResourceBundle I18N = 69 ResourceBundle.getBundle( 70 "jdk.packager.internal.resources.builders.windows.WindowsAppImageBuilder"); 71 72 private static final String MODULES_FILENAME = 73 "jdk/packager/internal/resources/windows/windows.jre.list"; 74 75 protected static final String WINDOWS_BUNDLER_PREFIX = 76 BUNDLER_PREFIX + "windows" + File.separator; 77 78 private final static String EXECUTABLE_NAME = "WinLauncher.exe"; 79 private final static String LIBRARY_NAME = "packager.dll"; 80 private final static String REDIST_MSVCR = "vcruntimeVS_VER.dll"; 81 private final static String REDIST_MSVCP = "msvcpVS_VER.dll"; 82 83 private final static String TEMPLATE_APP_ICON ="javalogo_white_48.ico"; 84 85 private static final String EXECUTABLE_PROPERTIES_TEMPLATE = 86 "WinLauncher.properties"; 87 88 private final Path root; 89 private final Path appDir; 90 private final Path runtimeDir; 91 private final Path mdir; 92 93 private final Map<String, ? super Object> params; 94 95 public static final BundlerParamInfo<File> CONFIG_ROOT = 96 new WindowsBundlerParam<>( 97 I18N.getString("param.config-root.name"), 98 I18N.getString("param.config-root.description"), 99 "configRoot", 100 File.class, 101 params -> { 102 File imagesRoot = 103 new File(BUILD_ROOT.fetchFrom(params), "windows"); 104 imagesRoot.mkdirs(); 105 return imagesRoot; 106 }, 107 (s, p) -> null); 108 109 public static final BundlerParamInfo<Boolean> REBRAND_EXECUTABLE = 110 new WindowsBundlerParam<>( 111 I18N.getString("param.rebrand-executable.name"), 112 I18N.getString("param.rebrand-executable.description"), 113 "win.launcher.rebrand", 114 Boolean.class, 115 params -> Boolean.TRUE, 116 (s, p) -> Boolean.valueOf(s)); 117 118 public static final BundlerParamInfo<File> ICON_ICO = 119 new StandardBundlerParam<>( 120 I18N.getString("param.icon-ico.name"), 121 I18N.getString("param.icon-ico.description"), 122 "icon.ico", 123 File.class, 124 params -> { 125 File f = ICON.fetchFrom(params); 126 if (f != null && !f.getName().toLowerCase().endsWith(".ico")) { 127 Log.info(MessageFormat.format( 128 I18N.getString("message.icon-not-ico"), f)); 129 return null; 130 } 131 return f; 132 }, 133 (s, p) -> new File(s)); 134 135 public static final StandardBundlerParam<Boolean> CONSOLE_HINT = 136 new WindowsBundlerParam<>( 137 I18N.getString("param.console-hint.name"), 138 I18N.getString("param.console-hint.description"), 139 Arguments.CLIOptions.WIN_CONSOLE_HINT.getId(), 140 Boolean.class, 141 params -> false, 142 // valueOf(null) is false, 143 // and we actually do want null in some cases 144 (s, p) -> (s == null 145 || "null".equalsIgnoreCase(s)) ? true : Boolean.valueOf(s)); 146 147 public WindowsAppImageBuilder(Map<String, Object> config, Path imageOutDir) 148 throws IOException { 149 super(config, 150 imageOutDir.resolve(APP_NAME.fetchFrom(config) + "/runtime")); 151 152 Objects.requireNonNull(imageOutDir); 153 154 this.params = config; 155 156 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params)); 157 this.appDir = root.resolve("app"); 158 this.runtimeDir = root.resolve("runtime"); 159 this.mdir = runtimeDir.resolve("lib"); 160 Files.createDirectories(appDir); 161 Files.createDirectories(runtimeDir); 162 } 163 164 public WindowsAppImageBuilder(String jreName, Path imageOutDir) 165 throws IOException { 166 super(null, imageOutDir.resolve(jreName)); 167 168 Objects.requireNonNull(imageOutDir); 169 170 this.params = null; 171 this.root = imageOutDir.resolve(jreName); 172 this.appDir = null; 173 this.runtimeDir = root; 174 this.mdir = runtimeDir.resolve("lib"); 175 Files.createDirectories(runtimeDir); 176 } 177 178 private Path destFile(String dir, String filename) { 179 return runtimeDir.resolve(dir).resolve(filename); 180 } 181 182 private void writeEntry(InputStream in, Path dstFile) throws IOException { 183 Files.createDirectories(dstFile.getParent()); 184 Files.copy(in, dstFile); 185 } 186 187 private void writeSymEntry(Path dstFile, Path target) throws IOException { 188 Files.createDirectories(dstFile.getParent()); 189 Files.createLink(dstFile, target); 190 } 191 192 /** 193 * chmod ugo+x file 194 */ 195 private void setExecutable(Path file) { 196 try { 197 Set<PosixFilePermission> perms = 198 Files.getPosixFilePermissions(file); 199 perms.add(PosixFilePermission.OWNER_EXECUTE); 200 perms.add(PosixFilePermission.GROUP_EXECUTE); 201 perms.add(PosixFilePermission.OTHERS_EXECUTE); 202 Files.setPosixFilePermissions(file, perms); 203 } catch (IOException ioe) { 204 throw new UncheckedIOException(ioe); 205 } 206 } 207 208 private static void createUtf8File(File file, String content) 209 throws IOException { 210 try (OutputStream fout = new FileOutputStream(file); 211 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 212 output.write(content); 213 } 214 } 215 216 // This method is static for the sake of sharing with "installer" bundlers 217 // that may skip calls to validate/bundle in this class! 218 public static File getRootDir(File outDir, Map<String, ? super Object> p) { 219 return new File(outDir, APP_FS_NAME.fetchFrom(p)); 220 } 221 222 public static String getLauncherName(Map<String, ? super Object> p) { 223 return APP_FS_NAME.fetchFrom(p) + ".exe"; 224 } 225 226 // Returns launcher resource name for launcher we need to use. 227 public static String getLauncherResourceName(Map<String, ? super Object> p) { 228 if (CONSOLE_HINT.fetchFrom(p)) { 229 return "papplauncherc.exe"; 230 } 231 232 return "papplauncher.exe"; 233 } 234 235 public static String getLauncherCfgName(Map<String, ? super Object> p) { 236 return "app/" + APP_FS_NAME.fetchFrom(p) +".cfg"; 237 } 238 239 private File getConfig_AppIcon(Map<String, ? super Object> params) { 240 return new File(getConfigRoot(params), 241 APP_FS_NAME.fetchFrom(params) + ".ico"); 242 } 243 244 private File getConfig_ExecutableProperties( 245 Map<String, ? super Object> params) { 246 return new File(getConfigRoot(params), 247 APP_FS_NAME.fetchFrom(params) + ".properties"); 248 } 249 250 File getConfigRoot(Map<String, ? super Object> params) { 251 return CONFIG_ROOT.fetchFrom(params); 252 } 253 254 protected void cleanupConfigFiles(Map<String, ? super Object> params) { 255 getConfig_AppIcon(params).delete(); 256 getConfig_ExecutableProperties(params).delete(); 257 } 258 259 @Override 260 public InputStream getResourceAsStream(String name) { 261 return WinResources.class.getResourceAsStream(name); 262 } 263 264 @Override 265 public void prepareApplicationFiles() throws IOException { 266 Map<String, ? super Object> originalParams = new HashMap<>(params); 267 File rootFile = root.toFile(); 268 if (!rootFile.isDirectory() && !rootFile.mkdirs()) { 269 throw new RuntimeException(MessageFormat.format(I18N.getString( 270 "error.cannot-create-output-dir"), rootFile.getAbsolutePath())); 271 } 272 if (!rootFile.canWrite()) { 273 throw new RuntimeException(MessageFormat.format( 274 I18N.getString("error.cannot-write-to-output-dir"), 275 rootFile.getAbsolutePath())); 276 } 277 try { 278 // create the .exe launchers 279 createLauncherForEntryPoint(params); 280 281 // copy the jars 282 copyApplication(params); 283 284 // copy in the needed libraries 285 try (InputStream is_lib = getResourceAsStream("packager.dll")) { 286 Files.copy(is_lib, root.resolve(LIBRARY_NAME)); 287 } 288 289 copyMSVCDLLs(); 290 291 // create the secondary launchers, if any 292 List<Map<String, ? super Object>> entryPoints = 293 StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(params); 294 for (Map<String, ? super Object> entryPoint : entryPoints) { 295 Map<String, ? super Object> tmp = new HashMap<>(originalParams); 296 tmp.putAll(entryPoint); 297 createLauncherForEntryPoint(tmp); 298 } 299 300 } catch (IOException ex) { 301 Log.info("Exception: "+ex); 302 Log.debug(ex); 303 } finally { 304 cleanupConfigFiles(params); 305 } 306 } 307 308 @Override 309 public void prepareServerJreFiles() throws IOException {} 310 311 private void copyMSVCDLLs() throws IOException { 312 AtomicReference<IOException> ioe = new AtomicReference<>(); 313 try (Stream<Path> files = Files.list(runtimeDir.resolve("bin"))) { 314 files.filter(p -> Pattern.matches( 315 "^(vcruntime|msvcp|msvcr|ucrtbase|api-ms-win-).*\\.dll$", 316 p.toFile().getName().toLowerCase())) 317 .forEach(p -> { 318 try { 319 Files.copy(p, root.resolve((p.toFile().getName()))); 320 } catch (IOException e) { 321 ioe.set(e); 322 } 323 }); 324 } 325 326 IOException e = ioe.get(); 327 if (e != null) { 328 throw e; 329 } 330 } 331 332 // TODO: do we still need this? 333 private boolean copyMSVCDLLs(String VS_VER) throws IOException { 334 final InputStream REDIST_MSVCR_URL = 335 WinResources.class.getResourceAsStream( 336 REDIST_MSVCR.replaceAll("VS_VER", VS_VER)); 337 final InputStream REDIST_MSVCP_URL = 338 WinResources.class.getResourceAsStream( 339 REDIST_MSVCP.replaceAll("VS_VER", VS_VER)); 340 341 if (REDIST_MSVCR_URL != null && REDIST_MSVCP_URL != null) { 342 Files.copy( 343 REDIST_MSVCR_URL, 344 root.resolve(REDIST_MSVCR.replaceAll("VS_VER", VS_VER))); 345 Files.copy( 346 REDIST_MSVCP_URL, 347 root.resolve(REDIST_MSVCP.replaceAll("VS_VER", VS_VER))); 348 return true; 349 } 350 351 return false; 352 } 353 354 private void validateValueAndPut( 355 Map<String, String> data, String key, 356 BundlerParamInfo<String> param, 357 Map<String, ? super Object> params) { 358 String value = param.fetchFrom(params); 359 if (value.contains("\r") || value.contains("\n")) { 360 Log.info("Configuration Parameter " + param.getID() 361 + " contains multiple lines of text, ignore it"); 362 data.put(key, ""); 363 return; 364 } 365 data.put(key, value); 366 } 367 368 protected void prepareExecutableProperties( 369 Map<String, ? super Object> params) throws IOException { 370 Map<String, String> data = new HashMap<>(); 371 372 // mapping Java parameters in strings for version resource 373 data.put("COMMENTS", ""); 374 validateValueAndPut(data, "COMPANY_NAME", VENDOR, params); 375 validateValueAndPut(data, "FILE_DESCRIPTION", DESCRIPTION, params); 376 validateValueAndPut(data, "FILE_VERSION", VERSION, params); 377 data.put("INTERNAL_NAME", getLauncherName(params)); 378 validateValueAndPut(data, "LEGAL_COPYRIGHT", COPYRIGHT, params); 379 data.put("LEGAL_TRADEMARK", ""); 380 data.put("ORIGINAL_FILENAME", getLauncherName(params)); 381 data.put("PRIVATE_BUILD", ""); 382 validateValueAndPut(data, "PRODUCT_NAME", APP_NAME, params); 383 validateValueAndPut(data, "PRODUCT_VERSION", VERSION, params); 384 data.put("SPECIAL_BUILD", ""); 385 386 Writer w = new BufferedWriter( 387 new FileWriter(getConfig_ExecutableProperties(params))); 388 String content = preprocessTextResource(WINDOWS_BUNDLER_PREFIX 389 + getConfig_ExecutableProperties(params).getName(), 390 I18N.getString("resource.executable-properties-template"), 391 EXECUTABLE_PROPERTIES_TEMPLATE, data, 392 VERBOSE.fetchFrom(params), 393 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 394 w.write(content); 395 w.close(); 396 } 397 398 private void createLauncherForEntryPoint( 399 Map<String, ? super Object> p) throws IOException { 400 401 File launcherIcon = ICON_ICO.fetchFrom(p); 402 File icon = launcherIcon != null ? 403 launcherIcon : ICON_ICO.fetchFrom(params); 404 File iconTarget = getConfig_AppIcon(p); 405 406 InputStream in = locateResource( 407 "package/windows/" + APP_NAME.fetchFrom(params) + ".ico", 408 "icon", 409 TEMPLATE_APP_ICON, 410 icon, 411 VERBOSE.fetchFrom(params), 412 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 413 Files.copy(in, iconTarget.toPath()); 414 415 writeCfgFile(p, root.resolve( 416 getLauncherCfgName(p)).toFile(), "$APPDIR\\runtime"); 417 418 prepareExecutableProperties(p); 419 420 // Copy executable root folder 421 Path executableFile = root.resolve(getLauncherName(p)); 422 try (InputStream is_launcher = 423 getResourceAsStream(getLauncherResourceName(p))) { 424 writeEntry(is_launcher, executableFile); 425 } 426 427 File launcher = executableFile.toFile(); 428 launcher.setWritable(true, true); 429 430 // Update branding of EXE file 431 if (REBRAND_EXECUTABLE.fetchFrom(p)) { 432 File tool = new File( 433 System.getProperty("java.home") + "\\bin\\jpackager.exe"); 434 435 // Run tool on launcher file to change the icon and the metadata. 436 try { 437 if (WindowsDefender.isThereAPotentialWindowsDefenderIssue()) { 438 Log.info(MessageFormat.format(I18N.getString( 439 "message.potential.windows.defender.issue"), 440 WindowsDefender.getUserTempDirectory())); 441 } 442 443 launcher.setWritable(true); 444 445 if (iconTarget.exists()) { 446 ProcessBuilder pb = new ProcessBuilder( 447 tool.getAbsolutePath(), 448 "--icon-swap", 449 iconTarget.getAbsolutePath(), 450 launcher.getAbsolutePath()); 451 IOUtils.exec(pb, false); 452 } 453 454 File executableProperties = getConfig_ExecutableProperties(p); 455 456 if (executableProperties.exists()) { 457 ProcessBuilder pb = new ProcessBuilder( 458 tool.getAbsolutePath(), 459 "--version-swap", 460 executableProperties.getAbsolutePath(), 461 launcher.getAbsolutePath()); 462 IOUtils.exec(pb, false); 463 } 464 } 465 finally { 466 executableFile.toFile().setReadOnly(); 467 } 468 } 469 470 Files.copy(iconTarget.toPath(), 471 root.resolve(APP_NAME.fetchFrom(p) + ".ico")); 472 } 473 474 private void copyApplication(Map<String, ? super Object> params) 475 throws IOException { 476 List<RelativeFileSet> appResourcesList = 477 APP_RESOURCES_LIST.fetchFrom(params); 478 if (appResourcesList == null) { 479 throw new RuntimeException("Null app resources?"); 480 } 481 for (RelativeFileSet appResources : appResourcesList) { 482 if (appResources == null) { 483 throw new RuntimeException("Null app resources?"); 484 } 485 File srcdir = appResources.getBaseDirectory(); 486 for (String fname : appResources.getIncludedFiles()) { 487 copyEntry(appDir, srcdir, fname); 488 } 489 } 490 } 491 492 @Override 493 public String getPlatformSpecificModulesFile() { 494 return MODULES_FILENAME; 495 } 496 497 }