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