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