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