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 String relativeModsDir; 78 private final Path runtimeDir; 79 private final Path mdir; 80 private final Path binDir; 81 82 private final Map<String, ? super Object> params; 83 84 public static final BundlerParamInfo<Boolean> REBRAND_EXECUTABLE = 85 new WindowsBundlerParam<>( 86 "win.launcher.rebrand", 87 Boolean.class, 88 params -> Boolean.TRUE, 89 (s, p) -> Boolean.valueOf(s)); 90 91 public static final BundlerParamInfo<File> ICON_ICO = 92 new StandardBundlerParam<>( 93 "icon.ico", 94 File.class, 95 params -> { 96 File f = ICON.fetchFrom(params); 97 if (f != null && !f.getName().toLowerCase().endsWith(".ico")) { 98 Log.error(MessageFormat.format( 99 I18N.getString("message.icon-not-ico"), f)); 100 return null; 101 } 102 return f; 103 }, 104 (s, p) -> new File(s)); 105 106 public static final StandardBundlerParam<Boolean> CONSOLE_HINT = 107 new WindowsBundlerParam<>( 108 Arguments.CLIOptions.WIN_CONSOLE_HINT.getId(), 109 Boolean.class, 110 params -> false, 111 // valueOf(null) is false, 112 // and we actually do want null in some cases 113 (s, p) -> (s == null 114 || "null".equalsIgnoreCase(s)) ? true : Boolean.valueOf(s)); 115 116 public WindowsAppImageBuilder(Map<String, Object> config, Path imageOutDir) 117 throws IOException { 118 super(config, 119 imageOutDir.resolve(APP_NAME.fetchFrom(config) + "/runtime")); 120 121 Objects.requireNonNull(imageOutDir); 122 123 this.params = config; 124 125 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params)); 126 this.appDir = root.resolve("app"); 127 this.appModsDir = appDir.resolve("mods"); 128 this.relativeModsDir = "app/mods"; 129 this.runtimeDir = root.resolve("runtime"); 130 this.mdir = runtimeDir.resolve("lib"); 131 this.binDir = root.resolve("bin"); 132 Files.createDirectories(appDir); 133 Files.createDirectories(runtimeDir); 134 } 135 136 public WindowsAppImageBuilder(String jreName, Path imageOutDir) 137 throws IOException { 138 super(null, imageOutDir.resolve(jreName)); 139 140 Objects.requireNonNull(imageOutDir); 141 142 this.params = null; 143 this.root = imageOutDir.resolve(jreName); 144 this.appDir = null; 145 this.appModsDir = null; 146 this.relativeModsDir = null; 147 this.runtimeDir = root; 148 this.mdir = runtimeDir.resolve("lib"); 149 this.binDir = null; 150 Files.createDirectories(runtimeDir); 151 } 152 153 private void writeEntry(InputStream in, Path dstFile) throws IOException { 154 Files.createDirectories(dstFile.getParent()); 155 Files.copy(in, dstFile); 156 } 157 158 private static String getLauncherName(Map<String, ? super Object> params) { 159 return APP_NAME.fetchFrom(params) + ".exe"; 160 } 161 162 // Returns launcher resource name for launcher we need to use. 163 public static String getLauncherResourceName( 164 Map<String, ? super Object> params) { 165 if (CONSOLE_HINT.fetchFrom(params)) { 166 return "jpackageapplauncher.exe"; 167 } else { 168 return "jpackageapplauncherw.exe"; 169 } 170 } 171 172 public static String getLauncherCfgName( 173 Map<String, ? super Object> params) { 174 return "app/" + APP_NAME.fetchFrom(params) +".cfg"; 175 } 176 177 private File getConfig_AppIcon(Map<String, ? super Object> params) { 178 return new File(getConfigRoot(params), 179 APP_NAME.fetchFrom(params) + ".ico"); 180 } 181 182 private File getConfig_ExecutableProperties( 183 Map<String, ? super Object> params) { 184 return new File(getConfigRoot(params), 185 APP_NAME.fetchFrom(params) + ".properties"); 186 } 187 188 File getConfigRoot(Map<String, ? super Object> params) { 189 return CONFIG_ROOT.fetchFrom(params); 190 } 191 192 @Override 193 public Path getAppDir() { 194 return appDir; 195 } 196 197 @Override 198 public Path getAppModsDir() { 199 return appModsDir; 200 } 201 202 @Override 203 public String getRelativeModsDir() { 204 return relativeModsDir; 205 } 206 207 @Override 208 public void prepareApplicationFiles() throws IOException { 209 Map<String, ? super Object> originalParams = new HashMap<>(params); 210 211 try { 212 IOUtils.writableOutputDir(root); 213 IOUtils.writableOutputDir(binDir); 214 } catch (PackagerException pe) { 215 throw new RuntimeException(pe); 216 } 217 218 // create the .exe launchers 219 createLauncherForEntryPoint(params); 220 221 // copy the jars 222 copyApplication(params); 223 224 // copy in the needed libraries 225 try (InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { 226 Files.copy(is_lib, binDir.resolve(LIBRARY_NAME)); 227 } 228 229 copyMSVCDLLs(); 230 231 // create the additional launcher(s), if any 232 List<Map<String, ? super Object>> entryPoints = 233 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); 234 for (Map<String, ? super Object> entryPoint : entryPoints) { 235 createLauncherForEntryPoint( 236 AddLauncherArguments.merge(originalParams, entryPoint)); 237 } 238 } 239 240 @Override 241 public void prepareJreFiles() throws IOException {} 242 243 private void copyMSVCDLLs() throws IOException { 244 AtomicReference<IOException> ioe = new AtomicReference<>(); 245 try (Stream<Path> files = Files.list(runtimeDir.resolve("bin"))) { 246 files.filter(p -> Pattern.matches( 247 "^(vcruntime|msvcp|msvcr|ucrtbase|api-ms-win-).*\\.dll$", 248 p.toFile().getName().toLowerCase())) 249 .forEach(p -> { 250 try { 251 Files.copy(p, binDir.resolve((p.toFile().getName()))); 252 } catch (IOException e) { 253 ioe.set(e); 254 } 255 }); 256 } 257 258 IOException e = ioe.get(); 259 if (e != null) { 260 throw e; 261 } 262 } 263 264 private void validateValueAndPut( 265 Map<String, String> data, String key, 266 BundlerParamInfo<String> param, 267 Map<String, ? super Object> params) { 268 String value = param.fetchFrom(params); 269 if (value.contains("\r") || value.contains("\n")) { 270 Log.error("Configuration Parameter " + param.getID() 271 + " contains multiple lines of text, ignore it"); 272 data.put(key, ""); 273 return; 274 } 275 data.put(key, value); 276 } 277 278 protected void prepareExecutableProperties( 279 Map<String, ? super Object> params) throws IOException { 280 Map<String, String> data = new HashMap<>(); 281 282 // mapping Java parameters in strings for version resource 283 validateValueAndPut(data, "COMPANY_NAME", VENDOR, params); 284 validateValueAndPut(data, "FILE_DESCRIPTION", DESCRIPTION, params); 285 validateValueAndPut(data, "FILE_VERSION", VERSION, params); 286 data.put("INTERNAL_NAME", getLauncherName(params)); 287 validateValueAndPut(data, "LEGAL_COPYRIGHT", COPYRIGHT, params); 288 data.put("ORIGINAL_FILENAME", getLauncherName(params)); 289 validateValueAndPut(data, "PRODUCT_NAME", APP_NAME, params); 290 validateValueAndPut(data, "PRODUCT_VERSION", VERSION, params); 291 292 try (Writer w = Files.newBufferedWriter( 293 getConfig_ExecutableProperties(params).toPath(), 294 StandardCharsets.UTF_8)) { 295 String content = preprocessTextResource( 296 getConfig_ExecutableProperties(params).getName(), 297 I18N.getString("resource.executable-properties-template"), 298 EXECUTABLE_PROPERTIES_TEMPLATE, data, 299 VERBOSE.fetchFrom(params), 300 RESOURCE_DIR.fetchFrom(params)); 301 w.write(content); 302 } 303 } 304 305 private void createLauncherForEntryPoint( 306 Map<String, ? super Object> params) throws IOException { 307 308 File launcherIcon = ICON_ICO.fetchFrom(params); 309 File icon = launcherIcon != null ? 310 launcherIcon : ICON_ICO.fetchFrom(params); 311 File iconTarget = getConfig_AppIcon(params); 312 313 InputStream in = locateResource( 314 APP_NAME.fetchFrom(params) + ".ico", 315 "icon", 316 TEMPLATE_APP_ICON, 317 icon, 318 VERBOSE.fetchFrom(params), 319 RESOURCE_DIR.fetchFrom(params)); 320 321 Files.copy(in, iconTarget.toPath(), 322 StandardCopyOption.REPLACE_EXISTING); 323 324 writeCfgFile(params, root.resolve( 325 getLauncherCfgName(params)).toFile(), "$APPDIR\\runtime"); 326 327 prepareExecutableProperties(params); 328 329 // Copy executable to bin folder 330 Path executableFile = binDir.resolve(getLauncherName(params)); 331 332 try (InputStream is_launcher = 333 getResourceAsStream(getLauncherResourceName(params))) { 334 writeEntry(is_launcher, executableFile); 335 } 336 337 File launcher = executableFile.toFile(); 338 launcher.setWritable(true, true); 339 340 // Update branding of EXE file 341 if (REBRAND_EXECUTABLE.fetchFrom(params)) { 342 try { 343 String tempDirectory = WindowsDefender.getUserTempDirectory(); 344 if (Arguments.CLIOptions.context().userProvidedBuildRoot) { 345 tempDirectory = 346 TEMP_ROOT.fetchFrom(params).getAbsolutePath(); 347 } 348 if (WindowsDefender.isThereAPotentialWindowsDefenderIssue( 349 tempDirectory)) { 350 Log.error(MessageFormat.format(I18N.getString( 351 "message.potential.windows.defender.issue"), 352 tempDirectory)); 353 } 354 355 launcher.setWritable(true); 356 357 if (iconTarget.exists()) { 358 iconSwap(iconTarget.getAbsolutePath(), 359 launcher.getAbsolutePath()); 360 } 361 362 File executableProperties = 363 getConfig_ExecutableProperties(params); 364 365 if (executableProperties.exists()) { 366 if (versionSwap(executableProperties.getAbsolutePath(), 367 launcher.getAbsolutePath()) != 0) { 368 throw new RuntimeException(MessageFormat.format( 369 I18N.getString("error.version-swap"), 370 executableProperties.getAbsolutePath())); 371 } 372 } 373 } finally { 374 executableFile.toFile().setExecutable(true); 375 executableFile.toFile().setReadOnly(); 376 } 377 } 378 379 Files.copy(iconTarget.toPath(), 380 binDir.resolve(APP_NAME.fetchFrom(params) + ".ico")); 381 } 382 383 private void copyApplication(Map<String, ? super Object> params) 384 throws IOException { 385 List<RelativeFileSet> appResourcesList = 386 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 copyEntry(appDir, srcdir, fname); 397 } 398 } 399 } 400 401 private static native int iconSwap(String iconTarget, String launcher); 402 403 private static native int versionSwap(String executableProperties, String launcher); 404 405 }