src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java
branchJDK-8200758-branch
changeset 58994 b09ba68c6a19
parent 58791 fca9cb5f4953
equal deleted inserted replaced
58993:b5e1baa9d2c3 58994:b09ba68c6a19
       
     1 /*
       
     2  * Copyright (c) 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 package jdk.incubator.jpackage.internal;
       
    26 
       
    27 import java.io.*;
       
    28 import java.nio.file.InvalidPathException;
       
    29 import java.nio.file.Path;
       
    30 import java.text.MessageFormat;
       
    31 import java.util.*;
       
    32 import java.util.function.Function;
       
    33 import java.util.function.Predicate;
       
    34 import java.util.stream.Collectors;
       
    35 import java.util.stream.Stream;
       
    36 import static jdk.incubator.jpackage.internal.DesktopIntegration.*;
       
    37 import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
       
    38 import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
       
    39 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
       
    40 
       
    41 
       
    42 abstract class LinuxPackageBundler extends AbstractBundler {
       
    43 
       
    44     LinuxPackageBundler(BundlerParamInfo<String> packageName) {
       
    45         this.packageName = packageName;
       
    46     }
       
    47 
       
    48     @Override
       
    49     final public boolean validate(Map<String, ? super Object> params)
       
    50             throws ConfigException {
       
    51 
       
    52         // run basic validation to ensure requirements are met
       
    53         // we are not interested in return code, only possible exception
       
    54         APP_BUNDLER.fetchFrom(params).validate(params);
       
    55 
       
    56         validateInstallDir(LINUX_INSTALL_DIR.fetchFrom(params));
       
    57 
       
    58         validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
       
    59 
       
    60         // If package name has some restrictions, the string converter will
       
    61         // throw an exception if invalid
       
    62         packageName.getStringConverter().apply(packageName.fetchFrom(params),
       
    63             params);
       
    64 
       
    65         for (var validator: getToolValidators(params)) {
       
    66             ConfigException ex = validator.validate();
       
    67             if (ex != null) {
       
    68                 throw ex;
       
    69             }
       
    70         }
       
    71 
       
    72         withFindNeededPackages = LibProvidersLookup.supported();
       
    73         if (!withFindNeededPackages) {
       
    74             final String advice;
       
    75             if ("deb".equals(getID())) {
       
    76                 advice = "message.deb-ldd-not-available.advice";
       
    77             } else {
       
    78                 advice = "message.rpm-ldd-not-available.advice";
       
    79             }
       
    80             // Let user know package dependencies will not be generated.
       
    81             Log.error(String.format("%s\n%s", I18N.getString(
       
    82                     "message.ldd-not-available"), I18N.getString(advice)));
       
    83         }
       
    84 
       
    85         // Packaging specific validation
       
    86         doValidate(params);
       
    87 
       
    88         return true;
       
    89     }
       
    90 
       
    91     @Override
       
    92     final public String getBundleType() {
       
    93         return "INSTALLER";
       
    94     }
       
    95 
       
    96     @Override
       
    97     final public File execute(Map<String, ? super Object> params,
       
    98             File outputParentDir) throws PackagerException {
       
    99         IOUtils.writableOutputDir(outputParentDir.toPath());
       
   100 
       
   101         PlatformPackage thePackage = createMetaPackage(params);
       
   102 
       
   103         Function<File, ApplicationLayout> initAppImageLayout = imageRoot -> {
       
   104             ApplicationLayout layout = appImageLayout(params);
       
   105             layout.pathGroup().setPath(new Object(),
       
   106                     AppImageFile.getPathInAppImage(Path.of("")));
       
   107             return layout.resolveAt(imageRoot.toPath());
       
   108         };
       
   109 
       
   110         try {
       
   111             File appImage = StandardBundlerParam.getPredefinedAppImage(params);
       
   112 
       
   113             // we either have an application image or need to build one
       
   114             if (appImage != null) {
       
   115                 initAppImageLayout.apply(appImage).copy(
       
   116                         thePackage.sourceApplicationLayout());
       
   117             } else {
       
   118                 appImage = APP_BUNDLER.fetchFrom(params).doBundle(params,
       
   119                         thePackage.sourceRoot().toFile(), true);
       
   120                 ApplicationLayout srcAppLayout = initAppImageLayout.apply(
       
   121                         appImage);
       
   122                 if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) {
       
   123                     // Application image points to run-time image.
       
   124                     // Copy it.
       
   125                     srcAppLayout.copy(thePackage.sourceApplicationLayout());
       
   126                 } else {
       
   127                     // Application image is a newly created directory tree.
       
   128                     // Move it.
       
   129                     srcAppLayout.move(thePackage.sourceApplicationLayout());
       
   130                     if (appImage.exists()) {
       
   131                         // Empty app image directory might remain after all application
       
   132                         // directories have been moved.
       
   133                         appImage.delete();
       
   134                     }
       
   135                 }
       
   136             }
       
   137 
       
   138             if (!StandardBundlerParam.isRuntimeInstaller(params)) {
       
   139                 desktopIntegration = new DesktopIntegration(thePackage, params);
       
   140             } else {
       
   141                 desktopIntegration = null;
       
   142             }
       
   143 
       
   144             Map<String, String> data = createDefaultReplacementData(params);
       
   145             if (desktopIntegration != null) {
       
   146                 data.putAll(desktopIntegration.create());
       
   147             } else {
       
   148                 Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL,
       
   149                         UTILITY_SCRIPTS).forEach(v -> data.put(v, ""));
       
   150             }
       
   151 
       
   152             data.putAll(createReplacementData(params));
       
   153 
       
   154             File packageBundle = buildPackageBundle(Collections.unmodifiableMap(
       
   155                     data), params, outputParentDir);
       
   156 
       
   157             verifyOutputBundle(params, packageBundle.toPath()).stream()
       
   158                     .filter(Objects::nonNull)
       
   159                     .forEachOrdered(ex -> {
       
   160                 Log.verbose(ex.getLocalizedMessage());
       
   161                 Log.verbose(ex.getAdvice());
       
   162             });
       
   163 
       
   164             return packageBundle;
       
   165         } catch (IOException ex) {
       
   166             Log.verbose(ex);
       
   167             throw new PackagerException(ex);
       
   168         }
       
   169     }
       
   170 
       
   171     private List<String> getListOfNeededPackages(
       
   172             Map<String, ? super Object> params) throws IOException {
       
   173 
       
   174         PlatformPackage thePackage = createMetaPackage(params);
       
   175 
       
   176         final List<String> xdgUtilsPackage;
       
   177         if (desktopIntegration != null) {
       
   178             xdgUtilsPackage = desktopIntegration.requiredPackages();
       
   179         } else {
       
   180             xdgUtilsPackage = Collections.emptyList();
       
   181         }
       
   182 
       
   183         final List<String> neededLibPackages;
       
   184         if (withFindNeededPackages) {
       
   185             LibProvidersLookup lookup = new LibProvidersLookup();
       
   186             initLibProvidersLookup(params, lookup);
       
   187 
       
   188             neededLibPackages = lookup.execute(thePackage.sourceRoot());
       
   189         } else {
       
   190             neededLibPackages = Collections.emptyList();
       
   191         }
       
   192 
       
   193         // Merge all package lists together.
       
   194         // Filter out empty names, sort and remove duplicates.
       
   195         List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap(
       
   196                 List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect(
       
   197                 Collectors.toList());
       
   198 
       
   199         Log.verbose(String.format("Required packages: %s", result));
       
   200 
       
   201         return result;
       
   202     }
       
   203 
       
   204     private Map<String, String> createDefaultReplacementData(
       
   205             Map<String, ? super Object> params) throws IOException {
       
   206         Map<String, String> data = new HashMap<>();
       
   207 
       
   208         data.put("APPLICATION_PACKAGE", createMetaPackage(params).name());
       
   209         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
       
   210         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
       
   211         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
       
   212         data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
       
   213 
       
   214         String defaultDeps = String.join(", ", getListOfNeededPackages(params));
       
   215         String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip();
       
   216         if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
       
   217             customDeps = ", " + customDeps;
       
   218         }
       
   219         data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
       
   220         data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
       
   221 
       
   222         return data;
       
   223     }
       
   224 
       
   225     abstract protected List<ConfigException> verifyOutputBundle(
       
   226             Map<String, ? super Object> params, Path packageBundle);
       
   227 
       
   228     abstract protected void initLibProvidersLookup(
       
   229             Map<String, ? super Object> params,
       
   230             LibProvidersLookup libProvidersLookup);
       
   231 
       
   232     abstract protected List<ToolValidator> getToolValidators(
       
   233             Map<String, ? super Object> params);
       
   234 
       
   235     abstract protected void doValidate(Map<String, ? super Object> params)
       
   236             throws ConfigException;
       
   237 
       
   238     abstract protected Map<String, String> createReplacementData(
       
   239             Map<String, ? super Object> params) throws IOException;
       
   240 
       
   241     abstract protected File buildPackageBundle(
       
   242             Map<String, String> replacementData,
       
   243             Map<String, ? super Object> params, File outputParentDir) throws
       
   244             PackagerException, IOException;
       
   245 
       
   246     final protected PlatformPackage createMetaPackage(
       
   247             Map<String, ? super Object> params) {
       
   248         return new PlatformPackage() {
       
   249             @Override
       
   250             public String name() {
       
   251                 return packageName.fetchFrom(params);
       
   252             }
       
   253 
       
   254             @Override
       
   255             public Path sourceRoot() {
       
   256                 return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath();
       
   257             }
       
   258 
       
   259             @Override
       
   260             public ApplicationLayout sourceApplicationLayout() {
       
   261                 return appImageLayout(params).resolveAt(
       
   262                         applicationInstallDir(sourceRoot()));
       
   263             }
       
   264 
       
   265             @Override
       
   266             public ApplicationLayout installedApplicationLayout() {
       
   267                 return appImageLayout(params).resolveAt(
       
   268                         applicationInstallDir(Path.of("/")));
       
   269             }
       
   270 
       
   271             private Path applicationInstallDir(Path root) {
       
   272                 Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params),
       
   273                         name());
       
   274                 if (installDir.isAbsolute()) {
       
   275                     installDir = Path.of("." + installDir.toString()).normalize();
       
   276                 }
       
   277                 return root.resolve(installDir);
       
   278             }
       
   279         };
       
   280     }
       
   281 
       
   282     private ApplicationLayout appImageLayout(
       
   283             Map<String, ? super Object> params) {
       
   284         if (StandardBundlerParam.isRuntimeInstaller(params)) {
       
   285             return ApplicationLayout.javaRuntime();
       
   286         }
       
   287         return ApplicationLayout.linuxAppImage();
       
   288     }
       
   289 
       
   290     private static void validateInstallDir(String installDir) throws
       
   291             ConfigException {
       
   292         if (installDir.startsWith("/usr/") || installDir.equals("/usr")) {
       
   293             throw new ConfigException(MessageFormat.format(I18N.getString(
       
   294                     "error.unsupported-install-dir"), installDir), null);
       
   295         }
       
   296 
       
   297         if (installDir.isEmpty()) {
       
   298             throw new ConfigException(MessageFormat.format(I18N.getString(
       
   299                     "error.invalid-install-dir"), "/"), null);
       
   300         }
       
   301 
       
   302         boolean valid = false;
       
   303         try {
       
   304             final Path installDirPath = Path.of(installDir);
       
   305             valid = installDirPath.isAbsolute();
       
   306             if (valid && !installDirPath.normalize().toString().equals(
       
   307                     installDirPath.toString())) {
       
   308                 // Don't allow '/opt/foo/..' or /opt/.
       
   309                 valid = false;
       
   310             }
       
   311         } catch (InvalidPathException ex) {
       
   312         }
       
   313 
       
   314         if (!valid) {
       
   315             throw new ConfigException(MessageFormat.format(I18N.getString(
       
   316                     "error.invalid-install-dir"), installDir), null);
       
   317         }
       
   318     }
       
   319 
       
   320     private static void validateFileAssociations(
       
   321             List<Map<String, ? super Object>> associations) throws
       
   322             ConfigException {
       
   323         // only one mime type per association, at least one file extention
       
   324         int assocIdx = 0;
       
   325         for (var assoc : associations) {
       
   326             ++assocIdx;
       
   327             List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
       
   328             if (mimes == null || mimes.isEmpty()) {
       
   329                 String msgKey = "error.no-content-types-for-file-association";
       
   330                 throw new ConfigException(
       
   331                         MessageFormat.format(I18N.getString(msgKey), assocIdx),
       
   332                         I18N.getString(msgKey + ".advise"));
       
   333 
       
   334             }
       
   335 
       
   336             if (mimes.size() > 1) {
       
   337                 String msgKey = "error.too-many-content-types-for-file-association";
       
   338                 throw new ConfigException(
       
   339                         MessageFormat.format(I18N.getString(msgKey), assocIdx),
       
   340                         I18N.getString(msgKey + ".advise"));
       
   341             }
       
   342         }
       
   343     }
       
   344 
       
   345     private final BundlerParamInfo<String> packageName;
       
   346     private boolean withFindNeededPackages;
       
   347     private DesktopIntegration desktopIntegration;
       
   348 
       
   349     private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
       
   350         new StandardBundlerParam<>(
       
   351                 "linux.app.bundler",
       
   352                 LinuxAppBundler.class,
       
   353                 (params) -> new LinuxAppBundler(),
       
   354                 null
       
   355         );
       
   356 
       
   357 }