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