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