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