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