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